-
Notifications
You must be signed in to change notification settings - Fork 314
Implement OpaPolarisAuthorizer #2680
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
base: main
Are you sure you want to change the base?
Conversation
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.
Thanks @sungwy for this draft PR, I couldn't resist and took a look 😄
This is really interesting imho and a very nice addition to Polaris. We need to start thinking about ways to make Polaris RBAC fully pluggable.
...sions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java
Show resolved
Hide resolved
...sions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/config/AuthorizationConfiguration.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java
Outdated
Show resolved
Hide resolved
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.
Very interesting idea! Just some preliminary comments below :)
Thanks for working on it @sungwy ! Looking forward to the RFC/design doc! |
Will post my complete review soon. Adding one thing first, currently all grant and revoke privilege operation (e.g. |
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.
Hi @sungwy, thanks a lot for working on it. I believe this is the right direction. It opens a door for a lot of possibilities. Left some comments. Feel free to reach out for questions.
polaris-core/src/main/java/org/apache/polaris/core/auth/TokenProvider.java
Outdated
Show resolved
Hide resolved
polaris-core/src/main/java/org/apache/polaris/core/auth/JwtDecoder.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/auth/OpaPolarisAuthorizerFactory.java
Outdated
Show resolved
Hide resolved
...e/service/src/main/java/org/apache/polaris/service/auth/DefaultPolarisAuthorizerFactory.java
Outdated
Show resolved
Hide resolved
...sions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java
Show resolved
Hide resolved
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java
Outdated
Show resolved
Hide resolved
polaris-core/src/main/java/org/apache/polaris/core/auth/OpaPolarisAuthorizer.java
Outdated
Show resolved
Hide resolved
polaris-core/src/main/java/org/apache/polaris/core/auth/FileTokenProvider.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/auth/OpaPolarisAuthorizerFactory.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/config/AuthorizationConfiguration.java
Outdated
Show resolved
Hide resolved
Thank you all for the reviews. I think this PR is ready for another round of feedback. I've made note of the next TODO items we could start working on soon after this initial PR is merged, and listed them in the PR description |
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.
@sungwy , thanks for the quick response. The changes look great overall. I think we are pretty close. Could you also share some thoughts on this comment, #2680 (comment)?
String trustStorePassword, | ||
Object client) { | ||
|
||
if (Strings.isNullOrEmpty(opaServerUrl)) { |
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: -> Preconditions.checkArgument(...)
input.set("actor", buildActorNode(principal)); | ||
input.put("action", op.name()); | ||
input.set("resource", buildResourceNode(targets, secondaries)); | ||
input.set("context", buildContextNode()); | ||
ObjectNode root = objectMapper.createObjectNode(); | ||
root.set("input", input); |
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.
A followup PR sounds good to me.
runtime/service/src/main/java/org/apache/polaris/service/auth/OpaPolarisAuthorizerFactory.java
Outdated
Show resolved
Hide resolved
} else { | ||
if (sslSocketFactory != null) { | ||
httpClient = | ||
HttpClients.custom() | ||
.setDefaultRequestConfig(requestConfig) | ||
.setConnectionManager( | ||
PoolingHttpClientConnectionManagerBuilder.create() | ||
.setSSLSocketFactory(sslSocketFactory) | ||
.build()) | ||
.build(); | ||
} else { | ||
httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build(); | ||
} |
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 the http client is passed from the parameter, do we still need line 129 to line 145? Should createSslSocketFactory
also be part of ServiceProducer::opaHttpClient()
? Otherwise, the ssl config may not take any effect.
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.
Hi @sungwy , sorry for challenging the whole source layout, but it's the first time I looked at your PR for real 😅
I believe it would be valuable to isolate OPA from core and from runtime/service
and only include it into runtime/server
. So, I stopped commenting when I realized I was getting into the dependencies too much.
Here's a sketch of a possible source layout:
extensions/opa
authorizer
-OpaPolarisAuthorizer
and related stuffruntime
- CDI stuff (if necessary... it may be ok to keep producers inside theauthorizer
module)test
-OpaTestResource
and stuff - only for tests that require a full server
test
should be able to include :polaris-runtime-service
as a test dep. to get a running Quarkus server
WDYT?
polaris-core/build.gradle.kts
Outdated
|
||
dependencies { | ||
implementation(project(":polaris-api-management-model")) | ||
implementation(libs.apache.httpclient5) |
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 might be preferable to move OPA stuff out of "core". I propose extensions/opa
(include into runtime/server
).
Rationale:
- the new authorizer is pluggable, but not all downstream projects may want to have it by default.
- reducing core dependencies (IIRC, Quarkus runtime already has
httpclient5
, but core does not have to depend on 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 that makes a lot of sense as well. I was curious to hear opinions on the new dependencies and OPA plugin being introduced as a part of polaris-core
.
Let me take a stab at separating out these changes as a separate build in extensions/auth/opa
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 believe it would be valuable to isolate OPA from core and from runtime/service and only include it into runtime/server.
the new authorizer is pluggable, but not all downstream projects may want to have it by default.
I'm OK to move OPA related classes from core to runtime/service
, but I don't think we should put it in runtime/server
. Here are the reason:
- I think it's perfect fine to have another implementation of authorizer, we did that in multiple places.
- The native RBAC is still the default.
- There is no single extra lib dependencies introduced when it landed in the
service
module.http
andjwt
libs are there already.
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.
Do we still need this dep. in core?
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.
@flyrain : if OPA is not added to runtime/server
users of the OSS distributions will not be able to use it. I personally think it's a valuable feature for OSS users... Let's sync up on this aspect on the dev
ML thread for visibility.
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.
Let me review this dependency again.
I've moved most of the OPA factory and configuration classes into extensions/auth/opa
as suggested, but now I'm seeing a test failure at PolarisEventListenerTest
that's related to the Jandex index. I'll look into this a bit more and get this resolved.
PolarisEventListenerTest > testAllEventTypesHandled() FAILED
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.
Do we still need this dep. in core?
No. I think it’s perfectly fine to keep the OPA authorizer within the polaris-runtime-service
module rather than moving it to a separate extension module.
The rationale for separating components like hive
or hadoop
federation is that they introduce heavy and intrusive Hadoop
dependencies that we don’t want to pull into the service module.
The OPA authorizer, on the other hand, adds no additional dependencies, it integrates cleanly within the service module, so there’s no downside to keeping it there.
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.
Moreover, OpaFileTokenIntegrationTest
and OpaIntegrationTest
are still be part of the polaris-runtime-service
module. What's the point of having the tested classes(like OpaPolarisAuthorizer
) in an extension module?
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.
The pluggability aspect is not about "heavy" or "light" depenpencies per se, but about giving downstream projects control over what to enable/bundle or not. As this appears to be a point where a bit of misunderstanding developed, let's move the discussion about this particular aspect to the dev
ML for visibility and reaching consensus within the broader community (not everyone watches PRs).
* @return SSLConnectionSocketFactory for HTTPS connections, or null for HTTP | ||
* @throws Exception if SSL configuration fails | ||
*/ | ||
private static SSLConnectionSocketFactory createSslSocketFactory( |
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.
SSLConnectionSocketFactory
appears to be deprecated 🤔 Should we use DefaultClientTlsStrategy
as suggested by its javadoc?
|
||
// Configure SSL for HTTPS connections | ||
SSLConnectionSocketFactory sslSocketFactory = | ||
createSslSocketFactory(opaServerUrl, verifySsl, trustStorePath, trustStorePassword); |
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 will be called for every request (REST API). I'm not sure how heavy is the socket factory, but would it make sense to keep it in OpaPolarisAuthorizerFactory
(to be shared across OpaPolarisAuthorizer
instances)?
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.
Yes, I'm already refactoring this as per @flyrain 's comment. It looks like I'll need to introduce an OpaHttpClientFactory
anyways within this PR to clean this up.
@Produces | ||
@Singleton | ||
@Identifier("opa-bearer-token-provider") | ||
public BearerTokenProvider opaBearerTokenProvider( |
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 it this producer could be moved into OpaPolarisAuthorizerFactory
. It should only be required when that factory is active.
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.
Side note: if we take OPA out or core, it would be nice to avoid direct references to it here as well and rely only on CDI to find the right classes via annotations.
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 took a stab at this. Let me know if this aligns with what you were suggesting
@Produces | ||
@Singleton | ||
@Identifier("opa-http-client") | ||
public CloseableHttpClient opaHttpClient() { |
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.
@Singleton
might be an overkill. It is only needed when OPA is active. It should be a dependent bean, I think.
Also, OpaPolarisAuthorizerFactory
might be able to produce / dispose of this bean. Beans can produce other beans, AFAIK.
If dependent beans prove to be too hard to manage, perhaps we can keep the client as a field in the factory and dispose of it together with the factory.
java.time.Duration refreshInterval = | ||
java.time.Duration.ofSeconds(bearerToken.refreshInterval()); | ||
boolean jwtExpirationRefresh = bearerToken.jwtExpirationRefresh(); | ||
java.time.Duration jwtExpirationBuffer = |
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: import?
try { | ||
tokenFile = Files.createTempFile("opa-test-token", ".txt"); | ||
Files.writeString(tokenFile, "test-opa-bearer-token-from-file-67890"); | ||
tokenFile.toFile().deleteOnExit(); |
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: this is not guaranteed to delete the file, AFAIK. Plus the "JVM" may be long running, e.g. a Gradle process.
It might be best to manage this as a QuarkusTestResourceLifecycleManager
, which can produce extra server config entries.
@WithDefault("default") | ||
String type(); | ||
|
||
OpaConfig opa(); |
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.
If we move OPA out of core, let's create a separate config interface for it (with prefix polaris.authorization.opa
). This class will only declare type()
(which is common for all authorizers).
Hi @flyrain - I agree! In our planned architecture, we are thinking of returning |
polaris-misc-types=tools/misc-types | ||
polaris-extensions-federation-hadoop=extensions/federation/hadoop | ||
polaris-extensions-federation-hive=extensions/federation/hive | ||
polaris-extensions-auth-opa=extensions/auth/opa |
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: I'd personally be fine with polaris-auth-opa
(for brevity). The module name does not have to repeat all sub-dir names of its location. I think the polaris
prefix helps in case modules are imported into another project, but otherwise shorter name are easier to use, IMHO.
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 do like the consistency across the extension modules. We already have polaris-extensions-federation-hadoop
and polaris-extensions-federation-hive
above
polaris-core/build.gradle.kts
Outdated
|
||
dependencies { | ||
implementation(project(":polaris-api-management-model")) | ||
implementation(libs.apache.httpclient5) |
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.
Do we still need this dep. in core?
extensions/auth/opa/build.gradle.kts
Outdated
|
||
dependencies { | ||
implementation(project(":polaris-core")) | ||
implementation(project(":polaris-runtime-service")) |
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 does OPA need polaris-runtime-service
?
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.
Updated the dependencies 👍
|
||
@QuarkusTest | ||
@TestProfile(OpaFileTokenIntegrationTest.FileTokenOpaProfile.class) | ||
public class OpaFileTokenIntegrationTest { |
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.
Would it be possible to keep this test under extensions/auth/opa/...
somewhere? In light of making OPA a proper plugin, it would be nice to avoid hard dependencies from the main service module.
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 did try this out earlier and I found it quite challenging.
These are @QuarkusTest integration tests that need to start the full Polaris runtime service application and configure the complete CDI container with all service dependencies to run an end to end test of the authorization workflow. The OPA extension module is a pure library without the Quarkus application context needed for @QuarkusTest, and I think there's value in leaving it at just that.
IMHO I don't see too much of a problem in adding the extensions into runtime/service
as a test dependency.
RealmConfig realmConfig, | ||
@Any Instance<PolarisAuthorizerFactory> authorizerFactories) { | ||
PolarisAuthorizerFactory factory = | ||
authorizerFactories.select(Identifier.Literal.of(authorizationConfig.type())).get(); |
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 you move the factory into a separate @ApplicationScoped
producer to avoid .select()
calls on every request?
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.
Good point @dimas-b - I took care of this, and also made some adjustments into OpaPolarisAuthorizorFactory to validate the configurations and initialize the HttpClient at the application scope.
...sions/auth/opa/src/main/java/org/apache/polaris/extension/auth/opa/OpaHttpClientFactory.java
Show resolved
Hide resolved
input.set("actor", buildActorNode(principal)); | ||
input.put("action", op.name()); | ||
input.set("resource", buildResourceNode(targets, secondaries)); | ||
input.set("context", buildContextNode()); | ||
ObjectNode root = objectMapper.createObjectNode(); | ||
root.set("input", input); |
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.
Yes, I agree that it would be "simpler/cleaner" to create beans (eventually usign Jackson annotations) to generate the JSON input, generating the associate schema and validating it. It's pretty straight forward as we already "bundle" Jackson.
Resolves #138
RFC: https://docs.google.com/document/d/1HadMFygjbuZathZZPanO6cFVorx0Ju0FopkICxX1tCE/edit?tab=t.0
Mailing list discussion: https://lists.apache.org/thread/54qdbsxs3j7wwhv3tsccqj6qng5lqgmz
Some followup items highlighted as a part of this PR review:
OpaHttpClientFactory
that utilizesPoolingHttpClientConnectionManager
to get opa-http-client to be more production ready