feat(authorization): Adding AuthorizerContext + ResourceSpecResolver to context (#4982)

This commit is contained in:
John Joyce 2022-05-24 11:06:49 -07:00 committed by GitHub
parent b77f381429
commit ce061e3fb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 147 additions and 81 deletions

View File

@ -15,12 +15,12 @@ public interface Authorizer {
* @param authorizerConfig config provided to the authenticator derived from the Metadata Service YAML config. This
* config comes from the "authorization.authorizers.config" configuration.
*/
void init(@Nonnull final Map<String, Object> authorizerConfig);
void init(@Nonnull final Map<String, Object> authorizerConfig, @Nonnull final AuthorizerContext ctx);
/**
* Authorizes an action based on the actor, the resource, & required privileges.
*/
AuthorizationResult authorize(AuthorizationRequest request);
AuthorizationResult authorize(@Nonnull final AuthorizationRequest request);
/**
* Retrieves the current list of actors authorized to for a particular privilege against

View File

@ -0,0 +1,17 @@
package com.datahub.authorization;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* Context provided to an Authorizer on initialization.
*/
@Data
@AllArgsConstructor
public class AuthorizerContext {
/**
* A utility for resolving a {@link ResourceSpec} to resolved resource field values.
*/
private ResourceSpecResolver resourceSpecResolver;
}

View File

@ -0,0 +1,66 @@
package com.datahub.authorization;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* Wrapper around authorization request with field resolvers for lazily fetching the field values for each field type
*/
@RequiredArgsConstructor
public class ResolvedResourceSpec {
@Getter
private final ResourceSpec spec;
private final Map<ResourceFieldType, FieldResolver> fieldResolvers;
public Set<String> getFieldValues(ResourceFieldType resourceFieldType) {
if (!fieldResolvers.containsKey(resourceFieldType)) {
return Collections.emptySet();
}
return fieldResolvers.get(resourceFieldType).getFieldValuesFuture().join().getValues();
}
/**
* Fetch the entity-registry type for a resource. ('dataset', 'dashboard', 'chart').
* @return the entity type.
*/
public String getType() {
if (!fieldResolvers.containsKey(ResourceFieldType.RESOURCE_TYPE)) {
throw new UnsupportedOperationException("Failed to resolve resource type! No field resolver for RESOURCE_TYPE provided.");
}
Set<String> resourceTypes = fieldResolvers.get(ResourceFieldType.RESOURCE_TYPE).getFieldValuesFuture().join().getValues();
assert resourceTypes.size() == 1; // There should always be a single resource type.
return resourceTypes.stream().findFirst().get();
}
/**
* Fetch the owners for a resource.
* @return a set of owner urns, or empty set if none exist.
*/
public Set<String> getOwners() {
if (!fieldResolvers.containsKey(ResourceFieldType.OWNER)) {
return Collections.emptySet();
}
return fieldResolvers.get(ResourceFieldType.OWNER).getFieldValuesFuture().join().getValues();
}
/**
* Fetch the domain for a Resolved Resource Spec
* @return a Domain or null if one does not exist.
*/
@Nullable
public String getDomain() {
if (!fieldResolvers.containsKey(ResourceFieldType.DOMAIN)) {
return null;
}
Set<String> domains = fieldResolvers.get(ResourceFieldType.DOMAIN).getFieldValuesFuture().join().getValues();
if (domains.size() > 0) {
return domains.stream().findFirst().get();
}
return null;
}
}

View File

@ -1,4 +1,4 @@
package com.datahub.authorization.fieldresolverprovider;
package com.datahub.authorization;
/**
* List of resource field types to fetch for a given resource

View File

@ -0,0 +1,11 @@
package com.datahub.authorization;
/**
* A Resource Spec Resolver is responsible for resolving a {@link ResourceSpec} to a {@link ResolvedResourceSpec}.
*/
public interface ResourceSpecResolver {
/**
Resolve a {@link ResourceSpec} to a resolved resource spec.
**/
ResolvedResourceSpec resolve(ResourceSpec resourceSpec);
}

View File

@ -30,7 +30,7 @@ public class AuthorizerChain implements Authorizer {
}
@Override
public void init(@Nonnull Map<String, Object> authorizerConfig) {
public void init(@Nonnull Map<String, Object> authorizerConfig, @Nonnull AuthorizerContext ctx) {
// pass.
}

View File

@ -2,8 +2,6 @@ package com.datahub.authorization;
import com.datahub.authentication.Authentication;
import com.google.common.annotations.VisibleForTesting;
import com.linkedin.common.Owner;
import com.linkedin.common.Ownership;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.entity.client.EntityClient;
@ -13,18 +11,15 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import static com.linkedin.metadata.Constants.CORP_GROUP_ENTITY_NAME;
import static com.linkedin.metadata.Constants.CORP_USER_ENTITY_NAME;
/**
* The Authorizer is a singleton class responsible for authorizing
@ -43,13 +38,12 @@ public class DataHubAuthorizer implements Authorizer {
*/
DEFAULT,
/**
* Allow all means that the AuthorizationManager will allow all actions. This is used as an override to disable the
* Allow all means that the DataHubAuthorizer will allow all actions. This is used as an override to disable the
* policies feature.
*/
ALLOW_ALL
}
// Credentials used to make / authorize requests as the internal system actor.
private final Authentication _systemAuthentication;
@ -59,9 +53,8 @@ public class DataHubAuthorizer implements Authorizer {
private final ScheduledExecutorService _refreshExecutorService = Executors.newScheduledThreadPool(1);
private final PolicyRefreshRunnable _policyRefreshRunnable;
private final ResourceSpecResolver _resourceSpecResolver;
private final PolicyEngine _policyEngine;
private ResourceSpecResolver _resourceSpecResolver;
private AuthorizationMode _mode;
public static final String ALL = "ALL";
@ -72,20 +65,20 @@ public class DataHubAuthorizer implements Authorizer {
final int delayIntervalSeconds,
final int refreshIntervalSeconds,
final AuthorizationMode mode) {
_systemAuthentication = systemAuthentication;
_systemAuthentication = Objects.requireNonNull(systemAuthentication);
_mode = Objects.requireNonNull(mode);
_policyEngine = new PolicyEngine(systemAuthentication, Objects.requireNonNull(entityClient));
_policyRefreshRunnable = new PolicyRefreshRunnable(systemAuthentication, new PolicyFetcher(entityClient), _policyCache);
_refreshExecutorService.scheduleAtFixedRate(_policyRefreshRunnable, delayIntervalSeconds, refreshIntervalSeconds, TimeUnit.SECONDS);
_mode = mode;
_resourceSpecResolver = new ResourceSpecResolver(systemAuthentication, entityClient);
_policyEngine = new PolicyEngine(systemAuthentication, entityClient);
}
@Override
public void init(@Nonnull Map<String, Object> authorizerConfig) {
public void init(@Nonnull Map<String, Object> authorizerConfig, @Nonnull AuthorizerContext ctx) {
// Pass. No static config.
_resourceSpecResolver = Objects.requireNonNull(ctx.getResourceSpecResolver());
}
public AuthorizationResult authorize(final AuthorizationRequest request) {
public AuthorizationResult authorize(@Nonnull final AuthorizationRequest request) {
// 0. Short circuit: If the action is being performed by the system (root), always allow it.
if (isSystemRequest(request, this._systemAuthentication)) {
@ -265,20 +258,4 @@ public class DataHubAuthorizer implements Authorizer {
cache.put(ALL, existingPolicies);
}
}
private List<Urn> userOwners(final Ownership ownership) {
return ownership.getOwners()
.stream()
.filter(owner -> CORP_USER_ENTITY_NAME.equals(owner.getOwner().getEntityType()))
.map(Owner::getOwner)
.collect(Collectors.toList());
}
private List<Urn> groupOwners(final Ownership ownership) {
return ownership.getOwners()
.stream()
.filter(owner -> CORP_GROUP_ENTITY_NAME.equals(owner.getOwner().getEntityType()))
.map(Owner::getOwner)
.collect(Collectors.toList());
}
}

View File

@ -4,7 +4,6 @@ import com.datahub.authentication.Authentication;
import com.datahub.authorization.fieldresolverprovider.DomainFieldResolverProvider;
import com.datahub.authorization.fieldresolverprovider.EntityTypeFieldResolverProvider;
import com.datahub.authorization.fieldresolverprovider.EntityUrnFieldResolverProvider;
import com.datahub.authorization.fieldresolverprovider.ResourceFieldType;
import com.datahub.authorization.fieldresolverprovider.OwnerFieldResolverProvider;
import com.datahub.authorization.fieldresolverprovider.ResourceFieldResolverProvider;
import com.google.common.collect.ImmutableList;
@ -14,21 +13,22 @@ import java.util.Map;
import java.util.stream.Collectors;
public class ResourceSpecResolver {
public class DefaultResourceSpecResolver implements ResourceSpecResolver {
private final List<ResourceFieldResolverProvider> _resourceFieldResolverProviders;
public ResourceSpecResolver(Authentication systemAuthentication, EntityClient entityClient) {
public DefaultResourceSpecResolver(Authentication systemAuthentication, EntityClient entityClient) {
_resourceFieldResolverProviders =
ImmutableList.of(new EntityTypeFieldResolverProvider(), new EntityUrnFieldResolverProvider(),
new DomainFieldResolverProvider(entityClient, systemAuthentication),
new OwnerFieldResolverProvider(entityClient, systemAuthentication));
}
@Override
public ResolvedResourceSpec resolve(ResourceSpec resourceSpec) {
return new ResolvedResourceSpec(resourceSpec, getFieldResolvers(resourceSpec));
}
public Map<ResourceFieldType, FieldResolver> getFieldResolvers(ResourceSpec resourceSpec) {
private Map<ResourceFieldType, FieldResolver> getFieldResolvers(ResourceSpec resourceSpec) {
return _resourceFieldResolverProviders.stream()
.collect(Collectors.toMap(ResourceFieldResolverProvider::getFieldType,
hydrator -> hydrator.getFieldResolver(resourceSpec)));

View File

@ -1,6 +1,5 @@
package com.datahub.authorization;
import com.datahub.authorization.fieldresolverprovider.ResourceFieldType;
import com.linkedin.data.template.StringArray;
import com.linkedin.policy.PolicyMatchCondition;
import com.linkedin.policy.PolicyMatchCriterion;

View File

@ -1,7 +1,6 @@
package com.datahub.authorization;
import com.datahub.authentication.Authentication;
import com.datahub.authorization.fieldresolverprovider.ResourceFieldType;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.data.template.StringArray;

View File

@ -1,33 +0,0 @@
package com.datahub.authorization;
import com.datahub.authorization.fieldresolverprovider.ResourceFieldType;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* Wrapper around authorization request with field resolvers for lazily fetching the field values for each field type
*/
@RequiredArgsConstructor
public class ResolvedResourceSpec {
@Getter
private final ResourceSpec spec;
private final Map<ResourceFieldType, FieldResolver> fieldResolvers;
public Set<String> getFieldValues(ResourceFieldType resourceFieldType) {
if (!fieldResolvers.containsKey(resourceFieldType)) {
return Collections.emptySet();
}
return fieldResolvers.get(resourceFieldType).getFieldValuesFuture().join().getValues();
}
public Set<String> getOwners() {
if (!fieldResolvers.containsKey(ResourceFieldType.OWNER)) {
return Collections.emptySet();
}
return fieldResolvers.get(ResourceFieldType.OWNER).getFieldValuesFuture().join().getValues();
}
}

View File

@ -2,6 +2,7 @@ package com.datahub.authorization.fieldresolverprovider;
import com.datahub.authentication.Authentication;
import com.datahub.authorization.FieldResolver;
import com.datahub.authorization.ResourceFieldType;
import com.datahub.authorization.ResourceSpec;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;

View File

@ -1,6 +1,7 @@
package com.datahub.authorization.fieldresolverprovider;
import com.datahub.authorization.FieldResolver;
import com.datahub.authorization.ResourceFieldType;
import com.datahub.authorization.ResourceSpec;
import java.util.Collections;

View File

@ -1,6 +1,7 @@
package com.datahub.authorization.fieldresolverprovider;
import com.datahub.authorization.FieldResolver;
import com.datahub.authorization.ResourceFieldType;
import com.datahub.authorization.ResourceSpec;
import java.util.Collections;

View File

@ -2,6 +2,7 @@ package com.datahub.authorization.fieldresolverprovider;
import com.datahub.authentication.Authentication;
import com.datahub.authorization.FieldResolver;
import com.datahub.authorization.ResourceFieldType;
import com.datahub.authorization.ResourceSpec;
import com.linkedin.common.Ownership;
import com.linkedin.common.urn.Urn;

View File

@ -1,6 +1,7 @@
package com.datahub.authorization.fieldresolverprovider;
import com.datahub.authorization.FieldResolver;
import com.datahub.authorization.ResourceFieldType;
import com.datahub.authorization.ResourceSpec;

View File

@ -107,6 +107,7 @@ public class DataHubAuthorizerTest {
10,
DataHubAuthorizer.AuthorizationMode.DEFAULT
);
_dataHubAuthorizer.init(Collections.emptyMap(), createAuthorizerContext(systemAuthentication, _entityClient));
_dataHubAuthorizer.invalidateCache();
Thread.sleep(500); // Sleep so the runnable can execute. (not ideal)
}
@ -282,4 +283,8 @@ public class DataHubAuthorizerTest {
ownershipAspect.setLastModified(new AuditStamp().setTime(0).setActor(Urn.createFromString("urn:li:corpuser:foo")));
return ownershipAspect;
}
private AuthorizerContext createAuthorizerContext(final Authentication systemAuthentication, final EntityClient entityClient) {
return new AuthorizerContext(new DefaultResourceSpecResolver(systemAuthentication, entityClient));
}
}

View File

@ -1,7 +1,6 @@
package com.datahub.authorization;
import com.datahub.authentication.Authentication;
import com.datahub.authorization.fieldresolverprovider.ResourceFieldType;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;

View File

@ -1,9 +1,14 @@
package com.linkedin.gms.factory.auth;
import com.datahub.authentication.Authentication;
import com.datahub.authorization.AuthorizerConfiguration;
import com.datahub.authorization.AuthorizerContext;
import com.datahub.authorization.DataHubAuthorizer;
import com.datahub.authorization.Authorizer;
import com.datahub.authorization.AuthorizerChain;
import com.datahub.authorization.DefaultResourceSpecResolver;
import com.datahub.authorization.ResourceSpecResolver;
import com.linkedin.entity.client.JavaEntityClient;
import com.linkedin.gms.factory.config.ConfigurationProvider;
import com.linkedin.gms.factory.spring.YamlPropertySourceFactory;
import java.util.ArrayList;
@ -31,20 +36,36 @@ public class AuthorizerChainFactory {
@Autowired
@Qualifier("dataHubAuthorizer")
private DataHubAuthorizer _dataHubAuthorizer;
private DataHubAuthorizer dataHubAuthorizer;
@Autowired
@Qualifier("systemAuthentication")
private Authentication systemAuthentication;
@Autowired
@Qualifier("javaEntityClient")
private JavaEntityClient entityClient;
@Bean(name = "authorizerChain")
@Scope("singleton")
@Nonnull
protected AuthorizerChain getInstance() {
// Init authorizer context
final AuthorizerContext ctx = initAuthorizerContext();
// Extract + initialize customer authorizers from application configs.
final List<Authorizer> authorizers = new ArrayList<>(initCustomAuthorizers());
final List<Authorizer> authorizers = new ArrayList<>(initCustomAuthorizers(ctx));
// Add the DataHub core policies-based Authorizer - this one should always be enabled.
authorizers.add(this._dataHubAuthorizer);
this.dataHubAuthorizer.init(Collections.emptyMap(), ctx);
authorizers.add(this.dataHubAuthorizer);
return new AuthorizerChain(authorizers);
}
private List<Authorizer> initCustomAuthorizers() {
private AuthorizerContext initAuthorizerContext() {
final ResourceSpecResolver resolver = new DefaultResourceSpecResolver(systemAuthentication, entityClient);
return new AuthorizerContext(resolver);
}
private List<Authorizer> initCustomAuthorizers(AuthorizerContext ctx) {
final List<Authorizer> customAuthorizers = new ArrayList<>();
if (this.configurationProvider.getAuthorization().getAuthorizers() != null) {
@ -71,7 +92,7 @@ public class AuthorizerChainFactory {
// Else construct an instance of the class, each class should have an empty constructor.
try {
final Authorizer authorizerInstance = clazz.newInstance();
authorizerInstance.init(configs);
authorizerInstance.init(configs, ctx);
customAuthorizers.add(authorizerInstance);
} catch (Exception e) {
throw new RuntimeException(String.format("Failed to instantiate custom Authorizer with class name %s", clazz.getCanonicalName()), e);