Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal for predefined key access #169

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions posix-mapper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,29 @@ additional path elements (see [war-rename.conf](https://github.com/opencadc/dock
### configuration
The following runtime configuration must be made available via the `/config` directory.

### Key access (Optional, but required for certain clients)
The POSIX Mapper requires authentication for access, but not all clients will have an authenticated user in hand. To
facilitate this, the `/config` folder can contain a `keys` folder with key files inside`:

`/config/keys/service-a-api-key`:
```
MYSECRETKEYVALUE
```

`/config/keys/service-b-api-key`:
```
ANOTHERSECRETVALUE
```

Where Kubernetes is concerned, it's advisable to create a Secret with this value and mount it to the POSIX Mapper,
as well as any clients that need access to the POSIX Mapper service without an authenticated Subject.

Access for clients will involve setting the `authorization: api-key <token-value>` header to the value in the file:

```shell
$ curl --header "authorization: api-key MYSECRETKEYVALUE" https://example.org/posix-mapper/uid
```

### catalina.properties
This file contains java system properties to configure the tomcat server and some of the java libraries used in the service.

Expand Down Expand Up @@ -48,19 +71,18 @@ org.opencadc.posix.mapper.resourceID=ivo://{authority}/{name}
# Database schema
org.opencadc.posix.mapper.schema=mapping

# home dir root
org.opencadc.posix.mapper.homeDirRoot=/storage/home

# ID ranges to allow some customization where administration is necessary
org.opencadc.posix.mapper.uid.start=10000
org.opencadc.posix.mapper.gid.start=90000

# At least one group that are allowed to query the API. Use multiple org.opencadc.posix.mapper.group entries for
pdowler marked this conversation as resolved.
Show resolved Hide resolved
# multiple groups.
org.opencadc.posix.mapper.group=ivo://example.org/gms?mygroup
```
The _resourceID_ is the resourceID of _this_ posix-mapper service.

The _schema_ is the database schema used for interacting with tables in the database.

The _homeDirRoot_ is the path to the root of home folders. This is used to create entires in the `/etc/passwd` file.

_uid.start_ start of UID range
_gid.start_ start of GID range

Expand Down
2 changes: 1 addition & 1 deletion posix-mapper/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
# tags with and without build number so operators use the versioned
# tag but we always keep a timestamped tag in case a semantic tag gets
# replaced accidentally
VER=0.2.1
VER=0.2.2
TAGS="${VER} ${VER}-$(date -u +"%Y%m%dT%H%M%S")"
unset VER
1 change: 0 additions & 1 deletion posix-mapper/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ dependencies {
intTestCompile 'org.apache.commons:commons-lang3:3.13.0'

runtimeOnly 'org.opencadc:cadc-gms:[1.0.12,2.0)'
runtimeOnly 'org.opencadc:cadc-access-control-identity:[1.0.3,2.0)'
runtimeOnly 'org.postgresql:postgresql:[42.6.0,)'

// work around because 1.8.0-beta4 prints exceptions in log, eg:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package org.opencadc.posix.mapper.auth;

import ca.nrc.cadc.auth.*;

import javax.security.auth.Subject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.net.URI;
import java.util.*;
import java.util.stream.Collectors;


/**
* Special IdentityManager to enable delegating most tasks to the configured IdentityManager, but with the special
* case to validate pre-agreed upon API keys.
*/
public class DelegatingAPIKeyIdentityManager implements IdentityManager {
public static final File DEFAULT_KEY_FOLDER = new File(System.getProperty("user.home") + "/config/keys");
public static final String AUTHORIZATION_KEY = "api-key";

// Ugly way to store the currently delegated identity manager. This is set by this service's one-time listener.
public static IdentityManager DELEGATED_IDENTITY_MANAGER;


@Override
public Set<URI> getSecurityMethods() {
return Set.of();
}

@Override
public Subject validate(final Subject subject) throws NotAuthenticatedException {
// Will be null for API Key tokens.
final AuthMethod authMethod = AuthenticationUtil.getAuthMethod(subject);

// Any header with the Authorization key.
final Set<AuthorizationTokenPrincipal> rawAPIKeyTokens =
subject.getPrincipals(AuthorizationTokenPrincipal.class)
.stream()
.filter(token -> {
final String value = token.getHeaderValue();
return value != null
&& value.trim().toLowerCase()
.startsWith(DelegatingAPIKeyIdentityManager.AUTHORIZATION_KEY);
})
.collect(Collectors.toSet());

if (!rawAPIKeyTokens.isEmpty() && authMethod == null) {
final File[] keyFiles = DelegatingAPIKeyIdentityManager.DEFAULT_KEY_FOLDER.listFiles();
if (keyFiles != null) {
final Map<String, String> matchingKeyFiles = new HashMap<>();
for (final File keyFile : keyFiles) {
if (keyFile.isFile() && keyFile.canRead()) {
try (final BufferedReader reader = new BufferedReader(new FileReader(keyFile))) {
// Token value from the file.
final String line = reader.readLine();
if (rawAPIKeyTokens
.stream()
.map(token -> {
final String value = token.getHeaderValue().trim();
return value.substring(
DelegatingAPIKeyIdentityManager.AUTHORIZATION_KEY.length()).trim();
})
.anyMatch(token -> token.equals(line))) {
matchingKeyFiles.put(keyFile.getName(), line);
subject.getPrincipals().removeAll(rawAPIKeyTokens);
}
} catch (IOException ioException) {
throw new IllegalStateException(ioException.getMessage(), ioException);
}
}
}

// TODO: Verify client calling is what the key says it is.
if (matchingKeyFiles.isEmpty()) {
throw new NotAuthenticatedException("No API Keys matching.");
} else {
matchingKeyFiles.forEach((key, value) -> {
final AuthorizationToken authorizationToken =
new AuthorizationToken("bearer", value, Collections.emptyList(), null);
subject.getPublicCredentials().add(authorizationToken);
});

return subject;
}
} else {
throw new IllegalStateException("No Key files available in "
+ DelegatingAPIKeyIdentityManager.DEFAULT_KEY_FOLDER);
}
} else {
return DelegatingAPIKeyIdentityManager.DELEGATED_IDENTITY_MANAGER.validate(subject);
}
}

@Override
public Subject augment(Subject subject) {
return DelegatingAPIKeyIdentityManager.DELEGATED_IDENTITY_MANAGER.augment(subject);
}

@Override
public Subject toSubject(Object o) {
return DelegatingAPIKeyIdentityManager.DELEGATED_IDENTITY_MANAGER.toSubject(o);
}

@Override
public Object toOwner(Subject subject) {
return DelegatingAPIKeyIdentityManager.DELEGATED_IDENTITY_MANAGER.toOwner(subject);
}

@Override
public String toDisplayString(Subject subject) {
return DelegatingAPIKeyIdentityManager.DELEGATED_IDENTITY_MANAGER.toDisplayString(subject);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.opencadc.posix.mapper.web;

import ca.nrc.cadc.auth.AuthenticationUtil;
import ca.nrc.cadc.auth.IdentityManager;
import org.apache.log4j.Logger;
import org.opencadc.posix.mapper.auth.DelegatingAPIKeyIdentityManager;


/**
* Special InitAction class for performing an application-wide one-time operation. Ensure it's only configured on ONE servlet!
*/
public class DelegatingPosixInitAction extends PosixInitAction {
private static final Logger LOGGER = Logger.getLogger(DelegatingPosixInitAction.class);

private static void delegateIdentityManager() {
LOGGER.debug("delegateIdentityManager: START");
DelegatingAPIKeyIdentityManager.DELEGATED_IDENTITY_MANAGER = AuthenticationUtil.getIdentityManager();
System.setProperty(IdentityManager.class.getName(), DelegatingAPIKeyIdentityManager.class.getName());
LOGGER.debug("delegateIdentityManager (IdentityManager now set to " + System.getProperty(IdentityManager.class.getName()) + "): OK");
}

private static void resetIdentityManager() {
LOGGER.debug("resetIdentityManager: START");
// Reset.
System.setProperty(IdentityManager.class.getName(), DelegatingAPIKeyIdentityManager.DELEGATED_IDENTITY_MANAGER.getClass().getName());
DelegatingAPIKeyIdentityManager.DELEGATED_IDENTITY_MANAGER = null;
LOGGER.debug("resetIdentityManager: OK");
}

@Override
public void doInit() {
super.doInit();
DelegatingPosixInitAction.delegateIdentityManager();
}

@Override
public void doShutdown() {
super.doShutdown();
DelegatingPosixInitAction.resetIdentityManager();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ public class PosixInitAction extends InitAction {

static final String RESOURCE_ID_KEY = PosixInitAction.POSIX_KEY + ".resourceID";

// Add multiples of these to the Properties file.
static final String ALLOWED_GROUPS_KEY = PosixInitAction.POSIX_KEY + ".group";

static final String[] CHECK_CONFIG_KEYS = new String[] {
PosixInitAction.SCHEMA_KEY, PosixInitAction.RESOURCE_ID_KEY,
PosixInitAction.UID_START_KEY, PosixInitAction.GID_START_KEY
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,14 @@

import ca.nrc.cadc.auth.AuthMethod;
import ca.nrc.cadc.auth.AuthenticationUtil;
import ca.nrc.cadc.auth.HttpPrincipal;
import ca.nrc.cadc.auth.NotAuthenticatedException;
import ca.nrc.cadc.auth.PosixPrincipal;
import ca.nrc.cadc.rest.InlineContentHandler;
import ca.nrc.cadc.rest.RestAction;
import ca.nrc.cadc.util.MultiValuedProperties;
import org.opencadc.gms.GroupURI;
import org.opencadc.gms.IvoaGroupClient;
import org.opencadc.posix.mapper.Group;
import org.opencadc.posix.mapper.PosixClient;
import org.opencadc.posix.mapper.Postgres;
Expand All @@ -91,6 +95,11 @@
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.URI;
import java.security.PrivilegedExceptionAction;
import java.util.Set;
import java.util.stream.Collectors;


public abstract class PosixMapperAction extends RestAction {

Expand All @@ -113,18 +122,44 @@ public void initAction() throws Exception {
checkAuthorization();
}

private void checkAuthorization() {
private void checkAuthorization() throws Exception {
final Subject currentUser = AuthenticationUtil.getCurrentSubject();
if (currentUser.getPublicCredentials(AuthMethod.class).contains(AuthMethod.ANON)) {
final AuthMethod authMethod = AuthenticationUtil.getAuthMethod(currentUser);
if (AuthMethod.ANON.equals(authMethod)) {
throw new NotAuthenticatedException("Caller is not authenticated.");
} else {
// Standard validate() will provide NumericPrincipal and HTTPPrincipal. If they are missing, assume
// API Key access and no Group check.
final boolean missingPosixPrincipal = currentUser.getPrincipals(PosixPrincipal.class).isEmpty();
final boolean missingHTTPPrincipal = currentUser.getPrincipals(HttpPrincipal.class).isEmpty();
final boolean tokenFromAPIKey = AuthMethod.TOKEN.equals(authMethod) && missingPosixPrincipal
&& missingHTTPPrincipal;

if (!tokenFromAPIKey) {
checkGroupReadAccess(currentUser);
}
}
}

private void checkGroupReadAccess(final Subject currentUser) throws Exception {
final IvoaGroupClient ivoaGroupClient = new IvoaGroupClient();
final Set<GroupURI> allowedGroupURIs =
PosixMapperAction.POSIX_CONFIGURATION.getProperty(PosixInitAction.ALLOWED_GROUPS_KEY)
.stream()
.map(groupURIString -> new GroupURI(URI.create(groupURIString)))
.collect(Collectors.toSet());

Subject.doAs(currentUser, (PrivilegedExceptionAction<? extends Void>) () -> {
if (ivoaGroupClient.getMemberships(allowedGroupURIs).isEmpty()) {
throw new NotAuthenticatedException("Not authorized to use the POSIX Mapper service.");
} else {
return null;
}
});
}

protected GroupWriter getGroupWriter() throws IOException {
final String requestContentType = syncInput.getHeader("accept");
final String writeContentType = PosixMapperAction.TSV_CONTENT_TYPE.equals(requestContentType)
? PosixMapperAction.TSV_CONTENT_TYPE : "text/plain";
this.syncOutput.addHeader("content-type", writeContentType);
final String writeContentType = setContentType();
final Writer writer = new BufferedWriter(new OutputStreamWriter(this.syncOutput.getOutputStream()));
if (PosixMapperAction.TSV_CONTENT_TYPE.equals(writeContentType)) {
return new TSVGroupWriter(writer);
Expand All @@ -134,10 +169,7 @@ protected GroupWriter getGroupWriter() throws IOException {
}

protected UserWriter getUserWriter() throws IOException {
final String requestContentType = syncInput.getHeader("accept");
final String writeContentType = PosixMapperAction.TSV_CONTENT_TYPE.equals(requestContentType)
? PosixMapperAction.TSV_CONTENT_TYPE : "text/plain";
this.syncOutput.addHeader("content-type", writeContentType);
final String writeContentType = setContentType();
final Writer writer = new BufferedWriter(new OutputStreamWriter(this.syncOutput.getOutputStream()));
if (PosixMapperAction.TSV_CONTENT_TYPE.equals(writeContentType)) {
return new TSVUserWriter(writer);
Expand All @@ -146,6 +178,15 @@ protected UserWriter getUserWriter() throws IOException {
}
}

private String setContentType() {
final String requestContentType = syncInput.getHeader("accept");
final String writeContentType = PosixMapperAction.TSV_CONTENT_TYPE.equals(requestContentType)
? PosixMapperAction.TSV_CONTENT_TYPE : "text/plain";
this.syncOutput.addHeader("content-type", writeContentType);

return writeContentType;
}

/**
* Never used.
* @return null
Expand Down
7 changes: 5 additions & 2 deletions posix-mapper/src/main/webapp/WEB-INF/web.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"http://java.sun.com/j2ee/dtds/web-app_2_3.dtd">

<web-app>

<display-name>posix-mapper</display-name>

<servlet>
Expand Down Expand Up @@ -33,6 +32,10 @@
<load-on-startup>1</load-on-startup>
</servlet>

<!--
Note that this servlet uses a separate init action as it will perform one-time application initialization
of the delegating IdentityManager.
-->
<servlet>
<servlet-name>PosixUserManagementServlet</servlet-name>
<servlet-class>ca.nrc.cadc.rest.RestServlet</servlet-class>
Expand All @@ -42,7 +45,7 @@
</init-param>
<init-param>
<param-name>init</param-name>
<param-value>org.opencadc.posix.mapper.web.PosixInitAction</param-value>
<param-value>org.opencadc.posix.mapper.web.DelegatingPosixInitAction</param-value>
</init-param>
<init-param>
<param-name>get</param-name>
Expand Down
Loading