diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ddadba6df..33ac4d1c32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,19 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti are now always bootstrapped with the latest available schema version. - The `MaintenanceService.performMaintenance()` signature now requires an explicit `OptionalLong overrideRunId` argument to supersede the latest unfinished maintenance run. - Admin grant APIs now reject table-like privilege targets with an empty namespace. A table-like target without a namespace is considered invalid input. +- `PolarisPrincipal` now carries a generic `Map attributes` bag instead of the + previous `Map properties` and `Optional token` fields. Three well-known + attribute keys are defined as constants on the interface: + + - `PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY`: holds the `PrincipalEntity`; + - `PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY`: holds a boolean indicating if the + `PRINCIPAL_ROLE:ALL` pseudo-role is present; + - `PolarisPrincipal.JWT_ATTRIBUTE_KEY`: holds the raw JWT string. + + Use `PolarisPrincipal.getAttribute(key, type)` for type-safe access. The `Authenticator` interface + now accepts a Quarkus `SecurityIdentity` instead of a `PolarisCredential`, and throws + `AuthenticationFailedException` (mapped to HTTP 401) instead of Iceberg's + `NotAuthorizedException`. ### New Features - Added GCS principal attribution for vended credentials (the GCP counterpart of AWS STS session tags). Set `GCS_PRINCIPAL_ATTRIBUTION_ENABLED=true` to activate; the feature flags `GCS_PRINCIPAL_ATTRIBUTION_WIF_AUDIENCE`, `GCS_PRINCIPAL_ATTRIBUTION_TOKEN_ISSUER`, and `GCS_PRINCIPAL_ATTRIBUTION_SIGNING_KEY_FILE` are then required (a missing value is a fatal configuration error). Also requires a `gcpServiceAccount` on the catalog StorageConfiguration. When enabled, credential vending chains a catalog-signed JWT through a Workload Identity Federation token exchange and service-account impersonation, so the Polaris principal appears in GCS Data Access audit logs (`serviceAccountDelegationInfo.principalSubject`) for any client. `GCS_PRINCIPAL_ATTRIBUTION_SIGNING_KEY_ID` sets the JWT `kid` for JWKS key rotation. Attribution is keyed per-principal in the credential cache; when disabled (default), GCP vending behaviour is unchanged. diff --git a/extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/utils/RangerUtils.java b/extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/utils/RangerUtils.java index 740cb21ecd..117e982d30 100644 --- a/extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/utils/RangerUtils.java +++ b/extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/utils/RangerUtils.java @@ -30,6 +30,7 @@ import org.apache.polaris.core.auth.PolarisPrincipal; import org.apache.polaris.core.entity.PolarisEntity; import org.apache.polaris.core.entity.PolarisEntityType; +import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; import org.apache.polaris.core.persistence.ResolvedPolarisEntity; import org.apache.ranger.authz.model.RangerAccessInfo; @@ -127,10 +128,12 @@ private static Map getResourceAttributes( } private static Map getUserAttributes(PolarisPrincipal principal) { - Map properties = principal.getProperties(); + Map properties = + principal + .getAttribute(PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, PrincipalEntity.class) + .map(PrincipalEntity::getInternalPropertiesAsMap) + .orElse(Collections.emptyMap()); - return (properties == null || properties.isEmpty()) - ? Collections.emptyMap() - : new HashMap<>(properties); + return properties.isEmpty() ? Collections.emptyMap() : new HashMap<>(properties); } } diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java index 5d9e72b477..e5cf286a76 100644 --- a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java +++ b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java @@ -1028,7 +1028,7 @@ public void testCreatePrincipalAndRotateCredentials() { .isNotNull() .extracting(ErrorResponse::message) .asString() - .contains("PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE"); + .contains("must rotate credentials first"); } // Now try to rotate using the principal's token. diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationPreConditions.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationPreConditions.java index 32ed879124..452dee68be 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationPreConditions.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationPreConditions.java @@ -22,6 +22,7 @@ import org.apache.polaris.core.config.FeatureConfiguration; import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.entity.PolarisEntityConstants; +import org.apache.polaris.core.entity.PrincipalEntity; /** * Common pre-condition checks shared across authorizer implementations for credential-related @@ -48,12 +49,21 @@ public static void checkCredentialRotationRequired( if (realmConfig.getConfig( FeatureConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING) && authzOp != PolarisAuthorizableOperation.ROTATE_CREDENTIALS - && polarisPrincipal - .getProperties() - .containsKey(PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE)) { + && mustRotateCredentials(polarisPrincipal)) { throw new ForbiddenException( - "Principal '%s' is not authorized for op %s due to PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE", + "Principal '%s' is not authorized for op %s because it must rotate credentials first", polarisPrincipal.getName(), authzOp); } } + + private static boolean mustRotateCredentials(PolarisPrincipal principal) { + return principal + .getAttribute(PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, PrincipalEntity.class) + .map(PrincipalEntity::getInternalPropertiesAsMap) + .map( + map -> + map.containsKey( + PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE)) + .orElse(false); + } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisPrincipal.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisPrincipal.java index c29ac0f595..bfae04857d 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisPrincipal.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisPrincipal.java @@ -22,7 +22,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.immutables.PolarisImmutable; import org.immutables.value.Value; @@ -30,273 +29,75 @@ @PolarisImmutable public interface PolarisPrincipal extends Principal { - /** Indicates how principal roles should be selected during role resolution. */ - enum RoleSelection { - /** Resolve the specific role names returned by {@link PolarisPrincipal#getRoles()}. */ - SELECTED_ROLES, - - /** Resolve all roles currently granted to the principal. */ - ALL_ROLES - } - - /** - * Creates a new instance of {@link PolarisPrincipal} from the given {@link PrincipalEntity} and - * selected role names. - * - *

The created principal will have the same ID and name as the {@link PrincipalEntity}, and its - * properties will be derived from the internal properties of the entity. Role resolution will be - * limited to the given {@code roles}; an empty set means no principal roles are selected. - * - * @param principalEntity the principal entity representing the user or service - * @param roles the selected principal role names - */ - static PolarisPrincipal of(PrincipalEntity principalEntity, Set roles) { - return of(principalEntity, roles, RoleSelection.SELECTED_ROLES); - } - - /** - * Creates a new instance of {@link PolarisPrincipal} from the given {@link PrincipalEntity}, - * selected role names, and role selection mode. - * - *

The created principal will have the same ID and name as the {@link PrincipalEntity}, and its - * properties will be derived from the internal properties of the entity. - * - * @param principalEntity the principal entity representing the user or service - * @param roles the selected principal role names - * @param roleSelection how principal roles should be selected during role resolution - */ - static PolarisPrincipal of( - PrincipalEntity principalEntity, Set roles, RoleSelection roleSelection) { - return of( - principalEntity.getName(), - principalEntity.getInternalPropertiesAsMap(), - roles, - roleSelection, - Optional.empty()); - } - - /** - * Creates a new instance of {@link PolarisPrincipal} from the given {@link PrincipalEntity} and - * selected role names. - * - *

The created principal will have the same ID and name as the {@link PrincipalEntity}, and its - * properties will be derived from the internal properties of the entity. Role resolution will be - * limited to the given {@code roles}; an empty set means no principal roles are selected. - * - * @param principalEntity the principal entity representing the user or service - * @param roles the selected principal role names - * @param token the access token of the current user - */ - static PolarisPrincipal of( - PrincipalEntity principalEntity, Set roles, Optional token) { - return of(principalEntity, roles, RoleSelection.SELECTED_ROLES, token); - } - - /** - * Creates a new instance of {@link PolarisPrincipal} from the given {@link PrincipalEntity}, - * selected role names, role selection mode, and token. - * - *

The created principal will have the same ID and name as the {@link PrincipalEntity}, and its - * properties will be derived from the internal properties of the entity. - * - * @param principalEntity the principal entity representing the user or service - * @param roles the selected principal role names - * @param roleSelection how principal roles should be selected during role resolution - * @param token the access token of the current user - */ - static PolarisPrincipal of( - PrincipalEntity principalEntity, - Set roles, - RoleSelection roleSelection, - Optional token) { - return of( - principalEntity.getName(), - principalEntity.getInternalPropertiesAsMap(), - roles, - roleSelection, - token); - } - - /** - * Creates a new instance of {@link PolarisPrincipal} with the specified name, properties, and - * selected role names. - * - *

Role resolution will be limited to the given {@code roles}; an empty set means no principal - * roles are selected. - * - * @param name the name of the principal - * @param properties additional properties associated with the principal - * @param roles the selected principal role names - */ - static PolarisPrincipal of(String name, Map properties, Set roles) { - return of(name, properties, roles, RoleSelection.SELECTED_ROLES); - } - - /** - * Creates a new instance of {@link PolarisPrincipal} with the specified name, properties, - * selected role names, and role selection mode. - * - * @param name the name of the principal - * @param properties additional properties associated with the principal - * @param roles the selected principal role names - * @param roleSelection how principal roles should be selected during role resolution - */ - static PolarisPrincipal of( - String name, Map properties, Set roles, RoleSelection roleSelection) { - return of(name, properties, roles, roleSelection, Optional.empty()); - } - /** - * Creates a new instance of {@link PolarisPrincipal} with the specified name, properties, - * selected role names, and token. - * - *

Role resolution will be limited to the given {@code roles}; an empty set means no principal - * roles are selected. + * Attribute key for the principal entity attribute, of type {@link + * org.apache.polaris.core.entity.PrincipalEntity}. * - * @param name the name of the principal - * @param properties additional properties associated with the principal - * @param roles the selected principal role names - * @param token the access token of the current user + *

Note: callers must never assume that this attribute is present. */ - static PolarisPrincipal of( - String name, Map properties, Set roles, Optional token) { - return of(name, properties, roles, RoleSelection.SELECTED_ROLES, token); - } + String PRINCIPAL_ENTITY_ATTRIBUTE_KEY = "org.apache.polaris.core.auth.PRINCIPAL_ENTITY"; /** - * Creates a principal whose role selection should include all roles currently granted to the - * principal. + * Attribute key, of type {@link Boolean}, used to determine how authorizers should resolve + * principal roles. * - *

The returned principal does not carry a role-name snapshot. Callers that need a materialized - * list for downstream context should use an overload that accepts {@code roles}. - * - * @param principalEntity the principal entity representing the user or service - */ - static PolarisPrincipal ofAllRoles(PrincipalEntity principalEntity) { - return ofAllRoles(principalEntity, Optional.empty()); - } - - /** - * Creates a principal whose role selection should include all roles currently granted to the - * principal. + *

When absent or false, authorizers should resolve only the roles returned by {@link + * #getRoles()}; if an empty set is returned, then no principal roles should be resolved. * - *

The returned principal does not carry a role-name snapshot. Callers that need a materialized - * list for downstream context should use an overload that accepts {@code roles}. - * - * @param principalEntity the principal entity representing the user or service - * @param token the access token of the current user - */ - static PolarisPrincipal ofAllRoles(PrincipalEntity principalEntity, Optional token) { - return ofAllRoles(principalEntity, Set.of(), token); - } - - /** - * Creates a principal whose role selection should include all roles currently granted to the - * principal. - * - *

The returned principal does not carry a role-name snapshot. Callers that need a materialized - * list for downstream context should use an overload that accepts {@code roles}. - * - * @param name the name of the principal - * @param properties additional properties associated with the principal - * @param token the access token of the current user - */ - static PolarisPrincipal ofAllRoles( - String name, Map properties, Optional token) { - return ofAllRoles(name, properties, Set.of(), token); - } - - /** - * Creates a principal whose role selection should include all roles currently granted to the - * principal. + *

When present and true, authorizers should resolve all roles granted to the principal at the + * time of check, ignoring roles returned by {@link #getRoles()} (even if an empty set is + * returned). * - *

The {@code roles} argument is a materialized snapshot of activated role names, as captured - * or supplied when the principal was created, for downstream context such as request identity - * roles, diagnostics, events, and credential vending context. It does not limit role resolution - * when {@link #getRoleSelection()} is {@link RoleSelection#ALL_ROLES}. + *

This attribute is generally present and true when the principal presented a credential + * including the pseudo-role {@code PRINCIPAL_ROLE:ALL}. * - * @param principalEntity the principal entity representing the user or service - * @param roles materialized activated role names, if already known - * @param token the access token of the current user + *

Note: Callers must not assume that this attribute is always present. */ - static PolarisPrincipal ofAllRoles( - PrincipalEntity principalEntity, Set roles, Optional token) { - return of(principalEntity, roles, RoleSelection.ALL_ROLES, token); - } + String PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY = "org.apache.polaris.core.auth.PRINCIPAL_ROLE:ALL"; /** - * Creates a principal whose role selection should include all roles currently granted to the - * principal. + * Attribute key used to store or retrieve a JSON Web Token (JWT) associated with the {@link + * PolarisPrincipal}, of type {@link String}. * - *

The {@code roles} argument is a materialized snapshot of activated role names, as captured - * or supplied when the principal was created, for downstream context such as request identity - * roles, diagnostics, events, and credential vending context. It does not limit role resolution - * when {@link #getRoleSelection()} is {@link RoleSelection#ALL_ROLES}. + *

The associated value is expected to represent the JWT provided during authentication and may + * be used for further validation or for deriving additional claims. * - * @param name the name of the principal - * @param properties additional properties associated with the principal - * @param roles materialized activated role names, if already known - * @param token the access token of the current user + *

Note: callers must never assume that this attribute is present. */ - static PolarisPrincipal ofAllRoles( - String name, Map properties, Set roles, Optional token) { - return of(name, properties, roles, RoleSelection.ALL_ROLES, token); - } + String JWT_ATTRIBUTE_KEY = "org.apache.polaris.core.auth.JWT"; /** - * Creates a new instance of {@link PolarisPrincipal} with the specified name, properties, - * selected role names, role selection mode, and token. - * - *

When {@code roleSelection} is {@link RoleSelection#SELECTED_ROLES}, role resolution will be - * limited to the given {@code roles}; an empty set means no principal roles are selected. When - * {@code roleSelection} is {@link RoleSelection#ALL_ROLES}, role resolution is based on the - * principal's current grants rather than the role names returned by {@link #getRoles()}. + * Creates a new instance of {@link PolarisPrincipal} with the specified name, roles, and + * attributes. * * @param name the name of the principal - * @param properties additional properties associated with the principal - * @param roles the selected principal role names or materialized role-name snapshot - * @param roleSelection how principal roles should be selected during role resolution - * @param token the access token of the current user + * @param attributes the attributes of the principal + * @param roles the set of roles associated with the principal */ - static PolarisPrincipal of( - String name, - Map properties, - Set roles, - RoleSelection roleSelection, - Optional token) { + static PolarisPrincipal of(String name, Map attributes, Set roles) { return ImmutablePolarisPrincipal.builder() .name(name) - .properties(properties) - .token(token) + .attributes(attributes) .roles(roles) - .roleSelection(roleSelection) .build(); } - /** - * Returns the set of activated principal role names. - * - *

When {@link #getRoleSelection()} is {@link RoleSelection#SELECTED_ROLES}, this set is the - * selected role set used for role resolution. An empty set means no principal roles are selected. - * - *

When {@link #getRoleSelection()} is {@link RoleSelection#ALL_ROLES}, this set is only a - * materialized snapshot of activated role names, as captured or supplied when the principal was - * created, for downstream context. It does not limit role resolution. - */ - Set getRoles(); + /** Returns the principal attributes. */ + @Value.Redacted + Map getAttributes(); - /** Returns how principal roles should be selected during role resolution. */ - RoleSelection getRoleSelection(); + /** Returns the attribute value associated with the given key, if any. */ + default Optional getAttribute(String key, Class type) { + Object value = getAttributes().get(key); + return type.isInstance(value) ? Optional.of(type.cast(value)) : Optional.empty(); + } /** - * Returns the properties of this principal. + * Returns the set of activated principal role names. * - *

Properties are key-value pairs that provide additional information about the principal, such - * as permissions, preferences, or other metadata. + *

Activated role names are the roles that were explicitly requested by the client when + * authenticating, through JWT claims or other means. It may be a subset of the roles that the + * principal has in the system. */ - Map getProperties(); - - /** Optionally returns the access token of the current user. */ - @Value.Redacted - Optional getToken(); + Set getRoles(); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolver.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolver.java index 2a06f88a4d..c120eec833 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolver.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolver.java @@ -792,12 +792,15 @@ private ResolverStatus resolveCallerPrincipalAndPrincipalRoles( // activate all principal roles specified in the authenticated principal if (resolvePrincipalRoles) { - resolvedCallerPrincipalRoles = - switch (this.polarisPrincipal.getRoleSelection()) { - case ALL_ROLES -> resolveAllPrincipalRoles(toValidate, resolvedCallerPrincipal); - case SELECTED_ROLES -> - resolvePrincipalRolesByName(toValidate, this.polarisPrincipal.getRoles()); - }; + if (this.polarisPrincipal + .getAttribute(PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY, Boolean.class) + .orElse(false)) { + resolvedCallerPrincipalRoles = + resolveAllPrincipalRoles(toValidate, resolvedCallerPrincipal); + } else { + resolvedCallerPrincipalRoles = + resolvePrincipalRolesByName(toValidate, this.polarisPrincipal.getRoles()); + } } else { resolvedCallerPrincipalRoles = new ArrayList<>(); } diff --git a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationPreConditionsTest.java b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationPreConditionsTest.java index 59afef44ea..f6bc2bfaab 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationPreConditionsTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationPreConditionsTest.java @@ -28,13 +28,21 @@ import org.apache.iceberg.exceptions.ForbiddenException; import org.apache.polaris.core.config.FeatureConfiguration; import org.apache.polaris.core.config.RealmConfig; -import org.apache.polaris.core.entity.PolarisEntityConstants; +import org.apache.polaris.core.entity.PrincipalEntity; import org.junit.jupiter.api.Test; public class AuthorizationPreConditionsTest { - private static final String ROTATION_KEY = - PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE; + private static final PolarisPrincipal PRINCIPAL = + PolarisPrincipal.of( + "alice", + Map.of( + PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, + new PrincipalEntity.Builder() + .setName("alice") + .setCredentialRotationRequiredState() + .build()), + Set.of("role")); private static RealmConfig realmConfigWithEnforcement(boolean enforce) { RealmConfig realmConfig = mock(RealmConfig.class); @@ -46,13 +54,10 @@ private static RealmConfig realmConfigWithEnforcement(boolean enforce) { @Test public void testFlagOff_noException() { - PolarisPrincipal principal = - PolarisPrincipal.of("alice", Map.of(ROTATION_KEY, "true"), Set.of("role")); - assertThatCode( () -> AuthorizationPreConditions.checkCredentialRotationRequired( - principal, + PRINCIPAL, PolarisAuthorizableOperation.LIST_CATALOGS, realmConfigWithEnforcement(false))) .doesNotThrowAnyException(); @@ -60,13 +65,10 @@ public void testFlagOff_noException() { @Test public void testFlagOn_rotateCredentialsOp_noException() { - PolarisPrincipal principal = - PolarisPrincipal.of("alice", Map.of(ROTATION_KEY, "true"), Set.of("role")); - assertThatCode( () -> AuthorizationPreConditions.checkCredentialRotationRequired( - principal, + PRINCIPAL, PolarisAuthorizableOperation.ROTATE_CREDENTIALS, realmConfigWithEnforcement(true))) .doesNotThrowAnyException(); @@ -74,17 +76,14 @@ public void testFlagOn_rotateCredentialsOp_noException() { @Test public void testFlagOn_nonRotationOp_propertySet_throwsForbidden() { - PolarisPrincipal principal = - PolarisPrincipal.of("alice", Map.of(ROTATION_KEY, "true"), Set.of("role")); - assertThatThrownBy( () -> AuthorizationPreConditions.checkCredentialRotationRequired( - principal, + PRINCIPAL, PolarisAuthorizableOperation.LIST_CATALOGS, realmConfigWithEnforcement(true))) .isInstanceOf(ForbiddenException.class) - .hasMessageContaining("PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE"); + .hasMessageContaining("must rotate credentials"); } @Test diff --git a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BaseResolverTest.java b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BaseResolverTest.java index 562798c584..23556d8005 100644 --- a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BaseResolverTest.java +++ b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BaseResolverTest.java @@ -411,7 +411,7 @@ protected void testResolveCatalogRole(boolean useCache) { @NonNull private Resolver allocateResolver( @Nullable InMemoryEntityCache cache, - Set principalRolesScope, + @Nullable Set principalRolesScope, @Nullable String referenceCatalogName) { // create a new cache if needs be @@ -419,10 +419,18 @@ private Resolver allocateResolver( this.cache = new InMemoryEntityCache(diagServices, callCtx().getRealmConfig(), metaStoreManager()); } + + PrincipalEntity principalEntity = PrincipalEntity.of(P1); PolarisPrincipal authenticatedPrincipal = - principalRolesScope == null - ? PolarisPrincipal.ofAllRoles(PrincipalEntity.of(P1)) - : PolarisPrincipal.of(PrincipalEntity.of(P1), principalRolesScope); + PolarisPrincipal.of( + principalEntity.getName(), + Map.of( + PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, + principalEntity, + PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY, + principalRolesScope == null), + Optional.ofNullable(principalRolesScope).orElse(Set.of())); + return new Resolver( diagServices, callCtx(), diff --git a/runtime/service/src/main/java/org/apache/polaris/service/auth/AuthenticatingAugmentor.java b/runtime/service/src/main/java/org/apache/polaris/service/auth/AuthenticatingAugmentor.java index 4166b08008..5697272243 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/auth/AuthenticatingAugmentor.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/auth/AuthenticatingAugmentor.java @@ -18,7 +18,6 @@ */ package org.apache.polaris.service.auth; -import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.SecurityIdentityAugmentor; @@ -26,13 +25,14 @@ import io.smallrye.mutiny.Uni; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; -import org.apache.iceberg.exceptions.ServiceFailureException; import org.apache.polaris.core.auth.PolarisPrincipal; /** * A custom {@link SecurityIdentityAugmentor} that, after Quarkus OIDC or Internal Auth extracted * and validated the principal credentials, augments the {@link SecurityIdentity} by authenticating - * the principal and setting a {@link PolarisPrincipal} as the identity's principal. + * the principal. + * + * @see Authenticator */ @ApplicationScoped public class AuthenticatingAugmentor implements SecurityIdentityAugmentor { @@ -54,42 +54,18 @@ public int priority() { @Override public Uni augment( SecurityIdentity identity, AuthenticationRequestContext context) { - if (identity.isAnonymous()) { - return Uni.createFrom().item(identity); - } - PolarisCredential authInfo = extractPolarisCredential(identity); - return context.runBlocking(() -> authenticatePolarisPrincipal(identity, authInfo)); - } - - private PolarisCredential extractPolarisCredential(SecurityIdentity identity) { - PolarisCredential credential = identity.getCredential(PolarisCredential.class); - if (credential == null) { - throw new AuthenticationFailedException("No token credential available"); - } - return credential; + return identity.isAnonymous() + ? Uni.createFrom().item(identity) + : context.runBlocking(() -> authenticatePolarisPrincipal(identity)); } - private SecurityIdentity authenticatePolarisPrincipal( - SecurityIdentity identity, PolarisCredential polarisCredential) { - try { - PolarisPrincipal polarisPrincipal = authenticator.authenticate(polarisCredential); - QuarkusSecurityIdentity.Builder builder = - QuarkusSecurityIdentity.builder() - .setAnonymous(false) - .setPrincipal(polarisPrincipal) - .addRoles(polarisPrincipal.getRoles()) - .addCredentials(identity.getCredentials()) - .addAttributes(identity.getAttributes()) - .addPermissionChecker(identity::checkPermission); - // Also include the Polaris principal properties as attributes of the identity - polarisPrincipal.getProperties().forEach(builder::addAttribute); - return builder.build(); - } catch (ServiceFailureException e) { - // Let ServiceFailureException bubble up to be handled by IcebergExceptionMapper - // This will result in 503 Service Unavailable instead of 401 Unauthorized - throw e; - } catch (RuntimeException e) { - throw new AuthenticationFailedException(e); - } + private SecurityIdentity authenticatePolarisPrincipal(SecurityIdentity identity) { + PolarisPrincipal polarisPrincipal = authenticator.authenticate(identity); + return QuarkusSecurityIdentity.builder(identity) + .setAnonymous(false) + .setPrincipal(polarisPrincipal) + .addRoles(polarisPrincipal.getRoles()) + .addAttributes(polarisPrincipal.getAttributes()) + .build(); } } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/auth/Authenticator.java b/runtime/service/src/main/java/org/apache/polaris/service/auth/Authenticator.java index 269390b65b..7ef3524d14 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/auth/Authenticator.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/auth/Authenticator.java @@ -18,30 +18,29 @@ */ package org.apache.polaris.service.auth; -import org.apache.iceberg.exceptions.NotAuthorizedException; -import org.apache.iceberg.exceptions.ServiceFailureException; +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.identity.SecurityIdentity; import org.apache.polaris.core.auth.PolarisPrincipal; /** - * An interface for authenticating {@linkplain PolarisPrincipal principals} based on provided - * {@linkplain PolarisCredential credentials}. + * An interface for authenticating {@linkplain PolarisPrincipal principals}. * *

Authenticators are used in both internal and external authentication scenarios. */ public interface Authenticator { /** - * Authenticates the given {@link PolarisCredential} and returns an authenticated {@link + * Authenticates the given {@link SecurityIdentity} and returns an authenticated {@link * PolarisPrincipal}. * *

If the credentials are not valid or if the authentication fails, implementations MUST throw - * {@link NotAuthorizedException}. + * {@link AuthenticationFailedException}, which is mapped to 401 (Unauthorized) HTTP response + * codes. It is also possible to throw other unchecked exceptions to signal unusual conditions; + * these will be mapped to the appropriate HTTP response codes. * - * @param credentials the credentials to authenticate + * @param identity the identity to authenticate * @return the authenticated principal - * @throws NotAuthorizedException if the credentials are not authorized - * @throws ServiceFailureException if there is a failure in the authentication service + * @throws AuthenticationFailedException if authentication fails */ - PolarisPrincipal authenticate(PolarisCredential credentials) - throws NotAuthorizedException, ServiceFailureException; + PolarisPrincipal authenticate(SecurityIdentity identity) throws AuthenticationFailedException; } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java b/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java index 3235f2b930..fd6ab7c7a1 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java @@ -19,20 +19,20 @@ package org.apache.polaris.service.auth; import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.identity.SecurityIdentity; import io.smallrye.common.annotation.Identifier; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; +import jakarta.ws.rs.ServiceUnavailableException; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import org.apache.iceberg.exceptions.NotAuthorizedException; -import org.apache.iceberg.exceptions.ServiceFailureException; import org.apache.polaris.core.PolarisCallContext; import org.apache.polaris.core.PolarisDiagnostics; import org.apache.polaris.core.auth.PolarisPrincipal; -import org.apache.polaris.core.auth.PolarisPrincipal.RoleSelection; import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.entity.PolarisBaseEntity; import org.apache.polaris.core.entity.PolarisEntityType; @@ -41,6 +41,7 @@ import org.apache.polaris.core.entity.PrincipalRoleEntity; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.dao.entity.LoadGrantsResult; +import org.eclipse.microprofile.jwt.JsonWebToken; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -91,31 +92,36 @@ public class DefaultAuthenticator implements Authenticator { @Inject PolarisDiagnostics diagnostics; @Override - public PolarisPrincipal authenticate(PolarisCredential credentials) { + public PolarisPrincipal authenticate(SecurityIdentity identity) { + PolarisCredential credentials = extractPolarisCredential(identity); LOGGER.debug("Resolving principal for credentials: {}", credentials); PrincipalEntity principalEntity = resolvePrincipalEntity(credentials); PrincipalRoleSelection principalRoles = resolvePrincipalRoles(credentials, principalEntity); + Map principalAttributes = + resolvePrincipalAttributes(identity, principalEntity, principalRoles.allRolesRequested()); PolarisPrincipal polarisPrincipal = - PolarisPrincipal.of( - principalEntity, - principalRoles.roles(), - principalRoles.allRolesRequested() - ? RoleSelection.ALL_ROLES - : RoleSelection.SELECTED_ROLES, - Optional.ofNullable(credentials.getToken())); + PolarisPrincipal.of(principalEntity.getName(), principalAttributes, principalRoles.roles()); LOGGER.debug("Resolved principal: {}", polarisPrincipal); return polarisPrincipal; } + private static PolarisCredential extractPolarisCredential(SecurityIdentity identity) { + PolarisCredential credential = identity.getCredential(PolarisCredential.class); + if (credential == null) { + throw new AuthenticationFailedException("No Polaris credential available"); + } + return credential; + } + /** * Resolves the principal entity based on the provided credentials. * *

This method attempts to load the principal entity using either the principal ID or the * principal name from the credentials. If neither is available, nor if the principal entity can - * be found, it throws a {@link NotAuthorizedException}. + * be found, it throws a {@link AuthenticationFailedException}. */ protected PrincipalEntity resolvePrincipalEntity(PolarisCredential credentials) { @@ -141,18 +147,31 @@ protected PrincipalEntity resolvePrincipalEntity(PolarisCredential credentials) .atError() .addKeyValue("errMsg", e.getMessage()) .addKeyValue("stackTrace", Throwables.getStackTraceAsString(e)) - .log("Unable to authenticate user with token"); - throw new ServiceFailureException("Unable to fetch principal entity"); + .log("Unable to resolve principal entity from credentials"); + throw new ServiceUnavailableException("Unable to fetch principal entity"); } if (principal == null || principal.getType() != PolarisEntityType.PRINCIPAL) { LOGGER.warn("Failed to resolve principal from credentials={}", credentials); - throw new NotAuthorizedException("Unable to authenticate"); + throw new AuthenticationFailedException("Unable to authenticate"); } return principal; } + protected Map resolvePrincipalAttributes( + SecurityIdentity identity, PrincipalEntity principalEntity, boolean allRolesRequested) { + ImmutableMap.Builder principalAttributes = + ImmutableMap.builder() + .putAll(identity.getAttributes()) + .put(PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, principalEntity) + .put(PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY, allRolesRequested); + if (identity.getPrincipal() instanceof JsonWebToken jwt) { + principalAttributes.put(PolarisPrincipal.JWT_ATTRIBUTE_KEY, jwt.getRawToken()); + } + return principalAttributes.buildKeepingLast(); + } + /** * Resolves the roles for the given principal based on the provided credentials. * @@ -191,7 +210,7 @@ protected PrincipalRoleSelection resolvePrincipalRoles( .addKeyValue("credentials", credentials) .addKeyValue("roles", activeRoles) .log("Some principal roles were not found in the principal's grants"); - throw new NotAuthorizedException("Unable to authenticate"); + throw new AuthenticationFailedException("Unable to authenticate"); } return new PrincipalRoleSelection(activeRoles, requestedRoles.allRolesRequested()); @@ -249,7 +268,7 @@ protected LoadGrantsResult loadPrincipalGrants(PrincipalEntity principal) { "Failed to resolve principal roles for principal name={} id={}", principal.getName(), principal.getId()); - throw new NotAuthorizedException("Unable to authenticate"); + throw new AuthenticationFailedException("Unable to authenticate"); } return principalGrantResults; } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/auth/PolarisCredential.java b/runtime/service/src/main/java/org/apache/polaris/service/auth/PolarisCredential.java index 32b7f379b3..4093a9b18e 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/auth/PolarisCredential.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/auth/PolarisCredential.java @@ -21,7 +21,6 @@ import io.quarkus.security.credential.Credential; import java.util.Set; import org.apache.polaris.immutables.PolarisImmutable; -import org.immutables.value.Value; import org.jspecify.annotations.Nullable; /** @@ -33,19 +32,10 @@ public interface PolarisCredential extends Credential { static PolarisCredential of( @Nullable Long principalId, @Nullable String principalName, Set principalRoles) { - return of(principalId, principalName, principalRoles, null); - } - - static PolarisCredential of( - @Nullable Long principalId, - @Nullable String principalName, - Set principalRoles, - @Nullable String token) { return ImmutablePolarisCredential.builder() .principalId(principalId) .principalName(principalName) .principalRoles(principalRoles) - .token(token) .build(); } @@ -57,11 +47,4 @@ static PolarisCredential of( /** The principal roles, or empty if the principal has no roles. */ Set getPrincipalRoles(); - - /** - * The access token of the current user, or null if not applicable. Redacted from {@code - * toString()} so it is not leaked into logs. - */ - @Value.Redacted - @Nullable String getToken(); } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentor.java b/runtime/service/src/main/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentor.java index 0a8a8eeb33..30fe86cccd 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentor.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentor.java @@ -90,12 +90,7 @@ protected SecurityIdentity setPolarisCredential( principalMapper.mapPrincipalId(identity).stream().boxed().findFirst().orElse(null); String principalName = principalMapper.mapPrincipalName(identity).orElse(null); Set principalRoles = rolesMapper.mapPrincipalRoles(identity); - String token = null; - if (identity.getPrincipal() instanceof JsonWebToken jwt) { - token = jwt.getRawToken(); - } - PolarisCredential credential = - PolarisCredential.of(principalId, principalName, principalRoles, token); + PolarisCredential credential = PolarisCredential.of(principalId, principalName, principalRoles); // Note: we don't change the identity roles here, this will be done later on // by the AuthenticatingAugmentor, which will also validate them. return QuarkusSecurityIdentity.builder(identity).addCredential(credential).build(); diff --git a/runtime/service/src/main/java/org/apache/polaris/service/auth/external/tenant/OidcTenantResolvingAugmentor.java b/runtime/service/src/main/java/org/apache/polaris/service/auth/external/tenant/OidcTenantResolvingAugmentor.java index 8c22f8a44b..10f1900521 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/auth/external/tenant/OidcTenantResolvingAugmentor.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/auth/external/tenant/OidcTenantResolvingAugmentor.java @@ -35,7 +35,8 @@ @ApplicationScoped public class OidcTenantResolvingAugmentor implements SecurityIdentityAugmentor { - public static final String TENANT_CONFIG_ATTRIBUTE = "polaris-tenant-config"; + public static final String TENANT_CONFIG_ATTRIBUTE = + "org.apache.polaris.service.auth.TENANT_CONFIG"; // must run before OidcPolarisCredentialAugmentor public static final int PRIORITY = OidcPolarisCredentialAugmentor.PRIORITY + 100; diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java index 0e6fc24f09..52ab0a5cef 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java @@ -371,11 +371,14 @@ public void testUpdateCatalogWithDisallowedConfigs() { private PolarisAdminService setupPolarisAdminService( PolarisMetaStoreManager metaStoreManager, PolarisCallContext callContext) { + PrincipalEntity rootPrincipal = + new PrincipalEntity.Builder() + .setName(PolarisEntityConstants.getRootPrincipalName()) + .build(); PolarisPrincipal principal = PolarisPrincipal.of( - new PrincipalEntity.Builder() - .setName(PolarisEntityConstants.getRootPrincipalName()) - .build(), + rootPrincipal.getName(), + Map.of(PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, rootPrincipal), Set.of(PolarisEntityConstants.getNameOfPrincipalServiceAdminRole())); return new PolarisAdminService( callContext, diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java index 132eba1d74..9cc68bd942 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java @@ -42,7 +42,15 @@ @TestProfile(Profiles.PolarisAuthzBaseProfile.class) public class PolarisAdminServiceAuthzTest extends PolarisAuthzTestBase { private PolarisAdminService newTestAdminService() { - final PolarisPrincipal authenticatedPrincipal = PolarisPrincipal.ofAllRoles(principalEntity); + final PolarisPrincipal authenticatedPrincipal = + PolarisPrincipal.of( + principalEntity.getName(), + Map.of( + PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, + principalEntity, + PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY, + true), + Set.of()); return new PolarisAdminService( callContext, resolutionManifestFactory, @@ -56,7 +64,10 @@ private PolarisAdminService newTestAdminService() { private PolarisAdminService newTestAdminService(Set activatedPrincipalRoles) { final PolarisPrincipal authenticatedPrincipal = - PolarisPrincipal.of(principalEntity, activatedPrincipalRoles); + PolarisPrincipal.of( + principalEntity.getName(), + Map.of(PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, principalEntity), + activatedPrincipalRoles); return PolarisAdminServiceTestSupport.newAdminService( callContext, resolutionManifestFactory, diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java index ce9b775a92..a5b39813ef 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java @@ -33,6 +33,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import org.apache.iceberg.CatalogProperties; import org.apache.iceberg.Schema; @@ -218,7 +219,15 @@ public void before(TestInfo testInfo) { PrincipalEntity rootPrincipal = metaStoreManager.findRootPrincipal(polarisContext).orElseThrow(); - this.authenticatedRoot = PolarisPrincipal.ofAllRoles(rootPrincipal); + this.authenticatedRoot = + PolarisPrincipal.of( + rootPrincipal.getName(), + Map.of( + PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, + rootPrincipal, + PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY, + true), + Set.of()); QuarkusMock.installMockForType(authenticatedRoot, PolarisPrincipal.class); this.adminService = diff --git a/runtime/service/src/test/java/org/apache/polaris/service/auth/AuthenticatingAugmentorTest.java b/runtime/service/src/test/java/org/apache/polaris/service/auth/AuthenticatingAugmentorTest.java index 5a1c682a02..440a911c5c 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/auth/AuthenticatingAugmentorTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/auth/AuthenticatingAugmentorTest.java @@ -28,8 +28,6 @@ import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.smallrye.mutiny.Uni; import java.security.Principal; -import org.apache.iceberg.exceptions.NotAuthorizedException; -import org.apache.iceberg.exceptions.ServiceFailureException; import org.apache.polaris.core.auth.PolarisPrincipal; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -58,20 +56,6 @@ public void testAugmentAnonymousIdentity() { assertThat(result.await().indefinitely()).isSameAs(anonymousIdentity); } - @Test - public void testAugmentMissingCredential() { - // Given - Principal nonPolarisPrincipal = mock(Principal.class); - SecurityIdentity identity = - QuarkusSecurityIdentity.builder().setPrincipal(nonPolarisPrincipal).build(); - - // When/Then - assertThatThrownBy( - () -> augmentor.augment(identity, Uni.createFrom()::item).await().indefinitely()) - .isInstanceOf(AuthenticationFailedException.class) - .hasMessage("No token credential available"); - } - @Test public void testAugmentAuthenticationFailure() { // Given @@ -83,34 +67,14 @@ public void testAugmentAuthenticationFailure() { .addCredential(credential) .build(); - RuntimeException exception = new NotAuthorizedException("Authentication error"); - when(authenticator.authenticate(credential)).thenThrow(exception); + AuthenticationFailedException exception = + new AuthenticationFailedException("Authentication error"); + when(authenticator.authenticate(identity)).thenThrow(exception); // When/Then assertThatThrownBy( () -> augmentor.augment(identity, Uni.createFrom()::item).await().indefinitely()) - .isInstanceOf(AuthenticationFailedException.class) - .hasCause(exception); - } - - @Test - public void testServiceFailureExceptionBubblesUp() { - Principal nonPolarisPrincipal = mock(Principal.class); - PolarisCredential credential = mock(PolarisCredential.class); - SecurityIdentity identity = - QuarkusSecurityIdentity.builder() - .setPrincipal(nonPolarisPrincipal) - .addCredential(credential) - .build(); - - ServiceFailureException serviceException = - new ServiceFailureException("Unable to fetch principal entity"); - when(authenticator.authenticate(credential)).thenThrow(serviceException); - - assertThatThrownBy( - () -> augmentor.augment(identity, Uni.createFrom()::item).await().indefinitely()) - .isInstanceOf(ServiceFailureException.class) - .hasMessage("Unable to fetch principal entity"); + .isSameAs(exception); } @Test @@ -125,7 +89,7 @@ public void testAugmentSuccessfulAuthentication() { .addCredential(credential) .build(); - when(authenticator.authenticate(credential)).thenReturn(polarisPrincipal); + when(authenticator.authenticate(identity)).thenReturn(polarisPrincipal); // When SecurityIdentity result = diff --git a/runtime/service/src/test/java/org/apache/polaris/service/auth/DefaultAuthenticatorTest.java b/runtime/service/src/test/java/org/apache/polaris/service/auth/DefaultAuthenticatorTest.java index 3a97930520..4d8a9d58bd 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/auth/DefaultAuthenticatorTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/auth/DefaultAuthenticatorTest.java @@ -27,20 +27,20 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.CurrentIdentityAssociation; +import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.quarkus.test.junit.QuarkusTest; import io.smallrye.common.annotation.Identifier; import jakarta.inject.Inject; +import jakarta.ws.rs.ServiceUnavailableException; import java.util.Map; -import java.util.Optional; import java.util.Set; -import org.apache.iceberg.exceptions.NotAuthorizedException; import org.apache.polaris.core.PolarisDiagnostics; import org.apache.polaris.core.admin.model.PrincipalWithCredentialsCredentials; import org.apache.polaris.core.auth.PolarisAuthorizer; import org.apache.polaris.core.auth.PolarisPrincipal; -import org.apache.polaris.core.auth.PolarisPrincipal.RoleSelection; import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.entity.PolarisEntityConstants; import org.apache.polaris.core.entity.PolarisEntityType; @@ -55,6 +55,7 @@ import org.apache.polaris.service.admin.PolarisAdminServiceTestSupport; import org.apache.polaris.service.config.ReservedProperties; import org.apache.polaris.service.context.catalog.RealmContextHolder; +import org.eclipse.microprofile.jwt.JsonWebToken; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; @@ -95,8 +96,10 @@ public class DefaultAuthenticatorTest { public void setup(TestInfo testInfo) { realmContextHolder.set(() -> testInfo.getTestMethod().orElseThrow().getName()); PolarisPrincipal root = - PolarisPrincipal.ofAllRoles( - PolarisEntityConstants.getRootPrincipalName(), Map.of(), Set.of(), Optional.empty()); + PolarisPrincipal.of( + PolarisEntityConstants.getRootPrincipalName(), + Map.of(PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY, true), + Set.of()); authenticatedRoot = root; identityAssociation.setIdentity(QuarkusSecurityIdentity.builder().setPrincipal(root).build()); principalEntity = createPrincipal(PRINCIPAL_NAME, PRINCIPAL_ROLE1, PRINCIPAL_ROLE2); @@ -109,7 +112,7 @@ void testNullPrincipalIdAndName() { PolarisCredential credentials = PolarisCredential.of(null, null, Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_ALL)); - // When/Then: authentication should fail with NotAuthorizedException + // When/Then: authentication should fail with AuthenticationFailedException assertUnauthorized(credentials); } @@ -120,7 +123,7 @@ void testPrincipalNotFoundByName() { PolarisCredential.of( null, "non-existent-principal", Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_ALL)); - // When/Then: authentication should fail with NotAuthorizedException + // When/Then: authentication should fail with AuthenticationFailedException assertUnauthorized(credentials); } @@ -130,7 +133,7 @@ void testPrincipalNotFoundById() { PolarisCredential credentials = PolarisCredential.of(999999L, null, Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_ALL)); - // When/Then: authentication should fail with NotAuthorizedException + // When/Then: authentication should fail with AuthenticationFailedException assertUnauthorized(credentials); } @@ -141,12 +144,14 @@ public void testFetchPrincipalThrowsServiceExceptionOnMetastoreException() { PolarisCredential credentials = PolarisCredential.of(123L, null, Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_ALL)); - metaStoreManager = Mockito.spy(metaStoreManager); - when(metaStoreManager.loadEntity( - callContext.getPolarisCallContext(), 0L, 123L, PolarisEntityType.PRINCIPAL)) + PolarisMetaStoreManager metaStoreManagerSpy = Mockito.spy(metaStoreManager); + when(metaStoreManagerSpy.findPrincipalById(callContext.getPolarisCallContext(), 123L)) .thenThrow(new RuntimeException("Metastore exception")); - assertUnauthorized(credentials); + DefaultAuthenticator standaloneAuthenticator = newStandaloneAuthenticator(metaStoreManagerSpy); + + assertThatThrownBy(() -> standaloneAuthenticator.authenticate(identityFor(credentials))) + .isInstanceOf(ServiceUnavailableException.class); } @Test @@ -157,7 +162,7 @@ void testAuthenticationByPrincipalId() { principalEntity.getId(), null, Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_ALL)); // When: authenticating the principal - PolarisPrincipal result = authenticator.authenticate(credentials); + PolarisPrincipal result = authenticator.authenticate(identityFor(credentials)); // Then: should return principal with all assigned roles assertPrincipal(result, principalEntity, PRINCIPAL_ROLE1, PRINCIPAL_ROLE2); @@ -171,7 +176,7 @@ void testPrincipalFoundByName() { PolarisCredential.of(null, PRINCIPAL_NAME, Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_ALL)); // When: authenticating the principal - PolarisPrincipal result = authenticator.authenticate(credentials); + PolarisPrincipal result = authenticator.authenticate(identityFor(credentials)); // Then: should return principal with all assigned roles assertPrincipal(result, principalEntity, PRINCIPAL_ROLE1, PRINCIPAL_ROLE2); @@ -184,11 +189,12 @@ void testPrincipalFoundWithAllRolesRequested() { PolarisCredential.of(null, PRINCIPAL_NAME, Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_ALL)); // When: authenticating the principal - PolarisPrincipal result = authenticator.authenticate(credentials); + PolarisPrincipal result = authenticator.authenticate(identityFor(credentials)); // Then: should return principal with all assigned roles assertPrincipal(result, principalEntity, PRINCIPAL_ROLE1, PRINCIPAL_ROLE2); - assertThat(result.getRoleSelection()).isEqualTo(RoleSelection.ALL_ROLES); + assertThat(result.getAttributes()) + .containsEntry(PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY, true); } @Test @@ -201,11 +207,12 @@ void testPrincipalFoundWithSubsetOfRolesRequested() { Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_PREFIX + PRINCIPAL_ROLE1)); // When: authenticating the principal - PolarisPrincipal result = authenticator.authenticate(credentials); + PolarisPrincipal result = authenticator.authenticate(identityFor(credentials)); // Then: should return principal with only the requested role assertPrincipal(result, principalEntity, PRINCIPAL_ROLE1); - assertThat(result.getRoleSelection()).isEqualTo(RoleSelection.SELECTED_ROLES); + assertThat(result.getAttributes()) + .containsEntry(PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY, false); } @Test @@ -220,7 +227,7 @@ void testPrincipalFoundWithMultipleSpecificRolesRequested() { DefaultAuthenticator.PRINCIPAL_ROLE_PREFIX + PRINCIPAL_ROLE2)); // When: authenticating the principal - PolarisPrincipal result = authenticator.authenticate(credentials); + PolarisPrincipal result = authenticator.authenticate(identityFor(credentials)); // Then: should return principal with both requested roles assertPrincipal(result, principalEntity, PRINCIPAL_ROLE1, PRINCIPAL_ROLE2); @@ -234,7 +241,7 @@ void testPrincipalFoundButHasNoRolesAssigned() { null, PRINCIPAL_NAME_NO_ROLES, Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_ALL)); // When: authenticating the principal - PolarisPrincipal result = authenticator.authenticate(credentials); + PolarisPrincipal result = authenticator.authenticate(identityFor(credentials)); // Then: should return principal with empty roles set assertPrincipal(result, principalEntityNoRoles); @@ -249,7 +256,7 @@ void testRequestedRolesDoNotMapToSystemRoles() { PRINCIPAL_NAME, Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_PREFIX + "non-existent-role")); - // When/Then: authentication should fail with NotAuthorizedException + // When/Then: authentication should fail with AuthenticationFailedException assertUnauthorized(credentials); } @@ -264,7 +271,7 @@ void testMixedValidAndInvalidRolesRequested() { DefaultAuthenticator.PRINCIPAL_ROLE_PREFIX + PRINCIPAL_ROLE1, DefaultAuthenticator.PRINCIPAL_ROLE_PREFIX + "non-existent-role")); - // When/Then: authentication should fail with NotAuthorizedException + // When/Then: authentication should fail with AuthenticationFailedException assertUnauthorized(credentials); } @@ -282,7 +289,7 @@ void testRolesWithoutPrefixAreIgnored() { )); // When: authenticating the principal - PolarisPrincipal result = authenticator.authenticate(credentials); + PolarisPrincipal result = authenticator.authenticate(identityFor(credentials)); // Then: should return principal with only the properly prefixed role assertPrincipal(result, principalEntity, PRINCIPAL_ROLE1); @@ -297,11 +304,12 @@ void testEmptyRolesRequestedReturnsEmptyRoles() { ); // When: authenticating the principal - PolarisPrincipal result = authenticator.authenticate(credentials); + PolarisPrincipal result = authenticator.authenticate(identityFor(credentials)); // Then: should return principal with empty roles set assertPrincipal(result, principalEntity); - assertThat(result.getRoleSelection()).isEqualTo(RoleSelection.SELECTED_ROLES); + assertThat(result.getAttributes()) + .containsEntry(PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY, false); } @Test @@ -313,20 +321,12 @@ void testResolvePrincipalRolesUsesEntitiesFromLoadGrantsResult() { .isNotEmpty() .allMatch(e -> e.getType() == PolarisEntityType.PRINCIPAL_ROLE); - // Build a standalone DefaultAuthenticator wired to a spied metaStoreManager so we - // can verify call counts. Going through the CDI-managed authenticator would give - // us a client proxy, and field writes on that proxy do not propagate to the - // per-request instance the authenticator actually uses. PolarisMetaStoreManager metaStoreManagerSpy = Mockito.spy(metaStoreManager); - DefaultAuthenticator standaloneAuthenticator = new DefaultAuthenticator(); - standaloneAuthenticator.metaStoreManager = metaStoreManagerSpy; - standaloneAuthenticator.callContext = callContext; - standaloneAuthenticator.diagnostics = diagnostics; - PolarisCredential credentials = PolarisCredential.of(null, PRINCIPAL_NAME, Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_ALL)); - PolarisPrincipal result = standaloneAuthenticator.authenticate(credentials); + PolarisPrincipal result = + newStandaloneAuthenticator(metaStoreManagerSpy).authenticate(identityFor(credentials)); assertPrincipal(result, principalEntity, PRINCIPAL_ROLE1, PRINCIPAL_ROLE2); @@ -354,15 +354,11 @@ void testResolvePrincipalRolesFallsBackToLoadEntityWhenEntitiesNotPreloaded() { .loadGrantsToGrantee( any(), Mockito.argThat(p -> p != null && p.getId() == principalEntity.getId())); - DefaultAuthenticator standaloneAuthenticator = new DefaultAuthenticator(); - standaloneAuthenticator.metaStoreManager = metaStoreManagerSpy; - standaloneAuthenticator.callContext = callContext; - standaloneAuthenticator.diagnostics = diagnostics; - PolarisCredential credentials = PolarisCredential.of(null, PRINCIPAL_NAME, Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_ALL)); - PolarisPrincipal result = standaloneAuthenticator.authenticate(credentials); + PolarisPrincipal result = + newStandaloneAuthenticator(metaStoreManagerSpy).authenticate(identityFor(credentials)); // Roles should still resolve — the fallback path must produce the same result. assertPrincipal(result, principalEntity, PRINCIPAL_ROLE1, PRINCIPAL_ROLE2); @@ -381,12 +377,67 @@ void testPrincipalIdTakesPrecedenceOverName() { Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_ALL)); // When: authenticating the principal - PolarisPrincipal result = authenticator.authenticate(credentials); + PolarisPrincipal result = authenticator.authenticate(identityFor(credentials)); // Then: should return principal resolved by ID, not name assertPrincipal(result, principalEntity, PRINCIPAL_ROLE1, PRINCIPAL_ROLE2); } + @Test + void testJwtAttributeAbsentWhenNoJwtPrincipal() { + // Given: a normal (non-JWT) identity + PolarisCredential credentials = + PolarisCredential.of(null, PRINCIPAL_NAME, Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_ALL)); + + // When: authenticating the principal + PolarisPrincipal result = authenticator.authenticate(identityFor(credentials)); + + // Then: JWT attribute must not be present + assertThat(result.getAttribute(PolarisPrincipal.JWT_ATTRIBUTE_KEY, String.class)).isEmpty(); + } + + @Test + void testJwtAttributePresentWhenJwtPrincipal() { + // Given: an identity whose principal is a JsonWebToken + PolarisCredential credentials = + PolarisCredential.of(null, PRINCIPAL_NAME, Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_ALL)); + JsonWebToken jwt = Mockito.mock(JsonWebToken.class); + Mockito.when(jwt.getName()).thenReturn(PRINCIPAL_NAME); + Mockito.when(jwt.getRawToken()).thenReturn("raw.jwt.token"); + SecurityIdentity jwtIdentity = + QuarkusSecurityIdentity.builder() + .setAnonymous(false) + .setPrincipal(jwt) + .addCredential(credentials) + .build(); + + // When: authenticating + PolarisPrincipal result = authenticator.authenticate(jwtIdentity); + + // Then: JWT attribute must carry the raw token + assertThat(result.getAttribute(PolarisPrincipal.JWT_ATTRIBUTE_KEY, String.class)) + .hasValue("raw.jwt.token"); + } + + @Test + void testInputIdentityAttributesPassedThrough() { + // Given: an identity that already carries a custom attribute + PolarisCredential credentials = + PolarisCredential.of(null, PRINCIPAL_NAME, Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_ALL)); + SecurityIdentity identityWithAttrs = + QuarkusSecurityIdentity.builder() + .setAnonymous(true) + .addAttribute("custom-key", "custom-value") + .addCredential(credentials) + .build(); + + // When: authenticating + PolarisPrincipal result = authenticator.authenticate(identityWithAttrs); + + // Then: custom attribute must be present alongside the Polaris-specific ones + assertThat(result.getAttribute("custom-key", String.class)).hasValue("custom-value"); + } + private PrincipalEntity createPrincipal(String name, String... roles) { PrincipalWithCredentialsCredentials credentials = @@ -434,13 +485,37 @@ private void assertPrincipal(PolarisPrincipal result, PrincipalEntity entity, St assertThat(result).isNotNull(); assertThat(result.getName()).isEqualTo(entity.getName()); assertThat(result.getRoles()).containsExactlyInAnyOrder(roles); - assertThat(result.getProperties()) + assertThat(result.getAttributes()) + .hasSize(2) + .containsKey(PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY) + .containsKey(PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY); + assertThat( + result + .getAttribute( + PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, PrincipalEntity.class) + .map(PrincipalEntity::getInternalPropertiesAsMap) + .orElseThrow()) .containsKey(PolarisEntityConstants.getClientIdPropertyName()); } + private DefaultAuthenticator newStandaloneAuthenticator(PolarisMetaStoreManager msm) { + DefaultAuthenticator sa = new DefaultAuthenticator(); + sa.metaStoreManager = msm; + sa.callContext = callContext; + sa.diagnostics = diagnostics; + return sa; + } + + private static SecurityIdentity identityFor(PolarisCredential credentials) { + return QuarkusSecurityIdentity.builder() + .setPrincipal(() -> "alice") + .addCredential(credentials) + .build(); + } + private void assertUnauthorized(PolarisCredential credentials) { - assertThatThrownBy(() -> authenticator.authenticate(credentials)) - .isInstanceOf(NotAuthorizedException.class) + assertThatThrownBy(() -> authenticator.authenticate(identityFor(credentials))) + .isInstanceOf(AuthenticationFailedException.class) .hasMessageContaining("Unable to authenticate"); } } diff --git a/runtime/service/src/test/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentorTest.java b/runtime/service/src/test/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentorTest.java index 8aebaa88b2..4a583e7511 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentorTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentorTest.java @@ -100,7 +100,7 @@ public void testAugmentNonOidcPrincipal() { } @Test - public void testAugmentOidcPrincipalWithToken() { + public void testAugmentOidcPrincipal() { // Given JsonWebToken oidcPrincipal = mock(JsonWebToken.class); SecurityIdentity identity = @@ -109,7 +109,6 @@ public void testAugmentOidcPrincipalWithToken() { .addRole("ROLE1") .addAttribute(TENANT_CONFIG_ATTRIBUTE, config) .build(); - when(oidcPrincipal.getRawToken()).thenReturn("this_is_a_token"); when(principalMapper.mapPrincipalId(identity)).thenReturn(OptionalLong.of(123L)); when(principalMapper.mapPrincipalName(identity)).thenReturn(Optional.of("root")); when(principalRolesMapper.mapPrincipalRoles(identity)).thenReturn(Set.of("MAPPED_ROLE1")); @@ -122,35 +121,7 @@ public void testAugmentOidcPrincipalWithToken() { assertThat(result).isNotNull(); assertThat(result.getPrincipal()).isSameAs(oidcPrincipal); assertThat(result.getCredential(PolarisCredential.class)) - .isEqualTo(PolarisCredential.of(123L, "root", Set.of("MAPPED_ROLE1"), "this_is_a_token")); - // the identity roles should not change, since this is done by the ActiveRolesAugmentor - assertThat(result.getRoles()).containsExactlyInAnyOrder("ROLE1"); - } - - @Test - public void testAugmentOidcPrincipalWithNoToken() { - // Given - JsonWebToken oidcPrincipal = mock(JsonWebToken.class); - SecurityIdentity identity = - QuarkusSecurityIdentity.builder() - .setPrincipal(oidcPrincipal) - .addRole("ROLE1") - .addAttribute(TENANT_CONFIG_ATTRIBUTE, config) - .build(); - when(oidcPrincipal.getRawToken()).thenReturn(null); - when(principalMapper.mapPrincipalId(identity)).thenReturn(OptionalLong.of(123L)); - when(principalMapper.mapPrincipalName(identity)).thenReturn(Optional.of("root")); - when(principalRolesMapper.mapPrincipalRoles(identity)).thenReturn(Set.of("MAPPED_ROLE1")); - - // When - SecurityIdentity result = - augmentor.augment(identity, Uni.createFrom()::item).await().indefinitely(); - - // Then - assertThat(result).isNotNull(); - assertThat(result.getPrincipal()).isSameAs(oidcPrincipal); - assertThat(result.getCredential(PolarisCredential.class)) - .isEqualTo(PolarisCredential.of(123L, "root", Set.of("MAPPED_ROLE1"), null)); + .isEqualTo(PolarisCredential.of(123L, "root", Set.of("MAPPED_ROLE1"))); // the identity roles should not change, since this is done by the ActiveRolesAugmentor assertThat(result.getRoles()).containsExactlyInAnyOrder("ROLE1"); } diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/RootPrincipalAugmentor.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/RootPrincipalAugmentor.java index b6127a498d..8d9ff1dab3 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/RootPrincipalAugmentor.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/RootPrincipalAugmentor.java @@ -26,6 +26,7 @@ import io.smallrye.mutiny.Uni; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; +import java.util.Map; import java.util.Set; import org.apache.polaris.core.auth.PolarisPrincipal; import org.apache.polaris.core.context.CallContext; @@ -54,7 +55,11 @@ public Uni augment( .findRootPrincipal(innerCallContext.getPolarisCallContext()) .orElseThrow(); - PolarisPrincipal principal = PolarisPrincipal.of(rootPrincipal, Set.of("service_admin")); + PolarisPrincipal principal = + PolarisPrincipal.of( + rootPrincipal.getName(), + Map.of(PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, rootPrincipal), + Set.of("service_admin")); return Uni.createFrom() .item( diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java index 6f5bb312b1..97c9b0e307 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java @@ -29,6 +29,7 @@ import java.lang.reflect.Method; import java.util.List; import java.util.Map; +import java.util.Set; import org.apache.iceberg.CatalogProperties; import org.apache.iceberg.Schema; import org.apache.iceberg.catalog.Namespace; @@ -150,7 +151,15 @@ public void before(TestInfo testInfo) { PrincipalEntity rootPrincipal = metaStoreManager.findRootPrincipal(polarisContext).orElseThrow(); - authenticatedRoot = PolarisPrincipal.ofAllRoles(rootPrincipal); + authenticatedRoot = + PolarisPrincipal.of( + rootPrincipal.getName(), + Map.of( + PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, + rootPrincipal, + PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY, + true), + Set.of()); polarisPrincipalHolder.set(authenticatedRoot); PolarisAuthorizer authorizer = new PolarisAuthorizerImpl(realmConfig); diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/PolarisGenericTableCatalogHandlerAuthzTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/PolarisGenericTableCatalogHandlerAuthzTest.java index 116e9ef95f..3433a0119f 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/PolarisGenericTableCatalogHandlerAuthzTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/PolarisGenericTableCatalogHandlerAuthzTest.java @@ -46,11 +46,23 @@ public class PolarisGenericTableCatalogHandlerAuthzTest extends PolarisAuthzTest @jakarta.inject.Inject @Any Instance federatedCatalogFactories; private GenericTableCatalogHandler newWrapper() { - return newWrapper(PolarisPrincipal.ofAllRoles(principalEntity)); + return newWrapper( + PolarisPrincipal.of( + principalEntity.getName(), + Map.of( + PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, + principalEntity, + PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY, + true), + Set.of())); } private GenericTableCatalogHandler newWrapper(Set activatedPrincipalRoles) { - return newWrapper(PolarisPrincipal.of(principalEntity, activatedPrincipalRoles)); + return newWrapper( + PolarisPrincipal.of( + principalEntity.getName(), + Map.of(PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, principalEntity), + activatedPrincipalRoles)); } private GenericTableCatalogHandler newWrapper(PolarisPrincipal authenticatedPrincipal) { diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogHandlerAuthzTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogHandlerAuthzTest.java index 18410e479e..8270e1e834 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogHandlerAuthzTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogHandlerAuthzTest.java @@ -122,7 +122,15 @@ protected PolarisAuthzTestsFactory.Builder authzTestsBuilder(String operationNam @Inject IcebergCatalogHandlerFactory icebergCatalogHandlerFactory; protected IcebergCatalogHandler newHandler() { - PolarisPrincipal authenticatedPrincipal = PolarisPrincipal.ofAllRoles(principalEntity); + PolarisPrincipal authenticatedPrincipal = + PolarisPrincipal.of( + principalEntity.getName(), + Map.of( + PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, + principalEntity, + PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY, + true), + Set.of()); return icebergCatalogHandlerFactory.createHandler(CATALOG_NAME, authenticatedPrincipal); } @@ -133,7 +141,10 @@ private IcebergCatalogHandler newHandler(Set activatedPrincipalRoles) { private IcebergCatalogHandler newHandler( Set activatedPrincipalRoles, String catalogName, LocalCatalogFactory factory) { PolarisPrincipal authenticatedPrincipal = - PolarisPrincipal.of(principalEntity, activatedPrincipalRoles); + PolarisPrincipal.of( + principalEntity.getName(), + Map.of(PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, principalEntity), + activatedPrincipalRoles); IcebergCatalogHandler handler = icebergCatalogHandlerFactory.createHandler(catalogName, authenticatedPrincipal); ImmutableIcebergCatalogHandler.Builder builder = @@ -172,8 +183,12 @@ Stream testInsufficientPermissionsPriorToSecretRotation() { newRootAdminService().assignPrincipalRole(principalName, PRINCIPAL_ROLE1); newRootAdminService().assignPrincipalRole(principalName, PRINCIPAL_ROLE2); + PrincipalEntity principalEntity = newPrincipal.getPrincipal(); PolarisPrincipal authenticatedPrincipal = - PolarisPrincipal.of(newPrincipal.getPrincipal(), Set.of(PRINCIPAL_ROLE1, PRINCIPAL_ROLE2)); + PolarisPrincipal.of( + principalEntity.getName(), + Map.of(PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, principalEntity), + Set.of(PRINCIPAL_ROLE1, PRINCIPAL_ROLE2)); Supplier handler = () -> @@ -219,7 +234,10 @@ Stream testInsufficientPermissionsPriorToSecretRotation() { rotateAndRefreshPrincipal( metaStoreManager, principalName, credentials, callContext.getPolarisCallContext()); PolarisPrincipal authenticatedPrincipal1 = - PolarisPrincipal.of(refreshPrincipal, Set.of(PRINCIPAL_ROLE1, PRINCIPAL_ROLE2)); + PolarisPrincipal.of( + refreshPrincipal.getName(), + Map.of(PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, refreshPrincipal), + Set.of(PRINCIPAL_ROLE1, PRINCIPAL_ROLE2)); Supplier refreshedWrapper = () -> @@ -1166,7 +1184,15 @@ Stream testUpdateTableFallbackToCoarseGrainedWhenFeatureDisabled() * behavior to coarse-grained authorization. */ private IcebergCatalogHandler newHandlerWithFineGrainedAuthzDisabled() { - PolarisPrincipal authenticatedPrincipal = PolarisPrincipal.ofAllRoles(principalEntity); + PolarisPrincipal authenticatedPrincipal = + PolarisPrincipal.of( + principalEntity.getName(), + Map.of( + PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, + principalEntity, + PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY, + true), + Set.of()); // Create a custom CallContext that returns a custom RealmConfig CallContext mockCallContext = Mockito.mock(CallContext.class); diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractLocalIcebergCatalogViewTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractLocalIcebergCatalogViewTest.java index ae47409bc4..7e11ab1952 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractLocalIcebergCatalogViewTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractLocalIcebergCatalogViewTest.java @@ -167,7 +167,15 @@ public void before(TestInfo testInfo) { PrincipalEntity rootPrincipal = metaStoreManager.findRootPrincipal(polarisContext).orElseThrow(); - authenticatedRoot = PolarisPrincipal.ofAllRoles(rootPrincipal); + authenticatedRoot = + PolarisPrincipal.of( + rootPrincipal.getName(), + Map.of( + PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, + rootPrincipal, + PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY, + true), + Set.of()); authorizer = new PolarisAuthorizerImpl(realmConfig); reservedProperties = ReservedProperties.NONE; diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFineGrainedDisabledTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFineGrainedDisabledTest.java index 1c345ee059..842e09fbaa 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFineGrainedDisabledTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFineGrainedDisabledTest.java @@ -24,6 +24,7 @@ import jakarta.inject.Inject; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.stream.Stream; import org.apache.iceberg.MetadataUpdate; @@ -47,7 +48,15 @@ public class IcebergCatalogHandlerFineGrainedDisabledTest extends PolarisAuthzTe @Inject IcebergCatalogHandlerFactory icebergCatalogHandlerFactory; private IcebergCatalogHandler newHandler() { - PolarisPrincipal authenticatedPrincipal = PolarisPrincipal.ofAllRoles(principalEntity); + PolarisPrincipal authenticatedPrincipal = + PolarisPrincipal.of( + principalEntity.getName(), + Map.of( + PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, + principalEntity, + PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY, + true), + Set.of()); IcebergCatalogHandler handler = icebergCatalogHandlerFactory.createHandler(CATALOG_NAME, authenticatedPrincipal); return ImmutableIcebergCatalogHandler.builder().from(handler).build(); diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java index 2e4ef40a54..3a1152bd5e 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java @@ -35,6 +35,8 @@ import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.Set; import org.apache.iceberg.CatalogProperties; import org.apache.iceberg.Schema; import org.apache.iceberg.catalog.Namespace; @@ -171,7 +173,15 @@ public void before(TestInfo testInfo) { PrincipalEntity rootPrincipal = metaStoreManager.findRootPrincipal(polarisContext).orElseThrow(); - authenticatedRoot = PolarisPrincipal.ofAllRoles(rootPrincipal); + authenticatedRoot = + PolarisPrincipal.of( + rootPrincipal.getName(), + Map.of( + PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, + rootPrincipal, + PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY, + true), + Set.of()); polarisPrincipalHolder.set(authenticatedRoot); PolarisAuthorizer authorizer = new PolarisAuthorizerImpl(realmConfig); diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java index 4826f87d87..a42f9e59a2 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java @@ -21,6 +21,7 @@ import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; import java.util.Arrays; +import java.util.Map; import java.util.Set; import java.util.stream.Stream; import org.apache.polaris.core.auth.PolarisPrincipal; @@ -43,11 +44,23 @@ public class PolicyCatalogHandlerAuthzTest extends PolarisAuthzTestBase { private PolicyCatalogHandler newHandler() { - return newHandler(PolarisPrincipal.ofAllRoles(principalEntity)); + return newHandler( + PolarisPrincipal.of( + principalEntity.getName(), + Map.of( + PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, + principalEntity, + PolarisPrincipal.PRINCIPAL_ROLE_ALL_ATTRIBUTE_KEY, + true), + Set.of())); } private PolicyCatalogHandler newHandler(Set activatedPrincipalRoles) { - return newHandler(PolarisPrincipal.of(principalEntity, activatedPrincipalRoles)); + return newHandler( + PolarisPrincipal.of( + principalEntity.getName(), + Map.of(PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, principalEntity), + activatedPrincipalRoles)); } private PolicyCatalogHandler newHandler(PolarisPrincipal authenticatedPrincipal) { diff --git a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java index 53375902f8..b870c7b2ec 100644 --- a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java +++ b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java @@ -296,7 +296,13 @@ public TestServices build() { .setCreateTimestamp(Instant.now().toEpochMilli()) .setCredentialRotationRequiredState() .build()); - PolarisPrincipal principal = PolarisPrincipal.of(createdPrincipal.getPrincipal(), Set.of()); + + PrincipalEntity principalEntity = createdPrincipal.getPrincipal(); + PolarisPrincipal principal = + PolarisPrincipal.of( + principalEntity.getName(), + Map.of(PolarisPrincipal.PRINCIPAL_ENTITY_ATTRIBUTE_KEY, principalEntity), + Set.of()); SecurityContext securityContext = new SecurityContext() {