Skip to content

Commit

Permalink
Merge pull request dCache#7634 from ThePhisch/pyscript-configuration
Browse files Browse the repository at this point in the history
Improved Pyscript Auth Module
  • Loading branch information
kofemann authored Aug 9, 2024
2 parents e576065 + 591e361 commit 8ae7f59
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 106 deletions.
34 changes: 34 additions & 0 deletions docs/TheBook/src/main/markdown/config-gplazma.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<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
`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

Expand Down
44 changes: 20 additions & 24 deletions modules/gplazma2-pyscript/gplazma2-pyscript/sample_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand All @@ -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
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Path> 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");
}
Expand All @@ -49,7 +47,6 @@ public void authenticate(
Set<Object> privateCredentials,
Set<Principal> principals
) throws AuthenticationException {

for (Path pluginPath : pluginPathList) {
// for each path in pluginPathList
try {
Expand All @@ -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
Expand All @@ -136,11 +109,11 @@ public void authenticate(
// update principals
// =================
// Convert principals back from string representation
List<String> convertedPyPrincipals = List.copyOf(pyPrincipals);

// Update original Principals
principals.addAll(Subjects.principalsFromArgs(convertedPyPrincipals));

if (!pyPrincipals.isEmpty()) {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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<Object> publicCredentials = new HashSet<>();
publicCredentials.add("Tom");
publicCredentials.add(pwcred);

// Principals
Set<Principal> 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<Object> privateCredentials = new HashSet<>();
privateCredentials.add("Rose");
Set<Principal> 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<Object> 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<Principal> 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()
);
}
}
5 changes: 5 additions & 0 deletions packages/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,11 @@
<artifactId>gplazma2-htpasswd</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.dcache</groupId>
<artifactId>gplazma2-pyscript</artifactId>
<version>10.2.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.dcache</groupId>
<artifactId>dcache-frontend</artifactId>
Expand Down

0 comments on commit 8ae7f59

Please sign in to comment.