Skip to content

Commit

Permalink
Add initial entitlement policy parsing (#114448)
Browse files Browse the repository at this point in the history
This change adds entitlement policy parsing with the following design:
* YAML file for readability and re-use of our x-content parsers
* hierarchical structure to group entitlements under a single scope
* no general entitlements without a scope or for the entire project
  • Loading branch information
jdconrad authored Oct 18, 2024
1 parent cc0da6d commit 68f0f00
Show file tree
Hide file tree
Showing 12 changed files with 602 additions and 5 deletions.
6 changes: 1 addition & 5 deletions distribution/tools/entitlement-runtime/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,12 @@ apply plugin: 'elasticsearch.publish'

dependencies {
compileOnly project(':libs:elasticsearch-core') // For @SuppressForbidden
compileOnly project(":libs:elasticsearch-x-content") // for parsing policy files
compileOnly project(':server') // To access the main server module for special permission checks
compileOnly project(':distribution:tools:entitlement-bridge')

testImplementation project(":test:framework")
}

tasks.named('forbiddenApisMain').configure {
replaceSignatureFiles 'jdk-signatures'
}

tasks.named('forbiddenApisMain').configure {
replaceSignatureFiles 'jdk-signatures'
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

module org.elasticsearch.entitlement.runtime {
requires org.elasticsearch.entitlement.bridge;
requires org.elasticsearch.xcontent;
requires org.elasticsearch.server;

exports org.elasticsearch.entitlement.runtime.api;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.entitlement.runtime.policy;

/**
* Marker interface to ensure that only {@link Entitlement} are
* part of a {@link Policy}. All entitlement classes should implement
* this.
*/
public interface Entitlement {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.entitlement.runtime.policy;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* This annotation indicates an {@link Entitlement} is available
* to "external" classes such as those used in plugins. Any {@link Entitlement}
* using this annotation is considered parseable as part of a policy file
* for entitlements.
*/
@Target(ElementType.CONSTRUCTOR)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExternalEntitlement {

/**
* This is the list of parameter names that are
* parseable in {@link PolicyParser#parseEntitlement(String, String)}.
* The number and order of parameter names much match the number and order
* of constructor parameters as this is how the parser will pass in the
* parsed values from a policy file. However, the names themselves do NOT
* have to match the parameter names of the constructor.
*/
String[] parameterNames() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.entitlement.runtime.policy;

import java.util.List;
import java.util.Objects;

/**
* Describes a file entitlement with a path and actions.
*/
public class FileEntitlement implements Entitlement {

public static final int READ_ACTION = 0x1;
public static final int WRITE_ACTION = 0x2;

private final String path;
private final int actions;

@ExternalEntitlement(parameterNames = { "path", "actions" })
public FileEntitlement(String path, List<String> actionsList) {
this.path = path;
int actionsInt = 0;

for (String actionString : actionsList) {
if ("read".equals(actionString)) {
if ((actionsInt & READ_ACTION) == READ_ACTION) {
throw new IllegalArgumentException("file action [read] specified multiple times");
}
actionsInt |= READ_ACTION;
} else if ("write".equals(actionString)) {
if ((actionsInt & WRITE_ACTION) == WRITE_ACTION) {
throw new IllegalArgumentException("file action [write] specified multiple times");
}
actionsInt |= WRITE_ACTION;
} else {
throw new IllegalArgumentException("unknown file action [" + actionString + "]");
}
}

this.actions = actionsInt;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FileEntitlement that = (FileEntitlement) o;
return actions == that.actions && Objects.equals(path, that.path);
}

@Override
public int hashCode() {
return Objects.hash(path, actions);
}

@Override
public String toString() {
return "FileEntitlement{" + "path='" + path + '\'' + ", actions=" + actions + '}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.entitlement.runtime.policy;

import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
* A holder for scoped entitlements.
*/
public class Policy {

public final String name;
public final List<Scope> scopes;

public Policy(String name, List<Scope> scopes) {
this.name = Objects.requireNonNull(name);
this.scopes = Collections.unmodifiableList(Objects.requireNonNull(scopes));
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Policy policy = (Policy) o;
return Objects.equals(name, policy.name) && Objects.equals(scopes, policy.scopes);
}

@Override
public int hashCode() {
return Objects.hash(name, scopes);
}

@Override
public String toString() {
return "Policy{" + "name='" + name + '\'' + ", scopes=" + scopes + '}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.entitlement.runtime.policy;

import org.elasticsearch.xcontent.ParseField;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xcontent.XContentParserConfiguration;
import org.elasticsearch.xcontent.yaml.YamlXContent;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static org.elasticsearch.entitlement.runtime.policy.PolicyParserException.newPolicyParserException;

/**
* A parser to parse policy files for entitlements.
*/
public class PolicyParser {

protected static final ParseField ENTITLEMENTS_PARSEFIELD = new ParseField("entitlements");

protected static final String entitlementPackageName = Entitlement.class.getPackage().getName();

protected final XContentParser policyParser;
protected final String policyName;

public PolicyParser(InputStream inputStream, String policyName) throws IOException {
this.policyParser = YamlXContent.yamlXContent.createParser(XContentParserConfiguration.EMPTY, Objects.requireNonNull(inputStream));
this.policyName = policyName;
}

public Policy parsePolicy() {
try {
if (policyParser.nextToken() != XContentParser.Token.START_OBJECT) {
throw newPolicyParserException("expected object <scope name>");
}
List<Scope> scopes = new ArrayList<>();
while (policyParser.nextToken() != XContentParser.Token.END_OBJECT) {
if (policyParser.currentToken() != XContentParser.Token.FIELD_NAME) {
throw newPolicyParserException("expected object <scope name>");
}
String scopeName = policyParser.currentName();
Scope scope = parseScope(scopeName);
scopes.add(scope);
}
return new Policy(policyName, scopes);
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
}

protected Scope parseScope(String scopeName) throws IOException {
try {
if (policyParser.nextToken() != XContentParser.Token.START_OBJECT) {
throw newPolicyParserException(scopeName, "expected object [" + ENTITLEMENTS_PARSEFIELD.getPreferredName() + "]");
}
if (policyParser.nextToken() != XContentParser.Token.FIELD_NAME
|| policyParser.currentName().equals(ENTITLEMENTS_PARSEFIELD.getPreferredName()) == false) {
throw newPolicyParserException(scopeName, "expected object [" + ENTITLEMENTS_PARSEFIELD.getPreferredName() + "]");
}
if (policyParser.nextToken() != XContentParser.Token.START_ARRAY) {
throw newPolicyParserException(scopeName, "expected array of <entitlement type>");
}
List<Entitlement> entitlements = new ArrayList<>();
while (policyParser.nextToken() != XContentParser.Token.END_ARRAY) {
if (policyParser.currentToken() != XContentParser.Token.START_OBJECT) {
throw newPolicyParserException(scopeName, "expected object <entitlement type>");
}
if (policyParser.nextToken() != XContentParser.Token.FIELD_NAME) {
throw newPolicyParserException(scopeName, "expected object <entitlement type>");
}
String entitlementType = policyParser.currentName();
Entitlement entitlement = parseEntitlement(scopeName, entitlementType);
entitlements.add(entitlement);
if (policyParser.nextToken() != XContentParser.Token.END_OBJECT) {
throw newPolicyParserException(scopeName, "expected closing object");
}
}
if (policyParser.nextToken() != XContentParser.Token.END_OBJECT) {
throw newPolicyParserException(scopeName, "expected closing object");
}
return new Scope(scopeName, entitlements);
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
}

protected Entitlement parseEntitlement(String scopeName, String entitlementType) throws IOException {
Class<?> entitlementClass;
try {
entitlementClass = Class.forName(
entitlementPackageName
+ "."
+ Character.toUpperCase(entitlementType.charAt(0))
+ entitlementType.substring(1)
+ "Entitlement"
);
} catch (ClassNotFoundException cnfe) {
throw newPolicyParserException(scopeName, "unknown entitlement type [" + entitlementType + "]");
}
if (Entitlement.class.isAssignableFrom(entitlementClass) == false) {
throw newPolicyParserException(scopeName, "unknown entitlement type [" + entitlementType + "]");
}
Constructor<?> entitlementConstructor = entitlementClass.getConstructors()[0];
ExternalEntitlement entitlementMetadata = entitlementConstructor.getAnnotation(ExternalEntitlement.class);
if (entitlementMetadata == null) {
throw newPolicyParserException(scopeName, "unknown entitlement type [" + entitlementType + "]");
}

if (policyParser.nextToken() != XContentParser.Token.START_OBJECT) {
throw newPolicyParserException(scopeName, entitlementType, "expected entitlement parameters");
}
Map<String, Object> parsedValues = policyParser.map();

Class<?>[] parameterTypes = entitlementConstructor.getParameterTypes();
String[] parametersNames = entitlementMetadata.parameterNames();
Object[] parameterValues = new Object[parameterTypes.length];
for (int parameterIndex = 0; parameterIndex < parameterTypes.length; ++parameterIndex) {
String parameterName = parametersNames[parameterIndex];
Object parameterValue = parsedValues.remove(parameterName);
if (parameterValue == null) {
throw newPolicyParserException(scopeName, entitlementType, "missing entitlement parameter [" + parameterName + "]");
}
Class<?> parameterType = parameterTypes[parameterIndex];
if (parameterType.isAssignableFrom(parameterValue.getClass()) == false) {
throw newPolicyParserException(
scopeName,
entitlementType,
"unexpected parameter type [" + parameterType.getSimpleName() + "] for entitlement parameter [" + parameterName + "]"
);
}
parameterValues[parameterIndex] = parameterValue;
}
if (parsedValues.isEmpty() == false) {
throw newPolicyParserException(scopeName, entitlementType, "extraneous entitlement parameter(s) " + parsedValues);
}

try {
return (Entitlement) entitlementConstructor.newInstance(parameterValues);
} catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new IllegalStateException("internal error");
}
}

protected PolicyParserException newPolicyParserException(String message) {
return PolicyParserException.newPolicyParserException(policyParser.getTokenLocation(), policyName, message);
}

protected PolicyParserException newPolicyParserException(String scopeName, String message) {
return PolicyParserException.newPolicyParserException(policyParser.getTokenLocation(), policyName, scopeName, message);
}

protected PolicyParserException newPolicyParserException(String scopeName, String entitlementType, String message) {
return PolicyParserException.newPolicyParserException(
policyParser.getTokenLocation(),
policyName,
scopeName,
entitlementType,
message
);
}
}
Loading

0 comments on commit 68f0f00

Please sign in to comment.