Skip to content

Commit

Permalink
Crude index permissions authz
Browse files Browse the repository at this point in the history
Signed-off-by: Derek Ho <dxho@amazon.com>
  • Loading branch information
derek-ho committed Dec 24, 2024
1 parent 9f695fa commit d3fcc4a
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 30 deletions.
15 changes: 10 additions & 5 deletions src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.List;
Expand All @@ -30,6 +29,7 @@
import org.opensearch.common.settings.Settings;
import org.opensearch.common.xcontent.XContentFactory;
import org.opensearch.core.xcontent.ToXContent;
import org.opensearch.core.xcontent.XContentBuilder;
import org.opensearch.security.action.apitokens.ApiToken;

import com.nimbusds.jose.JOSEException;
Expand Down Expand Up @@ -184,12 +184,17 @@ public ExpiringBearerAuthToken createJwt(
}

if (indexPermissions != null) {
List<String> permissionStrings = new ArrayList<>();
XContentBuilder builder = XContentFactory.jsonBuilder();
builder.startArray();
for (ApiToken.IndexPermission permission : indexPermissions) {
permissionStrings.add(permission.toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS).toString());
// Add each permission to the array
permission.toXContent(builder, ToXContent.EMPTY_PARAMS);
}
final String listOfIndexPermissions = String.join(",", permissionStrings);
claimsBuilder.claim("ip", encryptString(listOfIndexPermissions));
builder.endArray();

// Encrypt the entire JSON array
String jsonArray = builder.toString();
claimsBuilder.claim("ip", encryptString(jsonArray));
}

final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(signingKey.getAlgorithm().getName())).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ public class SecurityRestFilter {

protected final Logger log = LogManager.getLogger(this.getClass());
public static final String API_TOKEN_CLUSTERPERM_KEY = "security.api_token.clusterperm";
public static final String API_TOKEN_INDEXPERM_KEY = "security.api_token.indexperm";
public static final String API_TOKEN_INDEXACTIONS_KEY = "security.api_token.indexactions";
public static final String API_TOKEN_INDICES_KEY = "security.api_token.indices";
private final BackendRegistry registry;
private final RestLayerPrivilegesEvaluator evaluator;
private final AuditLog auditLog;
Expand Down Expand Up @@ -235,12 +236,6 @@ void authorizeRequest(RestHandler original, SecurityRequestChannel request, User
.add(route.name())
.build();

log.info("API token context value: " + threadContext.getTransient(API_TOKEN_CLUSTERPERM_KEY).toString());

if (threadContext.getTransient(API_TOKEN_CLUSTERPERM_KEY) != null) {
return;
}

pres = evaluator.evaluate(user, route.name(), actionNames);

if (log.isDebugEnabled()) {
Expand Down
107 changes: 89 additions & 18 deletions src/main/java/org/opensearch/security/http/ApiTokenAuthenticator.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@

package org.opensearch.security.http;

import java.io.IOException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;
Expand All @@ -31,7 +32,12 @@
import org.opensearch.SpecialPermission;
import org.opensearch.common.settings.Settings;
import org.opensearch.common.util.concurrent.ThreadContext;
import org.opensearch.common.xcontent.XContentType;
import org.opensearch.core.xcontent.DeprecationHandler;
import org.opensearch.core.xcontent.NamedXContentRegistry;
import org.opensearch.core.xcontent.XContentParser;
import org.opensearch.security.DefaultObjectMapper;
import org.opensearch.security.action.apitokens.ApiToken;
import org.opensearch.security.auth.HTTPAuthenticator;
import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil;
import org.opensearch.security.filter.SecurityRequest;
Expand All @@ -48,6 +54,8 @@
import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX;
import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX;
import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_CLUSTERPERM_KEY;
import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_INDEXACTIONS_KEY;
import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_INDICES_KEY;
import static org.opensearch.security.util.AuthTokenUtils.isAccessToRestrictedEndpoints;

public class ApiTokenAuthenticator implements HTTPAuthenticator {
Expand Down Expand Up @@ -109,32 +117,88 @@ private JwtParserBuilder initParserBuilder(final String signingKey) {
return jwtParserBuilder;
}

private String extractSecurityRolesFromClaims(Claims claims) {
private String extractClusterPermissionsFromClaims(Claims claims) {
Object cp = claims.get("cp");
Object ip = claims.get("ip");
String rolesClaim = "";
String clusterPermissions = "";

if (cp != null) {
rolesClaim = encryptionUtil.decrypt(cp.toString());
clusterPermissions = encryptionUtil.decrypt(cp.toString());
} else {
log.warn("This is a malformed Api Token");
}

return rolesClaim;
return clusterPermissions;
}

private String[] extractBackendRolesFromClaims(Claims claims) {
Object backendRolesObject = claims.get("br");
String[] backendRoles;
private String extractAllowedActionsFromClaims(Claims claims) throws IOException {
Object ip = claims.get("ip");

if (ip != null) {
String decryptedPermissions = encryptionUtil.decrypt(ip.toString());

try (
XContentParser parser = XContentType.JSON.xContent()
.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, decryptedPermissions)
) {

// Use built-in array parsing
List<ApiToken.IndexPermission> permissions = new ArrayList<>();

// Move to start of array
parser.nextToken(); // START_ARRAY
while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
permissions.add(ApiToken.IndexPermission.fromXContent(parser));
}
// Get first permission's actions
if (!permissions.isEmpty() && !permissions.get(0).getAllowedActions().isEmpty()) {
return permissions.get(0).getAllowedActions().get(0);
}

return "";
} catch (Exception e) {
log.error("Error extracting allowed actions", e);
return "";
}

}

return "";
}

private String extractIndicesFromClaims(Claims claims) throws IOException {
Object ip = claims.get("ip");

if (ip != null) {
String decryptedPermissions = encryptionUtil.decrypt(ip.toString());

try (
XContentParser parser = XContentType.JSON.xContent()
.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, decryptedPermissions)
) {

// Use built-in array parsing
List<ApiToken.IndexPermission> permissions = new ArrayList<>();

// Move to start of array
parser.nextToken(); // START_ARRAY
while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
permissions.add(ApiToken.IndexPermission.fromXContent(parser));
}

// Get first permission's actions
if (!permissions.isEmpty() && !permissions.get(0).getIndexPatterns().isEmpty()) {
return permissions.get(0).getIndexPatterns().get(0);
}

return "";
} catch (Exception e) {
log.error("Error extracting indices", e);
return "";
}

if (backendRolesObject == null) {
backendRoles = new String[0];
} else {
// Extracting roles based on the compatibility mode
backendRoles = Arrays.stream(backendRolesObject.toString().split(",")).map(String::trim).toArray(String[]::new);
}

return backendRoles;
return "";
}

@Override
Expand Down Expand Up @@ -193,10 +257,15 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final
return null;
}

String clusterPermissions = extractSecurityRolesFromClaims(claims);
String[] backendRoles = extractBackendRolesFromClaims(claims);
log.info("before extraction");

String clusterPermissions = extractClusterPermissionsFromClaims(claims);
String allowedActions = extractAllowedActionsFromClaims(claims);
String indices = extractIndicesFromClaims(claims);

log.info(clusterPermissions + allowedActions + indices);

final AuthCredentials ac = new AuthCredentials(subject, List.of(), backendRoles).markComplete();
final AuthCredentials ac = new AuthCredentials(subject, List.of(), new String[0]).markComplete();

for (Entry<String, Object> claim : claims.entrySet()) {
String key = "attr.jwt." + claim.getKey();
Expand All @@ -218,6 +287,8 @@ private AuthCredentials extractCredentials0(final SecurityRequest request, final
}

context.putTransient(API_TOKEN_CLUSTERPERM_KEY, clusterPermissions);
context.putTransient(API_TOKEN_INDEXACTIONS_KEY, allowedActions);
context.putTransient(API_TOKEN_INDICES_KEY, indices);

return ac;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -161,6 +162,14 @@ public PrivilegesEvaluatorResponse hasIndexPrivilege(
return response;
}

// API Token Authz
// TODO: this is very naive implementation
if (context.getIndices() != null && new HashSet<>(context.getIndices()).containsAll(resolvedIndices.getAllIndices())) {
if (new HashSet<>(context.getAllowedActions()).containsAll(actions)) {
return PrivilegesEvaluatorResponse.ok();
}
}

if (!resolvedIndices.isLocalAll() && resolvedIndices.getAllIndices().isEmpty()) {
// This is necessary for requests which operate on remote indices.
// Access control for the remote indices will be performed on the remote cluster.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public class PrivilegesEvaluationContext {
private final IndexNameExpressionResolver indexNameExpressionResolver;
private final Supplier<ClusterState> clusterStateSupplier;
private List<String> clusterPermissions;
private List<String> allowedActions;
private List<String> indices;

/**
* This caches the ready to use WildcardMatcher instances for the current request. Many index patterns have
Expand Down Expand Up @@ -182,4 +184,20 @@ public void setClusterPermissions(List<String> clusterPermissions) {
public List<String> getClusterPermissions() {
return clusterPermissions;
}

public List<String> getAllowedActions() {
return allowedActions;
}

public void setAllowedActions(List<String> allowedActions) {
this.allowedActions = allowedActions;
}

public List<String> getIndices() {
return indices;
}

public void setIndices(List<String> indices) {
this.indices = indices;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@

import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction;
import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_CLUSTERPERM_KEY;
import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_INDEXACTIONS_KEY;
import static org.opensearch.security.filter.SecurityRestFilter.API_TOKEN_INDICES_KEY;
import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT;

public class PrivilegesEvaluator {
Expand Down Expand Up @@ -352,6 +354,21 @@ public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context)
context.setClusterPermissions(clusterPermissions);
}

final String apiTokenIndexAllowedActions = threadContext.getTransient(API_TOKEN_INDEXACTIONS_KEY);
if (apiTokenIndexAllowedActions != null) {
List<String> allowedactions = Arrays.asList(apiTokenIndexAllowedActions.split(","));
context.setAllowedActions(allowedactions);
}

final String apiTokenIndices = threadContext.getTransient(API_TOKEN_INDICES_KEY);
if (apiTokenIndices != null) {
List<String> indices = Arrays.asList(apiTokenIndices.split(","));
context.setIndices(indices);
}

log.info("API Tokens actions" + apiTokenIndexAllowedActions);
log.info("API Tokens indices" + apiTokenIndices);

// Add the security roles for this user so that they can be used for DLS parameter substitution.
user.addSecurityRoles(mappedRoles);
setUserInfoInThreadContext(user);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ private void buildAAA() {
*/
Settings apiTokenSettings = getDynamicApiTokenSettings();
if (!isKeyNull(apiTokenSettings, "signing_key") && !isKeyNull(apiTokenSettings, "encryption_key")) {
log.info("we initialized the api tokenauthenticator");
final AuthDomain _ad = new AuthDomain(
new NoOpAuthenticationBackend(Settings.EMPTY, null),
new ApiTokenAuthenticator(getDynamicApiTokenSettings(), this.cih.getClusterName()),
Expand Down

0 comments on commit d3fcc4a

Please sign in to comment.