Skip to content

Commit

Permalink
Change authentication workflow to lookup principal roles using securi…
Browse files Browse the repository at this point in the history
…ty context (#623)

* Change authentication workflow to lookup principal roles using security context

* Refactored active role lookup into new interface

* Addressed PR comments

* Removed injected security context

---------

Co-authored-by: Michael Collado <michael.collado@snowflake.com>
  • Loading branch information
collado-mike and sfc-gh-mcollado authored Jan 11, 2025
1 parent 16f0093 commit dcbb005
Show file tree
Hide file tree
Showing 26 changed files with 631 additions and 163 deletions.
1 change: 1 addition & 0 deletions dropwizard/service/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies {
implementation("io.dropwizard:dropwizard-core")
implementation("io.dropwizard:dropwizard-auth")
implementation("io.dropwizard:dropwizard-json-logging")
implementation("org.glassfish.jersey.inject:jersey-hk2:3.0.16")

implementation(platform(libs.iceberg.bom))
implementation("org.apache.iceberg:iceberg-api")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@
import com.fasterxml.jackson.databind.module.SimpleValueInstantiators;
import com.fasterxml.jackson.databind.type.TypeFactory;
import io.dropwizard.auth.AuthDynamicFeature;
import io.dropwizard.auth.AuthFilter;
import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter;
import io.dropwizard.configuration.EnvironmentVariableSubstitutor;
import io.dropwizard.configuration.SubstitutingSourceProvider;
import io.dropwizard.core.Application;
Expand Down Expand Up @@ -83,7 +81,6 @@
import java.util.stream.Stream;
import org.apache.iceberg.rest.RESTSerializers;
import org.apache.polaris.core.PolarisConfigurationStore;
import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
import org.apache.polaris.core.auth.PolarisAuthorizer;
import org.apache.polaris.core.auth.PolarisAuthorizerImpl;
import org.apache.polaris.core.auth.PolarisGrantManager;
Expand All @@ -101,7 +98,8 @@
import org.apache.polaris.service.admin.api.PolarisPrincipalRolesApiService;
import org.apache.polaris.service.admin.api.PolarisPrincipalsApi;
import org.apache.polaris.service.admin.api.PolarisPrincipalsApiService;
import org.apache.polaris.service.auth.Authenticator;
import org.apache.polaris.service.auth.ActiveRolesProvider;
import org.apache.polaris.service.auth.DefaultActiveRolesProvider;
import org.apache.polaris.service.catalog.IcebergCatalogAdapter;
import org.apache.polaris.service.catalog.api.IcebergRestCatalogApi;
import org.apache.polaris.service.catalog.api.IcebergRestCatalogApiService;
Expand All @@ -116,6 +114,8 @@
import org.apache.polaris.service.context.CallContextResolver;
import org.apache.polaris.service.context.PolarisCallContextCatalogFactory;
import org.apache.polaris.service.context.RealmContextResolver;
import org.apache.polaris.service.dropwizard.auth.PolarisPrincipalAuthenticator;
import org.apache.polaris.service.dropwizard.auth.PolarisPrincipalRoleSecurityContextProvider;
import org.apache.polaris.service.dropwizard.config.PolarisApplicationConfig;
import org.apache.polaris.service.dropwizard.context.RealmScopeContext;
import org.apache.polaris.service.dropwizard.exception.JerseyViolationExceptionMapper;
Expand Down Expand Up @@ -297,19 +297,17 @@ protected void configure() {
.in(RealmScoped.class)
.to(PolarisRemoteCache.class);

// factory to use a cache delegating grant cache
// currently depends explicitly on the metaStoreManager as the delegate grant
// manager
bindFactory(PolarisMetaStoreManagerFactory.class)
.in(RealmScoped.class)
.to(PolarisGrantManager.class);
bindFactory(PolarisMetaStoreManagerFactory.class).to(PolarisGrantManager.class);
polarisMetricRegistry.init(
IcebergRestCatalogApi.class,
IcebergRestConfigurationApi.class,
IcebergRestOAuth2Api.class,
PolarisCatalogsApi.class,
PolarisPrincipalsApi.class,
PolarisPrincipalRolesApi.class);
bind(DefaultActiveRolesProvider.class).to(ActiveRolesProvider.class);
bindAsContract(RealmEntityManagerFactory.class).in(Singleton.class);
bind(PolarisCallContextCatalogFactory.class)
.to(CallContextCatalogFactory.class)
Expand Down Expand Up @@ -396,14 +394,8 @@ protected void configure() {
if (configuration.hasRateLimiter()) {
environment.jersey().register(RateLimiterFilter.class);
}
Authenticator<String, AuthenticatedPolarisPrincipal> authenticator =
configuration.findService(new TypeLiteral<>() {});
AuthFilter<String, AuthenticatedPolarisPrincipal> oauthCredentialAuthFilter =
new OAuthCredentialAuthFilter.Builder<AuthenticatedPolarisPrincipal>()
.setAuthenticator(authenticator::authenticate)
.setPrefix("Bearer")
.buildAuthFilter();
environment.jersey().register(new AuthDynamicFeature(oauthCredentialAuthFilter));
environment.jersey().register(new AuthDynamicFeature(PolarisPrincipalAuthenticator.class));
environment.jersey().register(PolarisPrincipalRoleSecurityContextProvider.class);
environment.healthChecks().register("polaris", new PolarisHealthCheck());

environment.jersey().register(IcebergRestOAuth2Api.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.polaris.service.dropwizard.auth;

import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.SecurityContext;
import java.io.IOException;
import java.security.Principal;
import java.util.Optional;
import org.apache.iceberg.exceptions.NotAuthorizedException;
import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
import org.apache.polaris.service.auth.Authenticator;

@jakarta.ws.rs.ext.Provider
@Priority(Priorities.AUTHENTICATION)
public class PolarisPrincipalAuthenticator implements ContainerRequestFilter {
@Inject private Authenticator<String, AuthenticatedPolarisPrincipal> authenticator;

@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String authHeader = requestContext.getHeaderString("Authorization");
if (authHeader == null) {
throw new IOException("Authorization header is missing");
}
int spaceIdx = authHeader.indexOf(' ');
if (spaceIdx <= 0 || !authHeader.substring(0, spaceIdx).equalsIgnoreCase("Bearer")) {
throw new IOException("Authorization header is not a Bearer token");
}
String credential = authHeader.substring(spaceIdx + 1);
Optional<AuthenticatedPolarisPrincipal> principal = authenticator.authenticate(credential);
if (principal.isEmpty()) {
throw new NotAuthorizedException("Unable to authenticate");
}
SecurityContext securityContext = requestContext.getSecurityContext();
requestContext.setSecurityContext(
new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return principal.get();
}

@Override
public boolean isUserInRole(String role) {
return securityContext.isUserInRole(role);
}

@Override
public boolean isSecure() {
return securityContext.isSecure();
}

@Override
public String getAuthenticationScheme() {
return securityContext.getAuthenticationScheme();
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.polaris.service.dropwizard.auth;

import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.SecurityContext;
import java.io.IOException;
import java.security.Principal;
import java.util.Set;
import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
import org.apache.polaris.service.auth.ActiveRolesProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Priority(Priorities.AUTHENTICATION + 1)
public class PolarisPrincipalRoleSecurityContextProvider implements ContainerRequestFilter {
private static final Logger LOGGER =
LoggerFactory.getLogger(PolarisPrincipalRoleSecurityContextProvider.class);
@Inject ActiveRolesProvider activeRolesProvider;

@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
AuthenticatedPolarisPrincipal polarisPrincipal =
(AuthenticatedPolarisPrincipal) requestContext.getSecurityContext().getUserPrincipal();
if (polarisPrincipal == null) {
return;
}
SecurityContext securityContext =
createSecurityContext(requestContext.getSecurityContext(), polarisPrincipal);
requestContext.setSecurityContext(securityContext);
}

public SecurityContext createSecurityContext(
SecurityContext ctx, AuthenticatedPolarisPrincipal principal) {
Set<String> validRoleNames = activeRolesProvider.getActiveRoles(principal);
return new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return principal;
}

@Override
public boolean isUserInRole(String role) {
return validRoleNames.contains(role);
}

@Override
public boolean isSecure() {
return ctx.isSecure();
}

@Override
public String getAuthenticationScheme() {
return ctx.getAuthenticationScheme();
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public class PolarisApplicationConfig extends Configuration {
/**
* Override the default binding of registered services so that the configured instances are used.
*/
private static final int OVERRIDE_BINDING_RANK = 10;
private static final int OVERRIDE_BINDING_RANK = 20;

private MetaStoreManagerFactory metaStoreManagerFactory;
private String defaultRealm = "default-realm";
Expand Down Expand Up @@ -125,7 +125,7 @@ protected void configure() {
.to(FileIOFactory.class)
.ranked(OVERRIDE_BINDING_RANK);
bindFactory(SupplierFactory.create(serviceLocator, config::getPolarisAuthenticator))
.to(Authenticator.class)
.to(new TypeLiteral<Authenticator<String, AuthenticatedPolarisPrincipal>>() {})
.ranked(OVERRIDE_BINDING_RANK);
bindFactory(SupplierFactory.create(serviceLocator, () -> tokenBroker))
.to(TokenBrokerFactoryConfig.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ private PolarisAdminService newTestAdminService(Set<String> activatedPrincipalRo
final AuthenticatedPolarisPrincipal authenticatedPrincipal =
new AuthenticatedPolarisPrincipal(principalEntity, activatedPrincipalRoles);
return new PolarisAdminService(
callContext, entityManager, metaStoreManager, authenticatedPrincipal, polarisAuthorizer);
callContext,
entityManager,
metaStoreManager,
securityContext(authenticatedPrincipal, activatedPrincipalRoles),
polarisAuthorizer);
}

private void doTestSufficientPrivileges(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
import com.google.common.collect.ImmutableMap;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import jakarta.ws.rs.core.SecurityContext;
import java.io.IOException;
import java.time.Clock;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.iceberg.CatalogProperties;
import org.apache.iceberg.Schema;
import org.apache.iceberg.catalog.Catalog;
Expand Down Expand Up @@ -199,7 +201,11 @@ public void before() {

this.adminService =
new PolarisAdminService(
callContext, entityManager, metaStoreManager, authenticatedRoot, polarisAuthorizer);
callContext,
entityManager,
metaStoreManager,
securityContext(authenticatedRoot, Set.of()),
polarisAuthorizer);

String storageLocation = "file:///tmp/authz";
FileStorageConfigInfo storageConfigModel =
Expand Down Expand Up @@ -305,6 +311,32 @@ public void after() {
}
}

protected @Nonnull SecurityContext securityContext(
AuthenticatedPolarisPrincipal p, Set<String> roles) {
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
Mockito.when(securityContext.getUserPrincipal()).thenReturn(p);
Set<String> principalRoleNames = loadPrincipalRolesNames(p);
Mockito.when(securityContext.isUserInRole(Mockito.anyString()))
.thenAnswer(invocation -> principalRoleNames.contains(invocation.getArgument(0)));
return securityContext;
}

protected @Nonnull Set<String> loadPrincipalRolesNames(AuthenticatedPolarisPrincipal p) {
return metaStoreManager
.loadGrantsToGrantee(
callContext.getPolarisCallContext(), 0L, p.getPrincipalEntity().getId())
.getGrantRecords()
.stream()
.filter(gr -> gr.getPrivilegeCode() == PolarisPrivilege.PRINCIPAL_ROLE_USAGE.getCode())
.map(
gr ->
metaStoreManager.loadEntity(
callContext.getPolarisCallContext(), 0L, gr.getSecurableId()))
.map(PolarisMetaStoreManager.EntityResult::getEntity)
.map(PolarisBaseEntity::getName)
.collect(Collectors.toSet());
}

protected @Nonnull PrincipalEntity rotateAndRefreshPrincipal(
PolarisMetaStoreManager metaStoreManager,
String principalName,
Expand Down Expand Up @@ -350,15 +382,19 @@ private void initBaseCatalog() {
throw new RuntimeException(e);
}
}
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
Mockito.when(securityContext.getUserPrincipal()).thenReturn(authenticatedRoot);
Mockito.when(securityContext.isUserInRole(Mockito.anyString())).thenReturn(true);
PolarisPassthroughResolutionView passthroughView =
new PolarisPassthroughResolutionView(
callContext, entityManager, authenticatedRoot, CATALOG_NAME);
callContext, entityManager, securityContext, CATALOG_NAME);
this.baseCatalog =
new BasePolarisCatalog(
entityManager,
metaStoreManager,
callContext,
passthroughView,
securityContext,
authenticatedRoot,
Mockito.mock(),
new DefaultFileIOFactory());
Expand Down Expand Up @@ -386,11 +422,13 @@ public PolarisEntityManager getOrCreateEntityManager(RealmContext realmContext)
public Catalog createCallContextCatalog(
CallContext context,
AuthenticatedPolarisPrincipal authenticatedPolarisPrincipal,
SecurityContext securityContext,
final PolarisResolutionManifest resolvedManifest) {
// This depends on the BasePolarisCatalog allowing calling initialize multiple times
// to override the previous config.
Catalog catalog =
super.createCallContextCatalog(context, authenticatedPolarisPrincipal, resolvedManifest);
super.createCallContextCatalog(
context, authenticatedPolarisPrincipal, securityContext, resolvedManifest);
catalog.initialize(
CATALOG_NAME,
ImmutableMap.of(
Expand Down
Loading

0 comments on commit dcbb005

Please sign in to comment.