-
Notifications
You must be signed in to change notification settings - Fork 25.6k
Add initial entitlement policy parsing #114448
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
Changes from all commits
d7cc7ba
8aa4a5c
8d40a4d
1f510d5
4297326
70a1e47
3ea1020
de4ce60
a522870
821dc3f
d2c3ce0
2c0d2c1
3adb0d3
4ed58a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some javadocs would be nice for other devs to know this is a marker interface There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added. |
||
|
|
||
| } | ||
| 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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. javadoc? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added both documentation and an @target for constructor. |
||
|
|
||
| /** | ||
| * 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 {}; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar here, javadocs to explain why this parameterNames exists at all There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added. |
||
| } | ||
| 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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps this could be a record? That would remove more boilerplate. But that could be in a followup. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will leave this as a follow up because I'm not sure we don't want to add some kind of entitlement comparison in each one where this one would take in a path and an action to see if it's permitted. (Not a new object.) |
||
|
|
||
| public static final int READ_ACTION = 0x1; | ||
| public static final int WRITE_ACTION = 0x2; | ||
|
|
||
| private final String path; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll need to access these, so they should be public or have accessors? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This depends on the future design if we end up doing entitlement checks as part of each entitlement class or not. I would prefer to change this in a follow up to match the design we decide on moving forward. |
||
| 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 | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not an objection; just a thought...
Beyond the initialization phase, we don't need the ability to parse policy files anymore, so for 99% of its lifetime, the runtime library doesn't need this. This suggests that the parsing might be better in another module whose job is to generate (by whatever means) a policy object that is used by the runtime.
This sort of separation could enhance testing too, because the interface between them is an immutable data structure (records?). The parser module tests would feed in text and assert that the right structures are generated; the runtime module tests would feed in data structures and ensure that the right permissions are granted.
I'm not sure exactly what I'm suggesting here though. 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm open to moving it for sure, but for now this still seems like the best starting point to me.