Skip to content

Conversation

sungwy
Copy link

@sungwy sungwy commented Sep 25, 2025

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:

  • introduce OpaHttpClientFactory that utilizes PoolingHttpClientConnectionManager to get opa-http-client to be more production ready
  • publish user-facing docs on OPA authorization
  • publish json schema for docs and opa server checks
  • introduce per-realm configuration support

Copy link
Contributor

@adutra adutra left a 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.

Copy link
Contributor

@dimas-b dimas-b left a 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 :)

@flyrain
Copy link
Contributor

flyrain commented Sep 25, 2025

Thanks for working on it @sungwy ! Looking forward to the RFC/design doc!

@sungwy sungwy marked this pull request as ready for review October 8, 2025 02:05
@sungwy sungwy requested review from adutra and dimas-b October 8, 2025 02:14
@sungwy sungwy requested a review from collado-mike October 8, 2025 12:34
@flyrain
Copy link
Contributor

flyrain commented Oct 8, 2025

Will post my complete review soon. Adding one thing first, currently all grant and revoke privilege operation (e.g. grantPrivilegeOnCatalogToRole) won’t work as expected, since these mapping has to happen in the OPA side. How do we position these APIs? If we still want to support them within Polaris, we need new interfaces between the OPA service and Polaris. I think the least thing we could do now is blocking these operation for OPA authorizer. Given the current PR is large, I’m OK to leave it in another PR. Thanks @singhpk234 for bring it up for discussion.

Copy link
Contributor

@flyrain flyrain left a 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.

@sungwy sungwy requested a review from flyrain October 9, 2025 14:43
@sungwy
Copy link
Author

sungwy commented Oct 9, 2025

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

Copy link
Contributor

@flyrain flyrain left a 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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: -> Preconditions.checkArgument(...)

Comment on lines +269 to +274
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);
Copy link
Contributor

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.

Comment on lines 133 to 145
} else {
if (sslSocketFactory != null) {
httpClient =
HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(
PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(sslSocketFactory)
.build())
.build();
} else {
httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
}
Copy link
Contributor

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.

Copy link
Contributor

@dimas-b dimas-b left a 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 stuff
  • runtime - CDI stuff (if necessary... it may be ok to keep producers inside the authorizer 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?


dependencies {
implementation(project(":polaris-api-management-model"))
implementation(libs.apache.httpclient5)
Copy link
Contributor

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:

  1. the new authorizer is pluggable, but not all downstream projects may want to have it by default.
  2. reducing core dependencies (IIRC, Quarkus runtime already has httpclient5, but core does not have to depend on it).

Copy link
Author

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

Copy link
Contributor

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:

  1. I think it's perfect fine to have another implementation of authorizer, we did that in multiple places.
  2. The native RBAC is still the default.
  3. There is no single extra lib dependencies introduced when it landed in the service module. http and jwt libs are there already.

Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Author

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

Copy link
Contributor

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.

Copy link
Contributor

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?

Copy link
Contributor

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(
Copy link
Contributor

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);
Copy link
Contributor

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)?

Copy link
Author

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(
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Author

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() {
Copy link
Contributor

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 =
Copy link
Contributor

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();
Copy link
Contributor

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();
Copy link
Contributor

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).

@sungwy
Copy link
Author

sungwy commented Oct 10, 2025

Will post my complete review soon. Adding one thing first, currently all grant and revoke privilege operation (e.g. grantPrivilegeOnCatalogToRole) won’t work as expected, since these mapping has to happen in the OPA side. How do we position these APIs? If we still want to support them within Polaris, we need new interfaces between the OPA service and Polaris. I think the least thing we could do now is blocking these operation for OPA authorizer. Given the current PR is large, I’m OK to leave it in another PR. Thanks @singhpk234 for bring it up for discussion.

Hi @flyrain - I agree! In our planned architecture, we are thinking of returning false on the Management APIs through the OPA Server rego. But I agree that we should review whether it would make sense to just systematically fail on these actions within the OpaPolarisAuthorizer itself before sending the request to OPA .

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
Copy link
Contributor

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.

Copy link
Author

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


dependencies {
implementation(project(":polaris-api-management-model"))
implementation(libs.apache.httpclient5)
Copy link
Contributor

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?


dependencies {
implementation(project(":polaris-core"))
implementation(project(":polaris-runtime-service"))
Copy link
Contributor

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?

Copy link
Author

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 {
Copy link
Contributor

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.

Copy link
Author

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();
Copy link
Contributor

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?

Copy link
Author

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.

Comment on lines +269 to +274
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);
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE REQUEST] Support OPA integration

6 participants