-
Notifications
You must be signed in to change notification settings - Fork 136
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
gplazma: add gplazma2-jython module for auth using Python (#7627)
Adds a new gplazma2 module to the project which uses Jython to parse and execute Python files (only version 2.7 supported) within the dcache Java runtime. This enables site admins to quickly script authentication plugins without needing to understand or write the native dcache Java code. This commit includes a sample "sample_plugin.py" auth plugin which is used during testing and serves as a reference. Implementation Details Jython limits the Python version to version 2.7. The Python files are currently stored directly within the source folder, i.e. `modules/gplazma2-jython/gplazma2-jython/sample_plugin.py`. Multiple files can be inserted which will be executed sequentially but in no particular order. Each Python file shall be structured so as to contain one function ´py_auth(public_credentials, private_credentials, principals)´ which returns a boolean whether the authentication was successful. Returning ´False´ will trigger an ´AuthenticationException´ in Java. Each parameter passed to ´py_auth´ will be a Python set. - ´private_credentials´ is a set of strings, integers or floats (implemented in Java as ´String || Number´ - ´public_credentials´ is the same as ´private_credentials´ - ´principals´ will be a set of strings in the form ´<principal_type>:<value>´, i.e. a pair of strings separated by a colon. Consider ´org.dcache.auth.Subjects#principalsFromArgs´to see how the principal type string maps to the principals classes. The conversion from a set of principals to a set of strings is done entirely within the ´authenticate´ method of the ´JythonPlugin´-class. The Python file only ever sees a set of strings, while someone using the plugin will only ever need to pass a set of principals from within Java. The conversion is done using existing functions. Tests A test class ´JythonPluginTest´ has been added that makes use of the internal logic of the sample ´sample_plugin.py´. The logic is simple and is repeated in the Java class and the Python file as a comment. Signed-off-by: Anton Schwarz <[email protected]>
- Loading branch information
Showing
6 changed files
with
347 additions
and
0 deletions.
There are no files selected for viewing
39 changes: 39 additions & 0 deletions
39
modules/gplazma2-pyscript/gplazma2-pyscript/sample_plugin.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
def py_auth(public_credentials, private_credentials, principals): | ||
""" | ||
Function py_auth, sample implementation for a gplazma2-pyscript plugin | ||
:param public_credentials: set of public auth credentials | ||
:param private_credentials: set of private auth credentials | ||
:param principals: set of principals (string values) | ||
:return: boolean whether authentication is permitted | ||
Note that the set of principals may only be added to, existing principals | ||
may never be removed! | ||
Note that the principals are handled as strings in Python! Consider | ||
org.dcache.auth.Subjects#principalsFromArgs how these are converted back | ||
into principals in Java. | ||
In case authentication is denied, we return None. | ||
In this sample implementation, the following logic is used: | ||
AUTHENTICATE: | ||
Condition: | ||
- Either "Tom" is in the public credentials | ||
- Or "Rose" is in the set of private credentials | ||
Result: | ||
- the username principal "Joad" is added (as string "username:joad") | ||
- we return True | ||
REFUSE: | ||
Condition: | ||
- Either "Connie" is in either one of the credentials | ||
- No passing condition from above is met | ||
Result: | ||
- we return False | ||
""" | ||
if ("Connie" in public_credentials) or ("Connie" in private_credentials): | ||
return False | ||
elif ("Tom" in public_credentials) or ("Rose" in private_credentials): | ||
principals.add("username:Joad") | ||
return True | ||
else: | ||
return False |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<parent> | ||
<groupId>org.dcache</groupId> | ||
<artifactId>dcache-parent</artifactId> | ||
<version>10.2.0-SNAPSHOT</version> | ||
<relativePath>../../pom.xml</relativePath> | ||
</parent> | ||
|
||
<artifactId>gplazma2-pyscript</artifactId> | ||
<packaging>jar</packaging> | ||
|
||
<name>gPlazma 2 Pyscript Generic plugin</name> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>org.slf4j</groupId> | ||
<artifactId>slf4j-api</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>com.google.guava</groupId> | ||
<artifactId>guava</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.python</groupId> | ||
<artifactId>jython-standalone</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.dcache</groupId> | ||
<artifactId>dcache-common</artifactId> | ||
<version>${project.version}</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.dcache</groupId> | ||
<artifactId>gplazma2</artifactId> | ||
<version>${project.version}</version> | ||
</dependency> | ||
</dependencies> | ||
</project> |
157 changes: 157 additions & 0 deletions
157
modules/gplazma2-pyscript/src/main/java/org/dcache/gplazma/pyscript/PyscriptPlugin.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
package org.dcache.gplazma.pyscript; | ||
|
||
import java.io.IOException; | ||
import java.nio.file.DirectoryStream; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.security.Principal; | ||
import java.util.Set; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
import java.util.Properties; | ||
import java.util.stream.Collectors; | ||
|
||
import org.dcache.auth.Subjects; | ||
import org.dcache.gplazma.AuthenticationException; | ||
import org.dcache.gplazma.plugins.GPlazmaAuthenticationPlugin; | ||
import org.python.core.PyFunction; | ||
import org.python.core.PyObject; | ||
import org.python.core.PySet; | ||
import org.python.util.PythonInterpreter; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
|
||
public class PyscriptPlugin implements GPlazmaAuthenticationPlugin { | ||
|
||
private static final Logger LOGGER = LoggerFactory.getLogger(PyscriptPlugin.class); | ||
|
||
private final PythonInterpreter PI = new PythonInterpreter(); | ||
private List<Path> pluginPathList = new ArrayList<>(); | ||
|
||
public PyscriptPlugin(Properties properties) { | ||
// constructor | ||
try { | ||
pluginPathList = listDirectoryContents("gplazma2-pyscript"); // TODO fix this, currently just reads from local folder | ||
} catch (IOException e) { | ||
LOGGER.error("Error reading Pyscript directory"); | ||
} | ||
} | ||
|
||
private static List<Path> listDirectoryContents(String directoryPath) throws IOException { | ||
return Files.list(Path.of(directoryPath)).toList(); | ||
} | ||
|
||
public void authenticate( | ||
Set<Object> publicCredentials, | ||
Set<Object> privateCredentials, | ||
Set<Principal> principals | ||
) throws AuthenticationException { | ||
|
||
for (Path pluginPath : pluginPathList) { | ||
// for each path in pluginPathList | ||
try { | ||
// =========== | ||
// IO | ||
// =========== | ||
// read file into string | ||
LOGGER.debug("gplazma2-pyscript running {}", pluginPath); | ||
String content = Files.lines(pluginPath).collect( | ||
Collectors.joining("\n") | ||
); | ||
// execute file content, loading the function "py_auth" into the namespace | ||
PI.exec(content); | ||
|
||
// ====================== | ||
// Prepare Python Data | ||
// ====================== | ||
// prepare datatypes for python | ||
PySet pyPublicCredentials = new PySet(); | ||
PySet pyPrivateCredentials = new PySet(); | ||
PySet pyPrincipals = new PySet(); | ||
|
||
// Only handle select datatypes | ||
for (Object pubCred : publicCredentials) { | ||
if ( | ||
pubCred instanceof String | ||
|| pubCred instanceof Number // all number datatypes | ||
) { | ||
pyPublicCredentials.add(pubCred); | ||
} else { | ||
LOGGER.warn( | ||
"gplazma2-pyscript: Public Credential {} dropped. Use String or Number.", | ||
pubCred | ||
); | ||
} | ||
} | ||
for (Object privCred : privateCredentials) { | ||
if ( | ||
privCred instanceof String | ||
|| privCred instanceof Number // all number datatypes | ||
) { | ||
pyPrivateCredentials.add(privCred); | ||
} else { | ||
LOGGER.warn( | ||
"gplazma2-pyscript: Private Credential {} dropped. Use String or Number.", | ||
privCred.toString() | ||
); | ||
} | ||
} | ||
|
||
// Convert principals into string representation | ||
String principalsString = Subjects.toString( | ||
Subjects.ofPrincipals(principals) | ||
); | ||
String[] principalStringList = principalsString.substring( | ||
1, principalsString.length() - 1 | ||
).split(", "); | ||
pyPrincipals.addAll(Arrays.asList(principalStringList)); | ||
|
||
|
||
|
||
// ============= | ||
// Run in Python | ||
// ============= | ||
// Get function | ||
PyFunction pluginAuthFunc = PI.get( | ||
"py_auth", | ||
PyFunction.class | ||
); | ||
// Invoke function | ||
PyObject pyAuthOutput = pluginAuthFunc.__call__( | ||
pyPublicCredentials, | ||
pyPrivateCredentials, | ||
pyPrincipals | ||
); | ||
// throw exception if needed | ||
Boolean AuthenticationPassed = (Boolean) pyAuthOutput.__tojava__(Boolean.class); | ||
if (!AuthenticationPassed) { | ||
throw new AuthenticationException( | ||
"Authentication failed in file " + pluginPath.getFileName().toString() | ||
); | ||
} | ||
// ================ | ||
// update principals | ||
// ================= | ||
// Convert principals back from string representation | ||
List<String> convertedPyPrincipals = List.copyOf(pyPrincipals); | ||
|
||
// Update original Principals | ||
principals.addAll(Subjects.principalsFromArgs(convertedPyPrincipals)); | ||
|
||
} catch (IOException e) { | ||
throw new AuthenticationException( | ||
"Authentication failed due to I/O error: " + e.getMessage(), e | ||
); | ||
} | ||
} | ||
|
||
|
||
|
||
// finish authentication | ||
LOGGER.debug("Finished gplazma2-pyscript step."); | ||
} | ||
|
||
} |
6 changes: 6 additions & 0 deletions
6
modules/gplazma2-pyscript/src/main/resources/META-INF/gplazma-plugins.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<plugins> | ||
<plugin> | ||
<name>pyscript</name> | ||
<class>org.dcache.gplazma.pyscript.PyscriptPlugin</class> | ||
</plugin> | ||
</plugins> |
104 changes: 104 additions & 0 deletions
104
modules/gplazma2-pyscript/src/test/java/org/dcache/gplazma/pyscript/PyscriptPluginTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
package org.dcache.gplazma.pyscript; | ||
|
||
import static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.hamcrest.Matchers.hasItems; | ||
|
||
import java.security.Principal; | ||
import java.util.*; | ||
|
||
import org.dcache.auth.UserNamePrincipal; | ||
import org.dcache.gplazma.AuthenticationException; | ||
import org.junit.Before; | ||
import org.junit.Test; | ||
|
||
public class PyscriptPluginTest { | ||
/* | ||
Test the gplazma2-pyscript module | ||
This module reads the Python (2.7) files provided and runs each auth attempt | ||
through the auth function defined in each file. Authentication may fail and | ||
throw an AuthenticationException, or it may pass and add new principals. | ||
In any case, the principals are only added to, never removed from. | ||
The internal logic of the "sample_plugin.py" file is the following | ||
AUTHENTICATE: | ||
Condition: | ||
- Either "Tom" is in the public credentials | ||
- Or "Rose" is in the set of private credentials | ||
Result: | ||
- the username principal "Joad" is added (as string "username:joad") | ||
- we return True | ||
REFUSE: | ||
Condition: | ||
- Either "Connie" is in either one of the credentials | ||
- No passing condition from above is met | ||
Result: | ||
- we return False | ||
These JUnit tests are written accordingly. | ||
*/ | ||
|
||
private static PyscriptPlugin plugin; | ||
|
||
@Before | ||
public void setUp() { | ||
plugin = new PyscriptPlugin(new Properties()); | ||
} | ||
|
||
@Test | ||
public void passesWithPublicCredential() throws Exception { | ||
Set<Object> publicCredentials = new HashSet<>(); | ||
publicCredentials.add("Tom"); | ||
Set<Principal> principals = new HashSet<>(); | ||
principals.add(new UserNamePrincipal("Casy")); | ||
plugin.authenticate( | ||
publicCredentials, | ||
Collections.emptySet(), | ||
principals | ||
); | ||
assertThat(principals, hasItems( | ||
new UserNamePrincipal("Joad"), // new principal added by plugin | ||
new UserNamePrincipal("Casy") // old principal still included | ||
)); | ||
} | ||
|
||
@Test | ||
public void passesWithPrivateCredential() throws Exception { | ||
Set<Object> privateCredentials = new HashSet<>(); | ||
privateCredentials.add("Rose"); | ||
Set<Principal> principals = new HashSet<>(); | ||
principals.add(new UserNamePrincipal("Casy")); | ||
plugin.authenticate( | ||
Collections.emptySet(), | ||
privateCredentials, | ||
principals | ||
); | ||
assertThat(principals, hasItems( | ||
new UserNamePrincipal("Joad"), // new principal added by plugin | ||
new UserNamePrincipal("Casy") // old principal still included | ||
)); | ||
} | ||
|
||
@Test(expected = AuthenticationException.class) | ||
public void failsOnNoParameters() throws Exception{ | ||
plugin.authenticate( | ||
Collections.emptySet(), | ||
Collections.emptySet(), | ||
Collections.emptySet() | ||
); | ||
} | ||
|
||
@Test(expected = AuthenticationException.class) | ||
public void failsOnNoParametersDespitePrincipals() throws Exception { | ||
Set<Principal> principals = new HashSet<>(); | ||
principals.add(new UserNamePrincipal("Ruthie")); | ||
principals.add(new UserNamePrincipal("Casy")); | ||
plugin.authenticate( | ||
Collections.emptySet(), | ||
Collections.emptySet(), | ||
principals | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters