From 591e361d2bdb921b0a3adc543cb7fb0de993c2a2 Mon Sep 17 00:00:00 2001 From: Anton Schwarz Date: Thu, 18 Jul 2024 19:37:31 +0200 Subject: [PATCH] Improved Pyscript Auth Module Auth plugin can use any kind of credentials now: the python script takes a set of Java credential objects (instead of native Python primitives). No conversion takes place, Python scripts need to be written such that they can deal with the Java objects themselves. - Added documentation to TheBook - Made the module configurable (workdir can be set via `gplazma.pyscript.workdir`) Signed-off-by: Anton Schwarz anton.schwarz@desy.de --- .../src/main/markdown/config-gplazma.md | 34 +++++++++ .../gplazma2-pyscript/sample_plugin.py | 44 ++++++------ .../gplazma/pyscript/PyscriptPlugin.java | 69 ++++++------------- .../gplazma/pyscript/PyscriptPluginTest.java | 65 +++++++++-------- packages/pom.xml | 5 ++ 5 files changed, 111 insertions(+), 106 deletions(-) diff --git a/docs/TheBook/src/main/markdown/config-gplazma.md b/docs/TheBook/src/main/markdown/config-gplazma.md index 2705e034218..f37c89e85bd 100644 --- a/docs/TheBook/src/main/markdown/config-gplazma.md +++ b/docs/TheBook/src/main/markdown/config-gplazma.md @@ -556,6 +556,40 @@ In the OP definition: allows for custom behaviour if the token is adhering to dCache's authorisation model. +##### pyscript + +A gplazma2 auth module which uses the Jython package to run Python files +from within the Java runtime used for dCache. It is intended for site +admins who want to quickly script authentication plugins without needing +to understand or write the native dCache Java code. + +*Implementation Details* + +Jython limits the Python version to version 2.7. Multiple files can be inserted which will +be executed sequentially but in no particular order. + +Each Python file shall be structured 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 Java credential objects. +- `public_credentials` is the same as `private_credentials` +- `principals` will be a set of strings in the form `:`, 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 +`PyscriptPlugin`-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. + +You will need to write Python code to handle the credentials properly. Note that the credentials will *not* come as +primitives but as objects. Read the [Jython](https://javadoc.io/doc/org.python/jython/latest/index.html) documentation +on how to handle Java objects from within Python. + +You will need to set the property `gplazma.pyscript.workdir` which tells the interpreter where to search for the Python +files. + + #### map Plug-ins diff --git a/modules/gplazma2-pyscript/gplazma2-pyscript/sample_plugin.py b/modules/gplazma2-pyscript/gplazma2-pyscript/sample_plugin.py index 06060cbf893..c438f5a7c31 100644 --- a/modules/gplazma2-pyscript/gplazma2-pyscript/sample_plugin.py +++ b/modules/gplazma2-pyscript/gplazma2-pyscript/sample_plugin.py @@ -4,7 +4,8 @@ def py_auth(public_credentials, private_credentials, principals): :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 + :return: boolean whether authentication is permitted. The principals are + changed as a side effect. Note that the set of principals may only be added to, existing principals may never be removed! @@ -13,27 +14,22 @@ def py_auth(public_credentials, private_credentials, principals): 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 + In case authentication is denied, 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 \ No newline at end of file + list_accepted = [ + ("admin", "dickerelch"), + ("Dust", "Bowl") + ] + for pubcred in public_credentials: + # public credentials iterated in random order + # first username:password combination in list of accepted credentials leads to acceptance + if (pubcred.getUsername(), pubcred.getPassword()) in list_accepted: + principals.add("username:%s" % (pubcred.getUsername())) # Python 2-style String formatting + return True + for privcred in private_credentials: + # public credentials iterated in random order + # first username:password combination in list of accepted credentials leads to acceptance + if (privcred.getUsername(), privcred.getPassword()) in list_accepted: + principals.add("username:%s" % (privcred.getUsername())) # Python 2-style String formatting + return True + return False \ No newline at end of file diff --git a/modules/gplazma2-pyscript/src/main/java/org/dcache/gplazma/pyscript/PyscriptPlugin.java b/modules/gplazma2-pyscript/src/main/java/org/dcache/gplazma/pyscript/PyscriptPlugin.java index ba8f4860c6c..811f610e406 100644 --- a/modules/gplazma2-pyscript/src/main/java/org/dcache/gplazma/pyscript/PyscriptPlugin.java +++ b/modules/gplazma2-pyscript/src/main/java/org/dcache/gplazma/pyscript/PyscriptPlugin.java @@ -6,11 +6,7 @@ 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.*; import java.util.stream.Collectors; import org.dcache.auth.Subjects; @@ -29,12 +25,14 @@ public class PyscriptPlugin implements GPlazmaAuthenticationPlugin { private static final Logger LOGGER = LoggerFactory.getLogger(PyscriptPlugin.class); private final PythonInterpreter PI = new PythonInterpreter(); + private final String pluginDirectoryString; private List pluginPathList = new ArrayList<>(); public PyscriptPlugin(Properties properties) { // constructor + pluginDirectoryString = properties.getProperty("gplazma.pyscript.workdir"); try { - pluginPathList = listDirectoryContents("gplazma2-pyscript"); // TODO fix this, currently just reads from local folder + pluginPathList = listDirectoryContents(pluginDirectoryString); } catch (IOException e) { LOGGER.error("Error reading Pyscript directory"); } @@ -49,7 +47,6 @@ public void authenticate( Set privateCredentials, Set principals ) throws AuthenticationException { - for (Path pluginPath : pluginPathList) { // for each path in pluginPathList try { @@ -72,44 +69,20 @@ public void authenticate( 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() - ); - } - } + // add credentials regardless of type -> type handling is done in Python! + pyPublicCredentials.addAll(publicCredentials); + pyPrivateCredentials.addAll(privateCredentials); // 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)); - - + if (!principals.isEmpty()) { + String principalsString = Subjects.toString( + Subjects.ofPrincipals(principals) + ); + String[] principalStringList = principalsString.substring( + 1, principalsString.length() - 1 + ).split(", "); + pyPrincipals.addAll(Arrays.asList(principalStringList)); + } // ============= // Run in Python @@ -136,11 +109,11 @@ public void authenticate( // update principals // ================= // Convert principals back from string representation - List convertedPyPrincipals = List.copyOf(pyPrincipals); - - // Update original Principals - principals.addAll(Subjects.principalsFromArgs(convertedPyPrincipals)); - + if (!pyPrincipals.isEmpty()) { + List 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 diff --git a/modules/gplazma2-pyscript/src/test/java/org/dcache/gplazma/pyscript/PyscriptPluginTest.java b/modules/gplazma2-pyscript/src/test/java/org/dcache/gplazma/pyscript/PyscriptPluginTest.java index 8895351d04c..6bc4edbb8a1 100644 --- a/modules/gplazma2-pyscript/src/test/java/org/dcache/gplazma/pyscript/PyscriptPluginTest.java +++ b/modules/gplazma2-pyscript/src/test/java/org/dcache/gplazma/pyscript/PyscriptPluginTest.java @@ -6,6 +6,7 @@ import java.security.Principal; import java.util.*; +import org.dcache.auth.PasswordCredential; import org.dcache.auth.UserNamePrincipal; import org.dcache.gplazma.AuthenticationException; import org.junit.Before; @@ -25,15 +26,14 @@ This module reads the Python (2.7) files provided and runs each auth attempt AUTHENTICATE: Condition: - - Either "Tom" is in the public credentials - - Or "Rose" is in the set of private credentials + - public_credentials contains at least one username:password combination + that is accepted (admin:dickerelch, Dust:Bowl) Result: - - the username principal "Joad" is added (as string "username:joad") + - We add the username principal corresponding to the accepted username - we return True REFUSE: Condition: - - Either "Connie" is in either one of the credentials - - No passing condition from above is met + - The above condition is not met. Result: - we return False @@ -44,61 +44,58 @@ This module reads the Python (2.7) files provided and runs each auth attempt @Before public void setUp() { - plugin = new PyscriptPlugin(new Properties()); + // Properties + Properties properties = new Properties(); + properties.setProperty("gplazma.pyscript.workdir", "gplazma2-pyscript"); + plugin = new PyscriptPlugin(properties); } @Test - public void passesWithPublicCredential() throws Exception { + public void passesWithPasswordCredential() throws Exception { + // Credentials + PasswordCredential pwcred = new PasswordCredential("Dust", "Bowl"); Set publicCredentials = new HashSet<>(); - publicCredentials.add("Tom"); + publicCredentials.add(pwcred); + + // Principals Set principals = new HashSet<>(); - principals.add(new UserNamePrincipal("Casy")); + principals.add(new UserNamePrincipal("joad")); + + // Execution 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 privateCredentials = new HashSet<>(); - privateCredentials.add("Rose"); - Set principals = new HashSet<>(); - principals.add(new UserNamePrincipal("Casy")); - plugin.authenticate( - Collections.emptySet(), - privateCredentials, - principals - ); + // Assert that principals has been added to assertThat(principals, hasItems( - new UserNamePrincipal("Joad"), // new principal added by plugin - new UserNamePrincipal("Casy") // old principal still included + new UserNamePrincipal("Dust"), + new UserNamePrincipal("joad") )); } @Test(expected = AuthenticationException.class) - public void failsOnNoParameters() throws Exception{ + public void failsOnBadPasswordCredential() throws Exception { + // Credentials + PasswordCredential pwcred = new PasswordCredential("Dustin", "Hoffman"); + Set publicCredentials = new HashSet<>(); + publicCredentials.add(pwcred); + + // Execution plugin.authenticate( - Collections.emptySet(), + publicCredentials, Collections.emptySet(), Collections.emptySet() ); } @Test(expected = AuthenticationException.class) - public void failsOnNoParametersDespitePrincipals() throws Exception { - Set principals = new HashSet<>(); - principals.add(new UserNamePrincipal("Ruthie")); - principals.add(new UserNamePrincipal("Casy")); + public void failsOnNoParameters() throws Exception{ plugin.authenticate( Collections.emptySet(), Collections.emptySet(), - principals + Collections.emptySet() ); } } diff --git a/packages/pom.xml b/packages/pom.xml index b7be98cd99e..a1b27d568d9 100644 --- a/packages/pom.xml +++ b/packages/pom.xml @@ -190,6 +190,11 @@ gplazma2-htpasswd ${project.version} + + org.dcache + gplazma2-pyscript + 10.2.0-SNAPSHOT + org.dcache dcache-frontend