diff --git a/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/AuthorizingConnectorCatalogService.java b/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/AuthorizingConnectorCatalogService.java index 6516fee93..df8c24eb2 100644 --- a/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/AuthorizingConnectorCatalogService.java +++ b/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/AuthorizingConnectorCatalogService.java @@ -21,8 +21,6 @@ import com.netflix.metacat.common.dto.Pageable; import com.netflix.metacat.common.dto.Sort; import com.netflix.metacat.common.server.connectors.model.CatalogInfo; -import com.netflix.metacat.common.server.util.AuthorizedCaller; -import com.netflix.metacat.common.server.util.ConnectorAuthorizationUtil; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import lombok.Getter; import lombok.NonNull; @@ -30,7 +28,6 @@ import javax.annotation.Nullable; import java.util.List; -import java.util.Set; /** * Connector decorator that authorizes catalog service calls based on SSO caller context. @@ -46,39 +43,46 @@ public class AuthorizingConnectorCatalogService implements ConnectorCatalogServi private final ConnectorCatalogService delegate; @NonNull - private final Set allowedCallers; + private final ConnectorAuthorizer authorizer; + + @NonNull + private final String authorizedCallers; @NonNull private final String catalogName; + private void authorize(final String operation, final QualifiedName resource) { + authorizer.checkAuthorization(catalogName, authorizedCallers, operation, resource); + } + @Override public void create(final ConnectorRequestContext context, final CatalogInfo resource) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "create", resource.getName()); + authorize("create", resource.getName()); delegate.create(context, resource); } @Override public void update(final ConnectorRequestContext context, final CatalogInfo resource) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "update", resource.getName()); + authorize("update", resource.getName()); delegate.update(context, resource); } @Override public void delete(final ConnectorRequestContext context, final QualifiedName name) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "delete", name); + authorize("delete", name); delegate.delete(context, name); } @Override public CatalogInfo get(final ConnectorRequestContext context, final QualifiedName name) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "get", name); + authorize("get", name); return delegate.get(context, name); } @Override @SuppressFBWarnings public boolean exists(final ConnectorRequestContext context, final QualifiedName name) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "exists", name); + authorize("exists", name); return delegate.exists(context, name); } @@ -90,7 +94,7 @@ public List list( @Nullable final Sort sort, @Nullable final Pageable pageable ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "list", name); + authorize("list", name); return delegate.list(context, name, prefix, sort, pageable); } @@ -102,7 +106,7 @@ public List listNames( @Nullable final Sort sort, @Nullable final Pageable pageable ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "listNames", name); + authorize("listNames", name); return delegate.listNames(context, name, prefix, sort, pageable); } @@ -112,7 +116,7 @@ public void rename( final QualifiedName oldName, final QualifiedName newName ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "rename", oldName); + authorize("rename", oldName); delegate.rename(context, oldName, newName); } diff --git a/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/AuthorizingConnectorDatabaseService.java b/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/AuthorizingConnectorDatabaseService.java index 2f3b105b9..d3e348a5d 100644 --- a/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/AuthorizingConnectorDatabaseService.java +++ b/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/AuthorizingConnectorDatabaseService.java @@ -21,8 +21,6 @@ import com.netflix.metacat.common.dto.Pageable; import com.netflix.metacat.common.dto.Sort; import com.netflix.metacat.common.server.connectors.model.DatabaseInfo; -import com.netflix.metacat.common.server.util.AuthorizedCaller; -import com.netflix.metacat.common.server.util.ConnectorAuthorizationUtil; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import lombok.Getter; import lombok.NonNull; @@ -30,7 +28,6 @@ import javax.annotation.Nullable; import java.util.List; -import java.util.Set; /** * Connector decorator that authorizes database service calls based on SSO caller context. @@ -46,39 +43,46 @@ public class AuthorizingConnectorDatabaseService implements ConnectorDatabaseSer private final ConnectorDatabaseService delegate; @NonNull - private final Set allowedCallers; + private final ConnectorAuthorizer authorizer; + + @NonNull + private final String authorizedCallers; @NonNull private final String catalogName; + private void authorize(final String operation, final QualifiedName resource) { + authorizer.checkAuthorization(catalogName, authorizedCallers, operation, resource); + } + @Override public void create(final ConnectorRequestContext context, final DatabaseInfo resource) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "create", resource.getName()); + authorize("create", resource.getName()); delegate.create(context, resource); } @Override public void update(final ConnectorRequestContext context, final DatabaseInfo resource) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "update", resource.getName()); + authorize("update", resource.getName()); delegate.update(context, resource); } @Override public void delete(final ConnectorRequestContext context, final QualifiedName name) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "delete", name); + authorize("delete", name); delegate.delete(context, name); } @Override public DatabaseInfo get(final ConnectorRequestContext context, final QualifiedName name) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "get", name); + authorize("get", name); return delegate.get(context, name); } @Override @SuppressFBWarnings public boolean exists(final ConnectorRequestContext context, final QualifiedName name) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "exists", name); + authorize("exists", name); return delegate.exists(context, name); } @@ -90,7 +94,7 @@ public List list( @Nullable final Sort sort, @Nullable final Pageable pageable ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "list", name); + authorize("list", name); return delegate.list(context, name, prefix, sort, pageable); } @@ -102,7 +106,7 @@ public List listNames( @Nullable final Sort sort, @Nullable final Pageable pageable ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "listNames", name); + authorize("listNames", name); return delegate.listNames(context, name, prefix, sort, pageable); } @@ -112,7 +116,7 @@ public void rename( final QualifiedName oldName, final QualifiedName newName ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "rename", oldName); + authorize("rename", oldName); delegate.rename(context, oldName, newName); } @@ -121,7 +125,7 @@ public List listViewNames( final ConnectorRequestContext context, final QualifiedName databaseName ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "listViewNames", databaseName); + authorize("listViewNames", databaseName); return delegate.listViewNames(context, databaseName); } diff --git a/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/AuthorizingConnectorPartitionService.java b/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/AuthorizingConnectorPartitionService.java index 224a92995..36b524479 100644 --- a/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/AuthorizingConnectorPartitionService.java +++ b/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/AuthorizingConnectorPartitionService.java @@ -25,8 +25,6 @@ import com.netflix.metacat.common.server.connectors.model.PartitionsSaveRequest; import com.netflix.metacat.common.server.connectors.model.PartitionsSaveResponse; import com.netflix.metacat.common.server.connectors.model.TableInfo; -import com.netflix.metacat.common.server.util.AuthorizedCaller; -import com.netflix.metacat.common.server.util.ConnectorAuthorizationUtil; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import lombok.Getter; import lombok.NonNull; @@ -35,7 +33,6 @@ import javax.annotation.Nullable; import java.util.List; import java.util.Map; -import java.util.Set; /** * Connector decorator that authorizes partition service calls based on SSO caller context. @@ -51,11 +48,18 @@ public class AuthorizingConnectorPartitionService implements ConnectorPartitionS private final ConnectorPartitionService delegate; @NonNull - private final Set allowedCallers; + private final ConnectorAuthorizer authorizer; + + @NonNull + private final String authorizedCallers; @NonNull private final String catalogName; + private void authorize(final String operation, final QualifiedName resource) { + authorizer.checkAuthorization(catalogName, authorizedCallers, operation, resource); + } + @Override public List getPartitions( final ConnectorRequestContext context, @@ -63,7 +67,7 @@ public List getPartitions( final PartitionListRequest partitionsRequest, final TableInfo tableInfo ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "getPartitions", table); + authorize("getPartitions", table); return delegate.getPartitions(context, table, partitionsRequest, tableInfo); } @@ -73,7 +77,7 @@ public PartitionsSaveResponse savePartitions( final QualifiedName table, final PartitionsSaveRequest partitionsSaveRequest ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "savePartitions", table); + authorize("savePartitions", table); return delegate.savePartitions(context, table, partitionsSaveRequest); } @@ -84,7 +88,7 @@ public void deletePartitions( final List partitionNames, final TableInfo tableInfo ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "deletePartitions", tableName); + authorize("deletePartitions", tableName); delegate.deletePartitions(context, tableName, partitionNames, tableInfo); } @@ -94,7 +98,7 @@ public int getPartitionCount( final QualifiedName table, final TableInfo tableInfo ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "getPartitionCount", table); + authorize("getPartitionCount", table); return delegate.getPartitionCount(context, table, tableInfo); } @@ -105,8 +109,7 @@ public Map> getPartitionNames( final boolean prefixSearch ) { // No specific resource name available, check catalog-level access - ConnectorAuthorizationUtil.checkAuthorization( - catalogName, allowedCallers, "getPartitionNames", QualifiedName.ofCatalog(catalogName)); + authorize("getPartitionNames", QualifiedName.ofCatalog(catalogName)); return delegate.getPartitionNames(context, uris, prefixSearch); } @@ -117,7 +120,7 @@ public List getPartitionKeys( final PartitionListRequest partitionsRequest, final TableInfo tableInfo ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "getPartitionKeys", table); + authorize("getPartitionKeys", table); return delegate.getPartitionKeys(context, table, partitionsRequest, tableInfo); } @@ -128,38 +131,38 @@ public List getPartitionUris( final PartitionListRequest partitionsRequest, final TableInfo tableInfo ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "getPartitionUris", table); + authorize("getPartitionUris", table); return delegate.getPartitionUris(context, table, partitionsRequest, tableInfo); } @Override public void create(final ConnectorRequestContext context, final PartitionInfo resource) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "create", resource.getName()); + authorize("create", resource.getName()); delegate.create(context, resource); } @Override public void update(final ConnectorRequestContext context, final PartitionInfo resource) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "update", resource.getName()); + authorize("update", resource.getName()); delegate.update(context, resource); } @Override public void delete(final ConnectorRequestContext context, final QualifiedName name) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "delete", name); + authorize("delete", name); delegate.delete(context, name); } @Override public PartitionInfo get(final ConnectorRequestContext context, final QualifiedName name) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "get", name); + authorize("get", name); return delegate.get(context, name); } @Override @SuppressFBWarnings public boolean exists(final ConnectorRequestContext context, final QualifiedName name) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "exists", name); + authorize("exists", name); return delegate.exists(context, name); } @@ -171,7 +174,7 @@ public List list( @Nullable final Sort sort, @Nullable final Pageable pageable ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "list", name); + authorize("list", name); return delegate.list(context, name, prefix, sort, pageable); } @@ -183,7 +186,7 @@ public List listNames( @Nullable final Sort sort, @Nullable final Pageable pageable ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "listNames", name); + authorize("listNames", name); return delegate.listNames(context, name, prefix, sort, pageable); } @@ -193,7 +196,7 @@ public void rename( final QualifiedName oldName, final QualifiedName newName ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "rename", oldName); + authorize("rename", oldName); delegate.rename(context, oldName, newName); } diff --git a/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/AuthorizingConnectorTableService.java b/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/AuthorizingConnectorTableService.java index d505ae31f..26f0d0aa2 100644 --- a/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/AuthorizingConnectorTableService.java +++ b/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/AuthorizingConnectorTableService.java @@ -21,8 +21,6 @@ import com.netflix.metacat.common.dto.Pageable; import com.netflix.metacat.common.dto.Sort; import com.netflix.metacat.common.server.connectors.model.TableInfo; -import com.netflix.metacat.common.server.util.AuthorizedCaller; -import com.netflix.metacat.common.server.util.ConnectorAuthorizationUtil; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import lombok.Getter; import lombok.NonNull; @@ -31,7 +29,6 @@ import javax.annotation.Nullable; import java.util.List; import java.util.Map; -import java.util.Set; /** * Connector decorator that authorizes calls based on SSO caller context. @@ -47,39 +44,46 @@ public class AuthorizingConnectorTableService implements ConnectorTableService { private final ConnectorTableService delegate; @NonNull - private final Set allowedCallers; + private final ConnectorAuthorizer authorizer; + + @NonNull + private final String authorizedCallers; @NonNull private final String catalogName; + private void authorize(final String operation, final QualifiedName resource) { + authorizer.checkAuthorization(catalogName, authorizedCallers, operation, resource); + } + @Override public void create(final ConnectorRequestContext context, final TableInfo resource) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "create", resource.getName()); + authorize("create", resource.getName()); delegate.create(context, resource); } @Override public void update(final ConnectorRequestContext context, final TableInfo resource) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "update", resource.getName()); + authorize("update", resource.getName()); delegate.update(context, resource); } @Override public void delete(final ConnectorRequestContext context, final QualifiedName name) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "delete", name); + authorize("delete", name); delegate.delete(context, name); } @Override public TableInfo get(final ConnectorRequestContext context, final QualifiedName name) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "get", name); + authorize("get", name); return delegate.get(context, name); } @Override @SuppressFBWarnings public boolean exists(final ConnectorRequestContext context, final QualifiedName name) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "exists", name); + authorize("exists", name); return delegate.exists(context, name); } @@ -91,7 +95,7 @@ public List list( @Nullable final Sort sort, @Nullable final Pageable pageable ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "list", name); + authorize("list", name); return delegate.list(context, name, prefix, sort, pageable); } @@ -103,7 +107,7 @@ public List listNames( @Nullable final Sort sort, @Nullable final Pageable pageable ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "listNames", name); + authorize("listNames", name); return delegate.listNames(context, name, prefix, sort, pageable); } @@ -113,7 +117,7 @@ public void rename( final QualifiedName oldName, final QualifiedName newName ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "rename", oldName); + authorize("rename", oldName); delegate.rename(context, oldName, newName); } @@ -124,8 +128,7 @@ public Map> getTableNames( final boolean prefixSearch ) { // No specific resource name available, check catalog-level access - ConnectorAuthorizationUtil.checkAuthorization( - catalogName, allowedCallers, "getTableNames", QualifiedName.ofCatalog(catalogName)); + authorize("getTableNames", QualifiedName.ofCatalog(catalogName)); return delegate.getTableNames(context, uris, prefixSearch); } @@ -136,7 +139,7 @@ public List getTableNames( final String filter, final Integer limit ) { - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, "getTableNames", name); + authorize("getTableNames", name); return delegate.getTableNames(context, name, filter, limit); } diff --git a/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/ConnectorAuthorizer.java b/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/ConnectorAuthorizer.java new file mode 100644 index 000000000..f4222fd76 --- /dev/null +++ b/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/ConnectorAuthorizer.java @@ -0,0 +1,45 @@ +/* + * + * Copyright 2024 Netflix, Inc. + * + * Licensed 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 com.netflix.metacat.common.server.connectors; + +import com.netflix.metacat.common.QualifiedName; + +/** + * Service Provider Interface for connector-level authorization. + * + * @author abozigian + */ +public interface ConnectorAuthorizer { + + /** + * Determines whether the current caller is authorized to perform the given operation + * against the given catalog. + * + * @param catalogName the name of the catalog being accessed + * @param authorizedCallers the raw, deployment-defined {@code connector.authorized-callers} + * configuration value for the catalog (may be empty) + * @param operation the operation being performed (for logging/decisioning) + * @param resource the resource being accessed (for logging/decisioning) + * @throws com.netflix.metacat.common.server.connectors.exception.CatalogUnauthorizedException + * if the caller is not authorized + */ + void checkAuthorization(String catalogName, + String authorizedCallers, + String operation, + QualifiedName resource); +} diff --git a/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/ConnectorFactoryDecorator.java b/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/ConnectorFactoryDecorator.java index 6350e9cc3..012459796 100644 --- a/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/ConnectorFactoryDecorator.java +++ b/metacat-common-server/src/main/java/com/netflix/metacat/common/server/connectors/ConnectorFactoryDecorator.java @@ -18,16 +18,10 @@ package com.netflix.metacat.common.server.connectors; import com.netflix.metacat.common.server.api.ratelimiter.RateLimiter; -import com.netflix.metacat.common.server.util.AuthorizedCaller; import lombok.Getter; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; -import java.util.Arrays; -import java.util.Collections; -import java.util.Set; -import java.util.stream.Collectors; - /** * A decorator for a connector factory to add additional cross-cutting functionality * to all connector services. @@ -56,7 +50,8 @@ public class ConnectorFactoryDecorator implements ConnectorFactory { private final RateLimiter rateLimiter; private final boolean rateLimiterEnabled; private final boolean authorizationRequired; - private final Set authorizedCallers; + private final ConnectorAuthorizer authorizer; + private final String authorizedCallers; /** * Creates the decorated connector factory that wraps connector services @@ -79,11 +74,22 @@ public ConnectorFactoryDecorator(@NonNull final ConnectorPlugin connectorPlugin, // changes from the Metacat dynamic configuration. this.rateLimiterEnabled = isRateLimiterEnabled(); this.authorizationRequired = isAuthorizationRequired(); - this.authorizedCallers = getAuthorizedCallers(); + this.authorizedCallers = getAuthorizedCallersConfig(); + // If authorization is required but no implementation is available, fail fast rather than silently + // allowing access. if (this.authorizationRequired) { - log.info("Authorization enabled for catalog {} with allowed callers: {}", - connectorContext.getCatalogName(), this.authorizedCallers); + this.authorizer = connectorContext.getApplicationContext() + .getBeanProvider(ConnectorAuthorizer.class).getIfAvailable(); + if (this.authorizer == null) { + throw new IllegalStateException(String.format( + "Catalog '%s' requires authorization (%s=true) but no ConnectorAuthorizer bean is configured", + connectorContext.getCatalogName(), CONFIG_AUTHORIZATION_REQUIRED)); + } + log.info("Authorization enabled for catalog {} using authorizer {}", + connectorContext.getCatalogName(), this.authorizer.getClass().getName()); + } else { + this.authorizer = null; } } @@ -107,7 +113,7 @@ public ConnectorCatalogService getCatalogService() { connectorContext.getConnectorType(), connectorPlugin.getType(), connectorContext.getCatalogName(), connectorContext.getCatalogShardName()); service = new AuthorizingConnectorCatalogService( - service, authorizedCallers, connectorContext.getCatalogName()); + service, authorizer, authorizedCallers, connectorContext.getCatalogName()); } return service; @@ -133,7 +139,7 @@ public ConnectorDatabaseService getDatabaseService() { connectorContext.getConnectorType(), connectorPlugin.getType(), connectorContext.getCatalogName(), connectorContext.getCatalogShardName()); service = new AuthorizingConnectorDatabaseService( - service, authorizedCallers, connectorContext.getCatalogName()); + service, authorizer, authorizedCallers, connectorContext.getCatalogName()); } return service; @@ -159,7 +165,7 @@ public ConnectorTableService getTableService() { connectorContext.getConnectorType(), connectorPlugin.getType(), connectorContext.getCatalogName(), connectorContext.getCatalogShardName()); service = new AuthorizingConnectorTableService( - service, authorizedCallers, connectorContext.getCatalogName()); + service, authorizer, authorizedCallers, connectorContext.getCatalogName()); } return service; @@ -185,7 +191,7 @@ public ConnectorPartitionService getPartitionService() { connectorContext.getConnectorType(), connectorPlugin.getType(), connectorContext.getCatalogName(), connectorContext.getCatalogShardName()); service = new AuthorizingConnectorPartitionService( - service, authorizedCallers, connectorContext.getCatalogName()); + service, authorizer, authorizedCallers, connectorContext.getCatalogName()); } return service; @@ -226,36 +232,11 @@ private boolean isAuthorizationRequired() { ); } - private Set getAuthorizedCallers() { + private String getAuthorizedCallersConfig() { if (connectorContext.getConfiguration() == null) { - return Collections.emptySet(); - } - - final String callers = connectorContext.getConfiguration() - .getOrDefault(CONFIG_AUTHORIZED_CALLERS, ""); - - if (callers.isEmpty()) { - return Collections.emptySet(); + return ""; } - // Split by comma, trim whitespace, filter empty strings, parse each token. - // Plain entry "app" matches on SsoDirectCallerAppName only. - // Compound entry "app:userName" requires both SsoDirectCallerAppName AND userName to match. - // Note: matching is case-sensitive (e.g., "IRC" will not match "irc") - return Arrays.stream(callers.split(",")) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .map(ConnectorFactoryDecorator::parseAuthorizedCaller) - .collect(Collectors.toSet()); - } - - private static AuthorizedCaller parseAuthorizedCaller(final String token) { - final int colon = token.indexOf(':'); - if (colon < 0) { - return new AuthorizedCaller(token, null); - } - final String app = token.substring(0, colon); - final String user = token.substring(colon + 1); - return new AuthorizedCaller(app, user); + return connectorContext.getConfiguration().getOrDefault(CONFIG_AUTHORIZED_CALLERS, ""); } } diff --git a/metacat-common-server/src/main/java/com/netflix/metacat/common/server/util/AuthorizedCaller.java b/metacat-common-server/src/main/java/com/netflix/metacat/common/server/util/AuthorizedCaller.java deleted file mode 100644 index f3573e225..000000000 --- a/metacat-common-server/src/main/java/com/netflix/metacat/common/server/util/AuthorizedCaller.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * - * Copyright 2024 Netflix, Inc. - * - * Licensed 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 com.netflix.metacat.common.server.util; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.ToString; - -import javax.annotation.Nullable; - -/** - * An entry in a connector authorization allow list. - * Matches on SsoDirectCallerAppName, and optionally on userName when {@link #userName} is set. - * - * @author jursetta - */ -@RequiredArgsConstructor -@EqualsAndHashCode -@ToString -@Getter -public final class AuthorizedCaller { - - @NonNull - private final String appName; - - @Nullable - private final String userName; - - /** - * Tests whether the given caller identity satisfies this rule. - * appName must always match exactly. If this rule has a userName, the caller's userName must - * match it exactly; if this rule has no userName, the caller's userName is ignored. - * - * @param callerAppName the SsoDirectCallerAppName from the request - * @param callerUserName the userName from the request (may be null) - * @return true if the caller is allowed by this rule - */ - public boolean matches(final String callerAppName, @Nullable final String callerUserName) { - if (!appName.equals(callerAppName)) { - return false; - } - if (userName == null) { - return true; - } - return userName.equals(callerUserName); - } -} diff --git a/metacat-common-server/src/main/java/com/netflix/metacat/common/server/util/ConnectorAuthorizationUtil.java b/metacat-common-server/src/main/java/com/netflix/metacat/common/server/util/ConnectorAuthorizationUtil.java deleted file mode 100644 index 171b2445d..000000000 --- a/metacat-common-server/src/main/java/com/netflix/metacat/common/server/util/ConnectorAuthorizationUtil.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * - * Copyright 2024 Netflix, Inc. - * - * Licensed 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 com.netflix.metacat.common.server.util; - -import com.netflix.metacat.common.MetacatRequestContext; -import com.netflix.metacat.common.QualifiedName; -import com.netflix.metacat.common.server.connectors.exception.CatalogUnauthorizedException; -import lombok.extern.slf4j.Slf4j; - -import java.util.Set; - -/** - * Utility class for connector authorization checks. - * Provides shared authorization logic for all connector service decorators. - * - * @author jursetta - */ -@Slf4j -public final class ConnectorAuthorizationUtil { - - private ConnectorAuthorizationUtil() { - // Utility class - prevent instantiation - } - - /** - * Checks if the current caller is authorized to access the specified catalog. - * Authorization is based on the SsoDirectCallerAppName from the request context's additionalContext, - * and optionally the request's userName when a rule specifies one. - * - * @param catalogName the name of the catalog being accessed - * @param allowedCallers the set of caller rules allowed to access this catalog - * @param operation the operation being performed (for logging) - * @param resource the resource being accessed (for logging) - * @throws CatalogUnauthorizedException if the caller is not authorized - */ - public static void checkAuthorization( - final String catalogName, - final Set allowedCallers, - final String operation, - final QualifiedName resource - ) { - final MetacatRequestContext context = MetacatContextManager.getContext(); - final String callerApp = context.getAdditionalContext() - .get(MetacatRequestContext.SSO_DIRECT_CALLER_APP_NAME); - final String callerUser = context.getUserName(); - - if (callerApp == null) { - log.warn("Authorization denied for catalog {}: No SsoDirectCallerAppName in request context for {} on {}", - catalogName, operation, resource); - throw new CatalogUnauthorizedException(catalogName); - } - - final boolean allowed = allowedCallers.stream() - .anyMatch(rule -> rule.matches(callerApp, callerUser)); - - if (!allowed) { - log.warn("Authorization denied for catalog {}: " - + "SsoDirectCallerAppName '{}' (userName '{}') not in allowed list for {} on {}", - catalogName, callerApp, callerUser, operation, resource); - throw new CatalogUnauthorizedException(catalogName, callerApp); - } - - log.debug("Authorization granted for catalog {}: SsoDirectCallerAppName '{}' (userName '{}') for {} on {}", - catalogName, callerApp, callerUser, operation, resource); - } -} diff --git a/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/AuthorizingConnectorCatalogServiceSpec.groovy b/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/AuthorizingConnectorCatalogServiceSpec.groovy index a3f5556b3..3c5babc8e 100644 --- a/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/AuthorizingConnectorCatalogServiceSpec.groovy +++ b/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/AuthorizingConnectorCatalogServiceSpec.groovy @@ -17,207 +17,113 @@ */ package com.netflix.metacat.common.server.connectors -import com.netflix.metacat.common.MetacatRequestContext import com.netflix.metacat.common.QualifiedName import com.netflix.metacat.common.server.connectors.exception.CatalogUnauthorizedException import com.netflix.metacat.common.server.connectors.model.CatalogInfo -import com.netflix.metacat.common.server.util.AuthorizedCaller -import com.netflix.metacat.common.server.util.MetacatContextManager import spock.lang.Specification class AuthorizingConnectorCatalogServiceSpec extends Specification { def delegate + def authorizer def context def resource def name def newName def catalogName + def authorizedCallers def service def setup() { delegate = Mock(ConnectorCatalogService) + authorizer = Mock(ConnectorAuthorizer) context = Mock(ConnectorRequestContext) - catalogName = "ads" + catalogName = "catalog1" + authorizedCallers = "caller-a,caller-b" name = QualifiedName.ofCatalog(catalogName) newName = QualifiedName.ofCatalog("ads2") resource = new CatalogInfo(name: name) - // Reset context before each test - MetacatContextManager.removeContext() + service = new AuthorizingConnectorCatalogService(delegate, authorizer, authorizedCallers, catalogName) } - def cleanup() { - MetacatContextManager.removeContext() - } - - private MetacatRequestContext buildContextWithCaller(String caller) { - def ctx = MetacatRequestContext.builder().build() - ctx.getAdditionalContext().put(MetacatRequestContext.SSO_DIRECT_CALLER_APP_NAME, caller) - return ctx - } - - def "create - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null), new AuthorizedCaller("irc-server", null)] as Set - service = new AuthorizingConnectorCatalogService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) - + def "delegates each operation when the authorizer permits the caller"() { when: service.create(context, resource) - then: 1 * delegate.create(context, resource) - } - - def "create - unauthorized caller throws exception"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null), new AuthorizedCaller("irc-server", null)] as Set - service = new AuthorizingConnectorCatalogService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("unauthorized-app")) - - when: - service.create(context, resource) - - then: - thrown(CatalogUnauthorizedException) - 0 * delegate.create(_, _) - } - - def "create - missing SsoDirectCallerAppName throws exception"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null), new AuthorizedCaller("irc-server", null)] as Set - service = new AuthorizingConnectorCatalogService(delegate, allowedCallers, catalogName) - - // No SsoDirectCallerAppName in additionalContext - def ctx = MetacatRequestContext.builder().build() - MetacatContextManager.setContext(ctx) - - when: - service.create(context, resource) - - then: - thrown(CatalogUnauthorizedException) - 0 * delegate.create(_, _) - } - - def "update - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorCatalogService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.update(context, resource) - then: 1 * delegate.update(context, resource) - } - - def "update - unauthorized caller throws exception"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorCatalogService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("spark")) - - when: - service.update(context, resource) - - then: - thrown(CatalogUnauthorizedException) - 0 * delegate.update(_, _) - } - - def "delete - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorCatalogService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.delete(context, name) - then: 1 * delegate.delete(context, name) - } - - def "get - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorCatalogService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.get(context, name) - then: 1 * delegate.get(context, name) - } - - def "exists - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorCatalogService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.exists(context, name) - then: 1 * delegate.exists(context, name) - } - - def "list - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorCatalogService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.list(context, name, null, null, null) - then: 1 * delegate.list(context, name, null, null, null) - } - - def "listNames - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorCatalogService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.listNames(context, name, null, null, null) - then: 1 * delegate.listNames(context, name, null, null, null) + + when: + service.rename(context, name, newName) + then: + 1 * delegate.rename(context, name, newName) } - def "rename - authorized caller succeeds"() { + def "throws and does not delegate when the authorizer denies the caller"() { given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorCatalogService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) + authorizer.checkAuthorization(catalogName, authorizedCallers, _, _) >> { throw new CatalogUnauthorizedException(catalogName) } when: - service.rename(context, name, newName) + service.create(context, resource) + then: + thrown(CatalogUnauthorizedException) + 0 * delegate.create(_, _) + + when: + service.get(context, name) + then: + thrown(CatalogUnauthorizedException) + 0 * delegate.get(_, _) + } + + def "passes the operation and resource through to the authorizer"() { + when: + service.get(context, name) then: - 1 * delegate.rename(context, name, newName) + 1 * authorizer.checkAuthorization(catalogName, authorizedCallers, "get", name) + 1 * delegate.get(context, name) } - def "exception message contains caller name when unauthorized"() { + def "unauthorized exception message identifies the catalog"() { given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorCatalogService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("bad-actor")) + authorizer.checkAuthorization(_, _, _, _) >> { throw new CatalogUnauthorizedException("catalog1") } when: service.get(context, name) then: def ex = thrown(CatalogUnauthorizedException) - ex.message.contains("bad-actor") - ex.message.contains("ads") + ex.message.contains("catalog1") } } diff --git a/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/AuthorizingConnectorDatabaseServiceSpec.groovy b/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/AuthorizingConnectorDatabaseServiceSpec.groovy index 9f214ecf7..f0e34187e 100644 --- a/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/AuthorizingConnectorDatabaseServiceSpec.groovy +++ b/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/AuthorizingConnectorDatabaseServiceSpec.groovy @@ -17,219 +17,106 @@ */ package com.netflix.metacat.common.server.connectors -import com.netflix.metacat.common.MetacatRequestContext import com.netflix.metacat.common.QualifiedName import com.netflix.metacat.common.server.connectors.exception.CatalogUnauthorizedException import com.netflix.metacat.common.server.connectors.model.DatabaseInfo -import com.netflix.metacat.common.server.util.AuthorizedCaller -import com.netflix.metacat.common.server.util.MetacatContextManager import spock.lang.Specification class AuthorizingConnectorDatabaseServiceSpec extends Specification { def delegate + def authorizer def context def resource def name def newName def catalogName + def authorizedCallers def service def setup() { delegate = Mock(ConnectorDatabaseService) + authorizer = Mock(ConnectorAuthorizer) context = Mock(ConnectorRequestContext) - catalogName = "ads" + catalogName = "catalog1" + authorizedCallers = "caller-a,caller-b" name = QualifiedName.ofDatabase(catalogName, "db") newName = QualifiedName.ofDatabase(catalogName, "db2") resource = new DatabaseInfo(name: name) - // Reset context before each test - MetacatContextManager.removeContext() + service = new AuthorizingConnectorDatabaseService(delegate, authorizer, authorizedCallers, catalogName) } - def cleanup() { - MetacatContextManager.removeContext() - } - - private MetacatRequestContext buildContextWithCaller(String caller) { - def ctx = MetacatRequestContext.builder().build() - ctx.getAdditionalContext().put(MetacatRequestContext.SSO_DIRECT_CALLER_APP_NAME, caller) - return ctx - } - - def "create - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null), new AuthorizedCaller("irc-server", null)] as Set - service = new AuthorizingConnectorDatabaseService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) - + def "delegates each operation when the authorizer permits the caller"() { when: service.create(context, resource) - then: 1 * delegate.create(context, resource) - } - - def "create - unauthorized caller throws exception"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null), new AuthorizedCaller("irc-server", null)] as Set - service = new AuthorizingConnectorDatabaseService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("unauthorized-app")) - - when: - service.create(context, resource) - - then: - thrown(CatalogUnauthorizedException) - 0 * delegate.create(_, _) - } - - def "create - missing SsoDirectCallerAppName throws exception"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null), new AuthorizedCaller("irc-server", null)] as Set - service = new AuthorizingConnectorDatabaseService(delegate, allowedCallers, catalogName) - - def ctx = MetacatRequestContext.builder().build() - MetacatContextManager.setContext(ctx) - - when: - service.create(context, resource) - - then: - thrown(CatalogUnauthorizedException) - 0 * delegate.create(_, _) - } - - def "update - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorDatabaseService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.update(context, resource) - then: 1 * delegate.update(context, resource) - } - - def "update - unauthorized caller throws exception"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorDatabaseService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("spark")) - - when: - service.update(context, resource) - - then: - thrown(CatalogUnauthorizedException) - 0 * delegate.update(_, _) - } - - def "delete - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorDatabaseService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.delete(context, name) - then: 1 * delegate.delete(context, name) - } - - def "get - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorDatabaseService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.get(context, name) - then: 1 * delegate.get(context, name) - } - - def "exists - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorDatabaseService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.exists(context, name) - then: 1 * delegate.exists(context, name) - } - - def "list - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorDatabaseService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.list(context, name, null, null, null) - then: 1 * delegate.list(context, name, null, null, null) - } - - def "listNames - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorDatabaseService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.listNames(context, name, null, null, null) - then: 1 * delegate.listNames(context, name, null, null, null) - } - - def "rename - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorDatabaseService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.rename(context, name, newName) - then: 1 * delegate.rename(context, name, newName) - } - - def "listViewNames - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorDatabaseService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.listViewNames(context, name) - then: 1 * delegate.listViewNames(context, name) } - def "exception message contains caller name when unauthorized"() { + def "throws and does not delegate when the authorizer denies the caller"() { given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorDatabaseService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("bad-actor")) + authorizer.checkAuthorization(catalogName, authorizedCallers, _, _) >> { throw new CatalogUnauthorizedException(catalogName) } + + when: + service.create(context, resource) + then: + thrown(CatalogUnauthorizedException) + 0 * delegate.create(_, _) + + when: + service.get(context, name) + then: + thrown(CatalogUnauthorizedException) + 0 * delegate.get(_, _) + } + def "passes the operation and resource through to the authorizer"() { when: service.get(context, name) then: - def ex = thrown(CatalogUnauthorizedException) - ex.message.contains("bad-actor") - ex.message.contains("ads") + 1 * authorizer.checkAuthorization(catalogName, authorizedCallers, "get", name) + 1 * delegate.get(context, name) } } diff --git a/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/AuthorizingConnectorPartitionServiceSpec.groovy b/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/AuthorizingConnectorPartitionServiceSpec.groovy index b0e3eb999..4c1aede12 100644 --- a/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/AuthorizingConnectorPartitionServiceSpec.groovy +++ b/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/AuthorizingConnectorPartitionServiceSpec.groovy @@ -17,342 +17,137 @@ */ package com.netflix.metacat.common.server.connectors -import com.netflix.metacat.common.MetacatRequestContext import com.netflix.metacat.common.QualifiedName import com.netflix.metacat.common.server.connectors.exception.CatalogUnauthorizedException import com.netflix.metacat.common.server.connectors.model.PartitionInfo -import com.netflix.metacat.common.server.connectors.model.PartitionListRequest -import com.netflix.metacat.common.server.connectors.model.PartitionsSaveRequest -import com.netflix.metacat.common.server.connectors.model.TableInfo -import com.netflix.metacat.common.server.util.AuthorizedCaller -import com.netflix.metacat.common.server.util.MetacatContextManager import spock.lang.Specification class AuthorizingConnectorPartitionServiceSpec extends Specification { def delegate + def authorizer def context def resource - def tableName - def partitionName + def table + def name def newName def catalogName + def authorizedCallers def service - def tableInfo - def partitionListRequest - def partitionsSaveRequest def setup() { delegate = Mock(ConnectorPartitionService) + authorizer = Mock(ConnectorAuthorizer) context = Mock(ConnectorRequestContext) - tableInfo = Mock(TableInfo) - partitionListRequest = Mock(PartitionListRequest) - partitionsSaveRequest = Mock(PartitionsSaveRequest) - catalogName = "ads" - tableName = QualifiedName.ofTable(catalogName, "db", "table") - partitionName = QualifiedName.ofPartition(catalogName, "db", "table", "dateint=20240101") - newName = QualifiedName.ofPartition(catalogName, "db", "table", "dateint=20240102") - resource = new PartitionInfo(name: partitionName) + catalogName = "catalog1" + authorizedCallers = "caller-a,caller-b" + table = QualifiedName.ofTable(catalogName, "db", "tbl") + name = QualifiedName.ofPartition(catalogName, "db", "tbl", "dateint=1") + newName = QualifiedName.ofPartition(catalogName, "db", "tbl", "dateint=2") + resource = new PartitionInfo(name: name) - // Reset context before each test - MetacatContextManager.removeContext() + service = new AuthorizingConnectorPartitionService(delegate, authorizer, authorizedCallers, catalogName) } - def cleanup() { - MetacatContextManager.removeContext() - } - - private MetacatRequestContext buildContextWithCaller(String caller) { - def ctx = MetacatRequestContext.builder().build() - ctx.getAdditionalContext().put(MetacatRequestContext.SSO_DIRECT_CALLER_APP_NAME, caller) - return ctx - } - - def "create - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null), new AuthorizedCaller("irc-server", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) - + def "delegates each operation when the authorizer permits the caller"() { when: - service.create(context, resource) - + service.getPartitions(context, table, null, null) then: - 1 * delegate.create(context, resource) - } - - def "create - unauthorized caller throws exception"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null), new AuthorizedCaller("irc-server", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("unauthorized-app")) + 1 * delegate.getPartitions(context, table, null, null) when: - service.create(context, resource) - + service.savePartitions(context, table, null) then: - thrown(CatalogUnauthorizedException) - 0 * delegate.create(_, _) - } - - def "create - missing SsoDirectCallerAppName throws exception"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null), new AuthorizedCaller("irc-server", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - - // No SsoDirectCallerAppName in additionalContext - def ctx = MetacatRequestContext.builder().build() - MetacatContextManager.setContext(ctx) + 1 * delegate.savePartitions(context, table, null) when: - service.create(context, resource) - + service.deletePartitions(context, table, ["dateint=1"], null) then: - thrown(CatalogUnauthorizedException) - 0 * delegate.create(_, _) - } - - def "update - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) + 1 * delegate.deletePartitions(context, table, ["dateint=1"], null) when: - service.update(context, resource) - + service.getPartitionKeys(context, table, null, null) then: - 1 * delegate.update(context, resource) - } - - def "delete - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) + 1 * delegate.getPartitionKeys(context, table, null, null) when: - service.delete(context, partitionName) - + service.getPartitionUris(context, table, null, null) then: - 1 * delegate.delete(context, partitionName) - } - - def "get - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) + 1 * delegate.getPartitionUris(context, table, null, null) when: - service.get(context, partitionName) - + service.create(context, resource) then: - 1 * delegate.get(context, partitionName) - } - - def "exists - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) + 1 * delegate.create(context, resource) when: - service.exists(context, partitionName) - + service.update(context, resource) then: - 1 * delegate.exists(context, partitionName) - } - - def "list - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) + 1 * delegate.update(context, resource) when: - service.list(context, tableName, null, null, null) - + service.delete(context, name) then: - 1 * delegate.list(context, tableName, null, null, null) - } - - def "listNames - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) + 1 * delegate.delete(context, name) when: - service.listNames(context, tableName, null, null, null) - + service.get(context, name) then: - 1 * delegate.listNames(context, tableName, null, null, null) - } - - def "rename - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) + 1 * delegate.get(context, name) when: - service.rename(context, partitionName, newName) - + service.exists(context, name) then: - 1 * delegate.rename(context, partitionName, newName) - } - - def "getPartitions - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) + 1 * delegate.exists(context, name) when: - service.getPartitions(context, tableName, partitionListRequest, tableInfo) - + service.list(context, name, null, null, null) then: - 1 * delegate.getPartitions(context, tableName, partitionListRequest, tableInfo) - } - - def "getPartitions - unauthorized caller throws exception"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("spark")) + 1 * delegate.list(context, name, null, null, null) when: - service.getPartitions(context, tableName, partitionListRequest, tableInfo) - + service.listNames(context, name, null, null, null) then: - thrown(CatalogUnauthorizedException) - 0 * delegate.getPartitions(_, _, _, _) - } - - def "savePartitions - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) + 1 * delegate.listNames(context, name, null, null, null) when: - service.savePartitions(context, tableName, partitionsSaveRequest) - + service.rename(context, name, newName) then: - 1 * delegate.savePartitions(context, tableName, partitionsSaveRequest) + 1 * delegate.rename(context, name, newName) } - def "savePartitions - unauthorized caller throws exception"() { + def "throws and does not delegate when the authorizer denies the caller"() { given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("hive")) + authorizer.checkAuthorization(catalogName, authorizedCallers, _, _) >> { throw new CatalogUnauthorizedException(catalogName) } when: - service.savePartitions(context, tableName, partitionsSaveRequest) - + service.savePartitions(context, table, null) then: thrown(CatalogUnauthorizedException) 0 * delegate.savePartitions(_, _, _) - } - - def "deletePartitions - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - def partitionNames = ["dateint=20240101"] - MetacatContextManager.setContext(buildContextWithCaller("irc")) - - when: - service.deletePartitions(context, tableName, partitionNames, tableInfo) - - then: - 1 * delegate.deletePartitions(context, tableName, partitionNames, tableInfo) - } - - def "getPartitionCount - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) - - when: - service.getPartitionCount(context, tableName, tableInfo) - - then: - 1 * delegate.getPartitionCount(context, tableName, tableInfo) - } - - def "getPartitionNames - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - def uris = ["s3://bucket/path"] - MetacatContextManager.setContext(buildContextWithCaller("irc")) - - when: - service.getPartitionNames(context, uris, false) - - then: - 1 * delegate.getPartitionNames(context, uris, false) - } - - def "getPartitionKeys - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: - service.getPartitionKeys(context, tableName, partitionListRequest, tableInfo) - + service.get(context, name) then: - 1 * delegate.getPartitionKeys(context, tableName, partitionListRequest, tableInfo) - } - - def "getPartitionUris - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) - - when: - service.getPartitionUris(context, tableName, partitionListRequest, tableInfo) - - then: - 1 * delegate.getPartitionUris(context, tableName, partitionListRequest, tableInfo) + thrown(CatalogUnauthorizedException) + 0 * delegate.get(_, _) } - def "exception message contains caller name when unauthorized"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("bad-actor")) - + def "passes the operation and resource through to the authorizer"() { when: - service.get(context, partitionName) + service.getPartitions(context, table, null, null) then: - def ex = thrown(CatalogUnauthorizedException) - ex.message.contains("bad-actor") - ex.message.contains("ads") + 1 * authorizer.checkAuthorization(catalogName, authorizedCallers, "getPartitions", table) + 1 * delegate.getPartitions(context, table, null, null) } - def "exception message indicates missing caller when no SsoDirectCallerAppName"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorPartitionService(delegate, allowedCallers, catalogName) - - def ctx = MetacatRequestContext.builder().build() - MetacatContextManager.setContext(ctx) - + def "checks catalog-level access for uri-based getPartitionNames"() { when: - service.get(context, partitionName) + service.getPartitionNames(context, ["s3://uri"], false) then: - def ex = thrown(CatalogUnauthorizedException) - ex.message.contains("authenticated caller") - ex.message.contains("ads") + 1 * authorizer.checkAuthorization(catalogName, authorizedCallers, "getPartitionNames", QualifiedName.ofCatalog(catalogName)) + 1 * delegate.getPartitionNames(context, ["s3://uri"], false) } } diff --git a/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/AuthorizingConnectorTableServiceSpec.groovy b/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/AuthorizingConnectorTableServiceSpec.groovy index f2d1fe4e4..7a3274bc9 100644 --- a/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/AuthorizingConnectorTableServiceSpec.groovy +++ b/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/AuthorizingConnectorTableServiceSpec.groovy @@ -17,320 +17,111 @@ */ package com.netflix.metacat.common.server.connectors -import com.netflix.metacat.common.MetacatRequestContext import com.netflix.metacat.common.QualifiedName import com.netflix.metacat.common.server.connectors.exception.CatalogUnauthorizedException import com.netflix.metacat.common.server.connectors.model.TableInfo -import com.netflix.metacat.common.server.util.AuthorizedCaller -import com.netflix.metacat.common.server.util.MetacatContextManager import spock.lang.Specification class AuthorizingConnectorTableServiceSpec extends Specification { def delegate + def authorizer def context def resource def name def newName def catalogName + def authorizedCallers def service - class Success extends RuntimeException {} - def setup() { delegate = Mock(ConnectorTableService) + authorizer = Mock(ConnectorAuthorizer) context = Mock(ConnectorRequestContext) - catalogName = "ads" - name = QualifiedName.ofTable(catalogName, "d", "t") - newName = QualifiedName.ofTable(catalogName, "d", "t2") + catalogName = "catalog1" + authorizedCallers = "caller-a,caller-b" + name = QualifiedName.ofTable(catalogName, "db", "tbl") + newName = QualifiedName.ofTable(catalogName, "db", "tbl2") resource = new TableInfo(name: name) - // Reset context before each test - MetacatContextManager.removeContext() - } - - def cleanup() { - MetacatContextManager.removeContext() - } - - private MetacatRequestContext buildContextWithCaller(String caller) { - def ctx = MetacatRequestContext.builder().build() - ctx.getAdditionalContext().put(MetacatRequestContext.SSO_DIRECT_CALLER_APP_NAME, caller) - return ctx + service = new AuthorizingConnectorTableService(delegate, authorizer, authorizedCallers, catalogName) } - def "create - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null), new AuthorizedCaller("irc-server", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) - + def "delegates each operation when the authorizer permits the caller"() { when: service.create(context, resource) - then: 1 * delegate.create(context, resource) - } - - def "create - unauthorized caller throws exception"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null), new AuthorizedCaller("irc-server", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("unauthorized-app")) - - when: - service.create(context, resource) - - then: - thrown(CatalogUnauthorizedException) - 0 * delegate.create(_, _) - } - - def "create - missing SsoDirectCallerAppName throws exception"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null), new AuthorizedCaller("irc-server", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - - // No SsoDirectCallerAppName in additionalContext - def ctx = MetacatRequestContext.builder().build() - MetacatContextManager.setContext(ctx) - - when: - service.create(context, resource) - - then: - thrown(CatalogUnauthorizedException) - 0 * delegate.create(_, _) - } - - def "update - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.update(context, resource) - then: 1 * delegate.update(context, resource) - } - - def "update - unauthorized caller throws exception"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("spark")) - - when: - service.update(context, resource) - - then: - thrown(CatalogUnauthorizedException) - 0 * delegate.update(_, _) - } - - def "delete - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.delete(context, name) - then: 1 * delegate.delete(context, name) - } - - def "delete - unauthorized caller throws exception"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("hive")) - - when: - service.delete(context, name) - - then: - thrown(CatalogUnauthorizedException) - 0 * delegate.delete(_, _) - } - - def "get - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.get(context, name) - then: 1 * delegate.get(context, name) - } - - def "get - unauthorized caller throws exception"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("presto")) - - when: - service.get(context, name) - - then: - thrown(CatalogUnauthorizedException) - 0 * delegate.get(_, _) - } - - def "exists - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.exists(context, name) - then: 1 * delegate.exists(context, name) - } - - def "list - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.list(context, name, null, null, null) - then: 1 * delegate.list(context, name, null, null, null) - } - - def "listNames - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.listNames(context, name, null, null, null) - then: 1 * delegate.listNames(context, name, null, null, null) - } - - def "rename - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.rename(context, name, newName) - then: 1 * delegate.rename(context, name, newName) - } - - def "getTableNames with filter - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) when: service.getTableNames(context, name, "filter", 10) - then: 1 * delegate.getTableNames(context, name, "filter", 10) - } - - def "getTableNames with URIs - authorized caller succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) - - when: - service.getTableNames(context, ["s3://bucket/path"], false) - - then: - 1 * delegate.getTableNames(context, ["s3://bucket/path"], false) - } - - def "exception message contains caller name when unauthorized"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("bad-actor")) when: - service.get(context, name) - + service.getTableNames(context, ["s3://uri"], false) then: - def ex = thrown(CatalogUnauthorizedException) - ex.message.contains("bad-actor") - ex.message.contains("ads") + 1 * delegate.getTableNames(context, ["s3://uri"], false) } - def "exception message indicates missing caller when no SsoDirectCallerAppName"() { + def "throws and does not delegate when the authorizer denies the caller"() { given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - - def ctx = MetacatRequestContext.builder().build() - MetacatContextManager.setContext(ctx) + authorizer.checkAuthorization(catalogName, authorizedCallers, _, _) >> { throw new CatalogUnauthorizedException(catalogName) } when: - service.get(context, name) - + service.create(context, resource) then: - def ex = thrown(CatalogUnauthorizedException) - ex.message.contains("authenticated caller") - ex.message.contains("ads") - } - - def "caller matching is case sensitive - uppercase caller rejected"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set // lowercase - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("IRC")) // uppercase + thrown(CatalogUnauthorizedException) + 0 * delegate.create(_, _) when: service.get(context, name) - then: thrown(CatalogUnauthorizedException) 0 * delegate.get(_, _) } - def "caller matching is case sensitive - lowercase caller accepted"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null)] as Set // lowercase - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("irc")) // lowercase matches - + def "checks catalog-level access for uri-based getTableNames"() { when: - service.get(context, name) + service.getTableNames(context, ["s3://uri"], false) then: - 1 * delegate.get(context, name) - } - - def "multiple allowed callers - second caller in list succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("irc", null), new AuthorizedCaller("irc-server", null), new AuthorizedCaller("spark", null)] as Set - service = new AuthorizingConnectorTableService(delegate, allowedCallers, catalogName) - MetacatContextManager.setContext(buildContextWithCaller("spark")) - - when: - service.get(context, name) - - then: - 1 * delegate.get(context, name) + 1 * authorizer.checkAuthorization(catalogName, authorizedCallers, "getTableNames", QualifiedName.ofCatalog(catalogName)) + 1 * delegate.getTableNames(context, ["s3://uri"], false) } } diff --git a/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/ConnectorFactoryDecoratorSpec.groovy b/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/ConnectorFactoryDecoratorSpec.groovy index e1bb84432..515968372 100644 --- a/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/ConnectorFactoryDecoratorSpec.groovy +++ b/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/connectors/ConnectorFactoryDecoratorSpec.groovy @@ -18,6 +18,7 @@ package com.netflix.metacat.common.server.connectors import com.netflix.metacat.common.server.api.ratelimiter.RateLimiter +import org.springframework.beans.factory.ObjectProvider import org.springframework.context.ApplicationContext import spock.lang.Specification import spock.lang.Unroll @@ -28,6 +29,10 @@ class ConnectorFactoryDecoratorSpec extends Specification { ConnectorContext connectorContext RateLimiter rateLimiter ApplicationContext applicationContext + ObjectProvider authorizerProvider + ConnectorAuthorizer authorizer + // The authorizer the context will hand back; null simulates "no implementation configured". + def availableAuthorizer ConnectorCatalogService catalogService ConnectorDatabaseService databaseService ConnectorTableService tableService @@ -41,6 +46,8 @@ class ConnectorFactoryDecoratorSpec extends Specification { connectorContext = Mock(ConnectorContext) rateLimiter = Mock(RateLimiter) applicationContext = Mock(ApplicationContext) + authorizerProvider = Mock(ObjectProvider) + authorizer = Mock(ConnectorAuthorizer) catalogService = Mock(ConnectorCatalogService) databaseService = Mock(ConnectorDatabaseService) @@ -51,6 +58,10 @@ class ConnectorFactoryDecoratorSpec extends Specification { connectorContext.getApplicationContext() >> applicationContext applicationContext.getBean(RateLimiter) >> rateLimiter + availableAuthorizer = authorizer + applicationContext.getBeanProvider(ConnectorAuthorizer) >> authorizerProvider + authorizerProvider.getIfAvailable() >> { availableAuthorizer } + delegate.getCatalogService() >> catalogService delegate.getDatabaseService() >> databaseService delegate.getTableService() >> tableService @@ -132,9 +143,9 @@ class ConnectorFactoryDecoratorSpec extends Specification { connectorContext.getConfiguration() >> [ "connector.rate-limiter-exempted": "false", "connector.authorization-required": "true", - "connector.authorized-callers": "irc,irc-server" + "connector.authorized-callers": "caller-a,caller-b" ] - connectorContext.getCatalogName() >> "ads" + connectorContext.getCatalogName() >> "catalog1" factory = new ConnectorFactoryDecorator(connectorPlugin, connectorContext) when: @@ -199,9 +210,9 @@ class ConnectorFactoryDecoratorSpec extends Specification { connectorContext.getConfiguration() >> [ "connector.rate-limiter-exempted": "true", "connector.authorization-required": "true", - "connector.authorized-callers": "irc" + "connector.authorized-callers": "caller-a" ] - connectorContext.getCatalogName() >> "ads" + connectorContext.getCatalogName() >> "catalog1" factory = new ConnectorFactoryDecorator(connectorPlugin, connectorContext) when: @@ -250,9 +261,9 @@ class ConnectorFactoryDecoratorSpec extends Specification { connectorContext.getConfiguration() >> [ "connector.rate-limiter-exempted": "false", "connector.authorization-required": "true", - "connector.authorized-callers": "irc" + "connector.authorized-callers": "caller-a" ] - connectorContext.getCatalogName() >> "ads" + connectorContext.getCatalogName() >> "catalog1" factory = new ConnectorFactoryDecorator(connectorPlugin, connectorContext) when: @@ -266,20 +277,20 @@ class ConnectorFactoryDecoratorSpec extends Specification { partitionSvc instanceof AuthorizingConnectorPartitionService } - def "empty authorized callers list"() { + def "empty authorized callers config still applies authorization"() { given: connectorContext.getConfiguration() >> [ "connector.rate-limiter-exempted": "true", "connector.authorization-required": "true", "connector.authorized-callers": "" ] - connectorContext.getCatalogName() >> "ads" + connectorContext.getCatalogName() >> "catalog1" factory = new ConnectorFactoryDecorator(connectorPlugin, connectorContext) when: def tblSvc = factory.getTableService() - then: "authorization still applied with empty callers set" + then: "authorization still applied; interpretation of the empty value is up to the authorizer" tblSvc instanceof AuthorizingConnectorTableService } @@ -298,55 +309,21 @@ class ConnectorFactoryDecoratorSpec extends Specification { tblSvc == tableService } - def "authorized callers list trims whitespace"() { - given: + def "fails fast when authorization is required but no ConnectorAuthorizer bean is configured"() { + given: "no authorizer implementation is available in the context" + availableAuthorizer = null connectorContext.getConfiguration() >> [ - "connector.rate-limiter-exempted": "true", "connector.authorization-required": "true", - "connector.authorized-callers": " irc , irc-server ,spark" + "connector.authorized-callers": "caller-a" ] - connectorContext.getCatalogName() >> "ads" - factory = new ConnectorFactoryDecorator(connectorPlugin, connectorContext) + connectorContext.getCatalogName() >> "catalog1" when: - def tblSvc = factory.getTableService() + new ConnectorFactoryDecorator(connectorPlugin, connectorContext) - then: "authorization is applied" - tblSvc instanceof AuthorizingConnectorTableService - // The whitespace should be trimmed, so "irc", "irc-server", and "spark" should all be valid - } - - def "authorized callers list accepts compound app:userName syntax"() { - given: - connectorContext.getConfiguration() >> [ - "connector.rate-limiter-exempted": "true", - "connector.authorization-required": "true", - "connector.authorized-callers": "irc,hadoop:idm-worker" - ] - connectorContext.getCatalogName() >> "ads" - factory = new ConnectorFactoryDecorator(connectorPlugin, connectorContext) - - when: - def tblSvc = factory.getTableService() - - then: "authorization is applied; compound entry parses without error" - tblSvc instanceof AuthorizingConnectorTableService - } - - def "authorized callers with only whitespace entries are filtered"() { - given: - connectorContext.getConfiguration() >> [ - "connector.rate-limiter-exempted": "true", - "connector.authorization-required": "true", - "connector.authorized-callers": "irc, ,," - ] - connectorContext.getCatalogName() >> "ads" - factory = new ConnectorFactoryDecorator(connectorPlugin, connectorContext) - - when: - def tblSvc = factory.getTableService() - - then: "authorization is applied with only non-empty callers" - tblSvc instanceof AuthorizingConnectorTableService + then: + def ex = thrown(IllegalStateException) + ex.message.contains("catalog1") + ex.message.contains("ConnectorAuthorizer") } } diff --git a/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/util/ConnectorAuthorizationUtilSpec.groovy b/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/util/ConnectorAuthorizationUtilSpec.groovy deleted file mode 100644 index de2e0ea9f..000000000 --- a/metacat-common-server/src/test/groovy/com/netflix/metacat/common/server/util/ConnectorAuthorizationUtilSpec.groovy +++ /dev/null @@ -1,223 +0,0 @@ -/* - * - * Copyright 2024 Netflix, Inc. - * - * Licensed 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 com.netflix.metacat.common.server.util - -import com.netflix.metacat.common.MetacatRequestContext -import com.netflix.metacat.common.QualifiedName -import com.netflix.metacat.common.server.connectors.exception.CatalogUnauthorizedException -import spock.lang.Specification - -class ConnectorAuthorizationUtilSpec extends Specification { - - def catalogName = "test-catalog" - def resource = QualifiedName.ofTable(catalogName, "db", "table") - def operation = "get" - - def setup() { - MetacatContextManager.removeContext() - } - - def cleanup() { - MetacatContextManager.removeContext() - } - - def "checkAuthorization - authorized SsoDirectCallerAppName succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("bdpauthzservice", null), new AuthorizedCaller("irc-server", null)] as Set - def ctx = MetacatRequestContext.builder().build() - ctx.getAdditionalContext().put(MetacatRequestContext.SSO_DIRECT_CALLER_APP_NAME, "bdpauthzservice") - MetacatContextManager.setContext(ctx) - - when: - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, operation, resource) - - then: - noExceptionThrown() - } - - def "checkAuthorization - unauthorized SsoDirectCallerAppName throws exception"() { - given: - def allowedCallers = [new AuthorizedCaller("bdpauthzservice", null), new AuthorizedCaller("irc-server", null)] as Set - def ctx = MetacatRequestContext.builder().build() - ctx.getAdditionalContext().put(MetacatRequestContext.SSO_DIRECT_CALLER_APP_NAME, "unauthorized-app") - MetacatContextManager.setContext(ctx) - - when: - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, operation, resource) - - then: - def ex = thrown(CatalogUnauthorizedException) - ex.message.contains("unauthorized-app") - ex.message.contains(catalogName) - } - - def "checkAuthorization - missing SsoDirectCallerAppName throws exception"() { - given: - def allowedCallers = [new AuthorizedCaller("bdpauthzservice", null)] as Set - def ctx = MetacatRequestContext.builder().build() - // No SsoDirectCallerAppName in additionalContext - MetacatContextManager.setContext(ctx) - - when: - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, operation, resource) - - then: - def ex = thrown(CatalogUnauthorizedException) - ex.message.contains("authenticated caller") - ex.message.contains(catalogName) - } - - def "checkAuthorization - empty allowed callers set denies all"() { - given: - def allowedCallers = [] as Set - def ctx = MetacatRequestContext.builder().build() - ctx.getAdditionalContext().put(MetacatRequestContext.SSO_DIRECT_CALLER_APP_NAME, "bdpauthzservice") - MetacatContextManager.setContext(ctx) - - when: - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, operation, resource) - - then: - def ex = thrown(CatalogUnauthorizedException) - ex.message.contains("bdpauthzservice") - ex.message.contains(catalogName) - } - - def "checkAuthorization - case sensitive matching"() { - given: - def allowedCallers = [new AuthorizedCaller("bdpauthzservice", null)] as Set // lowercase - def ctx = MetacatRequestContext.builder().build() - ctx.getAdditionalContext().put(MetacatRequestContext.SSO_DIRECT_CALLER_APP_NAME, "BDPAUTHZSERVICE") // uppercase - MetacatContextManager.setContext(ctx) - - when: - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, operation, resource) - - then: - thrown(CatalogUnauthorizedException) - } - - def "checkAuthorization - exact match required"() { - given: - def allowedCallers = [new AuthorizedCaller("bdpauthzservice", null)] as Set - def ctx = MetacatRequestContext.builder().build() - ctx.getAdditionalContext().put(MetacatRequestContext.SSO_DIRECT_CALLER_APP_NAME, "bdpauthz") // partial match - MetacatContextManager.setContext(ctx) - - when: - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, operation, resource) - - then: - thrown(CatalogUnauthorizedException) - } - - def "checkAuthorization - multiple allowed callers - any match succeeds"() { - given: - def allowedCallers = [new AuthorizedCaller("app1", null), new AuthorizedCaller("app2", null), new AuthorizedCaller("app3", null)] as Set - def ctx = MetacatRequestContext.builder().build() - ctx.getAdditionalContext().put(MetacatRequestContext.SSO_DIRECT_CALLER_APP_NAME, caller) - MetacatContextManager.setContext(ctx) - - when: - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, operation, resource) - - then: - noExceptionThrown() - - where: - caller << ["app1", "app2", "app3"] - } - - def "checkAuthorization - compound rule allows when both app and userName match"() { - given: - def allowedCallers = [new AuthorizedCaller("hadoop", "idm-worker")] as Set - def ctx = MetacatRequestContext.builder().build() - ctx.getAdditionalContext().put(MetacatRequestContext.SSO_DIRECT_CALLER_APP_NAME, "hadoop") - ctx.setUserName("idm-worker") - MetacatContextManager.setContext(ctx) - - when: - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, operation, resource) - - then: - noExceptionThrown() - } - - def "checkAuthorization - compound rule denies when userName does not match"() { - given: - def allowedCallers = [new AuthorizedCaller("hadoop", "idm-worker")] as Set - def ctx = MetacatRequestContext.builder().build() - ctx.getAdditionalContext().put(MetacatRequestContext.SSO_DIRECT_CALLER_APP_NAME, "hadoop") - ctx.setUserName("alice") - MetacatContextManager.setContext(ctx) - - when: - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, operation, resource) - - then: - thrown(CatalogUnauthorizedException) - } - - def "checkAuthorization - compound rule denies when userName is null"() { - given: - def allowedCallers = [new AuthorizedCaller("hadoop", "idm-worker")] as Set - def ctx = MetacatRequestContext.builder().build() - ctx.getAdditionalContext().put(MetacatRequestContext.SSO_DIRECT_CALLER_APP_NAME, "hadoop") - // userName not set - defaults to null - MetacatContextManager.setContext(ctx) - - when: - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, operation, resource) - - then: - thrown(CatalogUnauthorizedException) - } - - def "checkAuthorization - mixed list - plain rule still matches any userName"() { - given: - def allowedCallers = [ - new AuthorizedCaller("irc", null), - new AuthorizedCaller("hadoop", "idm-worker") - ] as Set - def ctx = MetacatRequestContext.builder().build() - ctx.getAdditionalContext().put(MetacatRequestContext.SSO_DIRECT_CALLER_APP_NAME, "irc") - ctx.setUserName("anyone") - MetacatContextManager.setContext(ctx) - - when: - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, operation, resource) - - then: - noExceptionThrown() - } - - def "checkAuthorization - compound rule does not match a different app"() { - given: - def allowedCallers = [new AuthorizedCaller("hadoop", "idm-worker")] as Set - def ctx = MetacatRequestContext.builder().build() - ctx.getAdditionalContext().put(MetacatRequestContext.SSO_DIRECT_CALLER_APP_NAME, "other") - ctx.setUserName("idm-worker") - MetacatContextManager.setContext(ctx) - - when: - ConnectorAuthorizationUtil.checkAuthorization(catalogName, allowedCallers, operation, resource) - - then: - thrown(CatalogUnauthorizedException) - } -} diff --git a/metacat-common/src/main/java/com/netflix/metacat/common/MetacatRequestContext.java b/metacat-common/src/main/java/com/netflix/metacat/common/MetacatRequestContext.java index 6c83547a9..1efb31d02 100644 --- a/metacat-common/src/main/java/com/netflix/metacat/common/MetacatRequestContext.java +++ b/metacat-common/src/main/java/com/netflix/metacat/common/MetacatRequestContext.java @@ -55,11 +55,6 @@ public class MetacatRequestContext implements Serializable { */ public static final String HEADER_KEY_DATA_TYPE_CONTEXT = "X-Netflix.data.type.context"; - /** - * Key for the SSO direct caller app name in additionalContext. - */ - public static final String SSO_DIRECT_CALLER_APP_NAME = "SsoDirectCallerAppName"; - /** * Default if unknown. */