-
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
Conversation
|
Pinging @elastic/es-core-infra (Team:Core/Infra) |
| tasks.named('forbiddenApisMain').configure { | ||
| replaceSignatureFiles 'jdk-signatures' | ||
| testImplementation(project(":test:framework")) { | ||
| exclude group: 'org.elasticsearch', module: 'elasticsearch-entitlement-runtime' |
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.
Why would test-framework bring in the entitlement-runtime project? Does :server depend on this?
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 didn't realize this project had been moved from libs to distribution. I'm working on fixing everything now when merging with main.
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.
Yeah, I think you can just remove this exclude as it's unnecessary.
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 think this is fixed with the main merge conflicts resolved.
| this.policyParser = policyParser; | ||
| } | ||
|
|
||
| protected abstract void parseEntitlement() throws IOException; |
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.
What is the contract of this method? From the implementations, it seems to use the parser to validate the structure, but doesn't actually have any other side effects or return anything?
Is the idea that this will ultimately return whatever data structure we end up with?
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 was thinking there would be two passes. One for parsing and one for building. The parsing one would validate things like is the structure correct and are these values strings, etc. The building would validate that the scopes exist and permissions have valid paths, etc then turn them into immutable structures that will be used for comparison. Open here to a single pass if we think that's better. Either way we will need to do a string lookup at some point to get the module object for comparison.
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 see. I have the same feeling, it looks "strange" to have a method that has only side effects.
But in the context of your explanation it makes sense, if you think there will be a public build method, and this will be only used internally.
Maybe to make this clear these classes could be internal (package private?)
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.
That sounds like an interesting separation, between syntax and semantics. Phase 1: make sure you can tell what they're asking for; phase 2: make sure they're asking for something reasonable.
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 not 100% sure this is the right approach, yet, but have to start somewhere. Either way parsing code will be useful.
| public class PolicyBuilderTests extends ESTestCase { | ||
|
|
||
| public void testPolicyBuilder() { | ||
| new PolicyBuilder("test-policy.yaml", PolicyBuilderTests.class.getResourceAsStream("test-policy.yaml")).buildPolicy(); |
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 needs some sad-path tests too, with various kinds of invalid YAML structures.
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.
Definitely!
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.
In the past I have had some success with a tool called pitest to inspire unit test cases based on the sorts of bugs they would detect. It can be tricky to set up though.
| policy: | ||
| - module: | ||
| name: entitlement-test | ||
| entitlements: |
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.
How do you envision this structure extending to support class-level grants?
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 envisioned having sub objects of module for package and then for class with their own set of entitlements for those specific objects. @rjernst and I had chatted about having all scoping be at the top-level where scope was separate by a colon, but I thought the trade off of having them be sub objects made it easier for users to see what permissions belong where. I'm really open to changing this either way.
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.
Oh I think I get it. This is the hierarchical structure you mentioned. I like it!
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 think it makes sense. We can certainly make changes to simplify or move structure around if we want to.
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.
Looks like a very good start, thanks!
Looking forward to see the final shape, with the Builder -> immutable objects.
I have a slight concern with naming: they are not really what you would expect from a builder, are they? The only one vaguely resembling a "traditional" java builder is ModuleScopeBuilder.
Why don't we call them Parser or Reader?
| } | ||
| } | ||
|
|
||
| public void buildPolicy() { |
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.
IMO this should be named consistently with the others (parsePolicy?) and be package private. So it's still accessible by tests, but it's clear that we will use build() instead (to be added).
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.
Changed.
|
|
||
| dependencies { | ||
| compileOnly project(':libs:elasticsearch-core') // For @SuppressForbidden | ||
| compileOnly project(":libs:elasticsearch-x-content") // for parsing policy files |
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.
| super(policyName, scopeName, policyParser); | ||
| } | ||
|
|
||
| protected void parseEntitlement() throws IOException { |
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.
NGL, I didn't read the implementation here in detail. This seems like a fairly low-stakes situation; I assume you did smart things in here, and if we find out something isn't right, we can just fix it. An extensive test suite is probably better for demonstrating that this sort of method is right, as opposed to careful review.
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 understand :) . Tests should cover this kind of thing typically.
| throw newPolicyParserException(policyParser.getTokenLocation(), policyName, "expected closing object"); | ||
| } | ||
| } catch (IOException ioe) { | ||
| throw new UncheckedIOException(ioe); |
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.
Given that this is using an InputStream, should the method be declared throws IOException?
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.
Changed.
|
|
||
| import org.elasticsearch.xcontent.XContentLocation; | ||
|
|
||
| public class PolicyParserException extends RuntimeException { |
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 not sure what Elastic's philosophy is on runtime exceptions. This strikes me as a situation where this feature (the policy parser) will be used in very few places--probably just one--and in that place, the caller is going to need to catch this exception and do something about it anyway. That means the drawbacks of checked exceptions are insignificant, and making it a checked exception might help remind future devs of the sharp edges here.
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.
Most of our other parse exceptions are also runtime exceptions. I think I'd prefer to go with that precedent for now. If a policy fails, ES will not start anyway, so is should be pretty obvious to anyone adding code around this what happened.
|
|
||
| import java.io.IOException; | ||
|
|
||
| public abstract class EntitlementBuilder { |
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 wonder, given the multi-phase parse-validate-build approach you mentioned in the comments... should this be called EntitlementParser?
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 suggested the same, Parser or Reader)
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 renamed everything to parser. I had considered these builders for the validation/build step, but maybe what makes the most sense is to separate out the builders completely from the parsers. So it would end up being parser -> builder -> immutable policy object(s). If that's the case I'll have the parsers return builder objects. WDYT?
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.
FWIW my hunch is that the extra step will pay off. There's a tendency to make parsers do a lot, but unless they're very simple, parsers are best left just determining what the text says rather than trying to validate that it makes sense, or trying to "execute" it by building some sort of efficient data structures.
It makes sense to parse into some convenient form, validate that, and then once we have all the validated grants, run a build step that generates a highly efficient data structure for entitlement checks.
It's not super clear though, because it's possible our situation will be simple enough that this distinction isn't pulling its weight, and the parser could just generate efficient data structures directly. 🤷
|
@jdconrad I think this is a good start pointing |
|
@ldematte @prdoyle Good call on trying to start creating the immutable objects because after starting to code some of these coming from the parsers, I realized what I was considering the builders are actually the first stage of immutable objects. It's really like there's two stages of immutable where we don't have the classes loaded yet, and have to rely on strings vs where we do have all the classes loaded and we can use the module objects and class objects instead. I think this is worth discussing more. |
|
Oh interesting. My recollection from our last meeting is that we'd need some sort of lazy resolution inside the permission checks themselves, since there's no straightforward way to identify a point in the initialization process where we know we'll have all the necessary modules/classes loaded. The trouble is, making lazy resolution perform well is tricky. This is something that We could consider instrumenting the target methods with invokedynamic calls perhaps. 🤔 |
|
@ldematte @prdoyle I have updated this PR based on some feedback about removing future boilerplate. I have modified the design based on our discussions with the following significant changes:
|
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 have a few more suggestions, but this looks pretty good to me!
| import java.util.List; | ||
| import java.util.Objects; | ||
|
|
||
| public class FileEntitlement implements Entitlement { |
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.
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 comment
The 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.)
|
|
||
| package org.elasticsearch.entitlement.runtime.policy; | ||
|
|
||
| public interface Entitlement { |
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Added.
| @Retention(RetentionPolicy.RUNTIME) | ||
| public @interface ExternalEntitlement { | ||
|
|
||
| String[] parameterNames() default {}; |
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.
Similar here, javadocs to explain why this parameterNames exists at all
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.
Added.
| import java.lang.annotation.RetentionPolicy; | ||
|
|
||
| @Retention(RetentionPolicy.RUNTIME) | ||
| public @interface ExternalEntitlement { |
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.
javadoc?
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.
Also @Target.
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.
Added both documentation and an @target for constructor.
| 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 comment
The 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 comment
The 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.
| ); | ||
| } catch (ClassNotFoundException cnfe) { | ||
| throw newPolicyParserException( | ||
| policyParser.getTokenLocation(), |
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.
nit: The token location and policy name seem to be the same in all calls to these helper exception methods, could those be removed as args?
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 cleaned this up with some additional helper methods.
| import java.util.List; | ||
| import java.util.Objects; | ||
|
|
||
| public class Scope { |
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.
Could this be a record?
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.
It depends on how we check entitlements moving forward. If they all become part of the top-level Policy class then I'm happy to change it then.
|
|
||
| public void testEntitlementExtraneousParameter() throws IOException { | ||
| try (XContentBuilder builder = YamlXContent.contentBuilder()) { | ||
| builder.startObject(); |
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.
It would be much easier to read these tests if they used multi line strings, so the file content is more understandable. I know we have used builders in tests in the past for much of xcontent, indexing, etc, but in those cases I think we care about varying the underlying xcontent type. Here we only care about yaml.
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.
Changed.
| @@ -0,0 +1,7 @@ | |||
| entitlement-module-1: | |||
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.
nit: maybe make this "example-module-name", with the 1 it looks like this is an arbitrary id
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.
Changed.
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.
Looks really good to me!
I agree with Ryan that more javadoc would be very nice (possibly copy/pasting some of the explanations from comments on this PR too).
Other than that, it looks good to me; I agree that more things (adding other entitlements, the "final" immutable classes, child scopes) can be added later, when needed.
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
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
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
* Add initial entitlement policy parsing (#114448) 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 * Avoid double instrumentation via class annotation (#115398) * Move entitlement jars to libs (#115883) The distribution tools are meant to be CLIs. This commit moves the entitlements jar projects to the libs dir, under a single libs/entitlement root directory to keep the related jars together. * Entitlement tools: SecurityManager scanner (#116020) * Dynamic entitlement agent (#116125) * Refactor: treat "maybe" JVM options uniformly * WIP * Get entitlement running with bridge all the way through, with qualified exports * Cosmetic changes to SystemJvmOptions * Disable entitlements by default * Bridge module comments * Fixup forbidden APIs * spotless * Rename EntitlementChecker * Fixup InstrumenterTests * exclude recursive dep * Fix some compliance stuff * Rename asm-provider * Stop using bridge in InstrumenterTests * Generalize readme for asm-provider * InstrumenterTests doesn't need EntitlementCheckerHandle * Better javadoc * Call parseBoolean * Add entitlement to internal module list * Docs as requested by Lorenzo * Changes from Jack * Rename ElasticsearchEntitlementChecker * Remove logging javadoc * exportInitializationToAgent should reference EntitlementInitialization, not EntitlementBootstrap. They're currently in the same module, but if that ever changes, this code would have become wrong. * Some suggestions from Mark --------- Co-authored-by: Ryan Ernst <ryan@iernst.net> * Remove unused EntitlementInternals (#116473) * Revert "Entitlement tools: SecurityManager scanner (#116020)" This reverts commit 023fb66. --------- Co-authored-by: Jack Conradson <osjdconrad@gmail.com> Co-authored-by: Lorenzo Dematté <lorenzo.dematte@elastic.co> Co-authored-by: Ryan Ernst <ryan@iernst.net>
This change adds entitlement policy parsing with the following design:
This is just the parsing logic. I would like early feedback before moving onto the immutable data structures we store the entitlements in for comparison.
Design decisions:
ConstructingObjectParseras this allows for better error messages and is often more intuitive in debugging with breakpoints if necessary.PolicyParserExceptionto give consistent, detailed error messages throughout.