Integrate DefaultAuthorizer with PolicyEvaluator (#2017)

This commit is contained in:
Matt 2022-01-09 21:04:10 -08:00 committed by GitHub
parent ed797dc335
commit 321a0e811b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 183 additions and 52 deletions

View File

@ -27,7 +27,9 @@ import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import io.federecio.dropwizard.swagger.SwaggerBundle;
import io.federecio.dropwizard.swagger.SwaggerBundleConfiguration;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.time.temporal.ChronoUnit;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.Response;
@ -37,6 +39,8 @@ import org.eclipse.jetty.servlet.ErrorPageErrorHandler;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ServerProperties;
import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.core.statement.SqlLogger;
import org.jdbi.v3.core.statement.StatementContext;
import org.openmetadata.catalog.events.EventFilter;
import org.openmetadata.catalog.events.EventPubSub;
import org.openmetadata.catalog.exception.CatalogGenericExceptionMapper;
@ -62,19 +66,25 @@ public class CatalogApplication extends Application<CatalogApplicationConfig> {
@Override
public void run(CatalogApplicationConfig catalogConfig, Environment environment)
throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException,
InvocationTargetException {
InvocationTargetException, IOException {
final JdbiFactory factory = new JdbiFactory();
final Jdbi jdbi = factory.build(environment, catalogConfig.getDataSourceFactory(), "mysql3");
// SqlLogger sqlLogger = new SqlLogger() {
// @Override
// public void logAfterExecution(StatementContext context) {
// LOG.info("sql {}, parameters {}, timeTaken {} ms", context.getRenderedSql(),
// context.getBinding().toString(), context.getElapsedTime(ChronoUnit.MILLIS));
// }
// };
// jdbi.setSqlLogger(sqlLogger);
SqlLogger sqlLogger =
new SqlLogger() {
@Override
public void logAfterExecution(StatementContext context) {
LOG.debug(
"sql {}, parameters {}, timeTaken {} ms",
context.getRenderedSql(),
context.getBinding(),
context.getElapsedTime(ChronoUnit.MILLIS));
}
};
if (LOG.isDebugEnabled()) {
jdbi.setSqlLogger(sqlLogger);
}
// Register Authorizer
registerAuthorizer(catalogConfig, environment, jdbi);
@ -128,7 +138,7 @@ public class CatalogApplication extends Application<CatalogApplicationConfig> {
private void registerAuthorizer(CatalogApplicationConfig catalogConfig, Environment environment, Jdbi jdbi)
throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InvocationTargetException,
InstantiationException {
InstantiationException, IOException {
AuthorizerConfiguration authorizerConf = catalogConfig.getAuthorizerConfiguration();
AuthenticationConfiguration authenticationConfiguration = catalogConfig.getAuthenticationConfiguration();
if (authorizerConf != null) {

View File

@ -15,6 +15,7 @@ package org.openmetadata.catalog;
import java.io.IOException;
import java.net.URI;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@ -24,13 +25,17 @@ import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import javax.ws.rs.core.UriInfo;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.openmetadata.catalog.exception.CatalogExceptionMessage;
import org.openmetadata.catalog.exception.EntityNotFoundException;
import org.openmetadata.catalog.jdbi3.EntityDAO;
import org.openmetadata.catalog.jdbi3.EntityRepository;
import org.openmetadata.catalog.type.EntityReference;
import org.openmetadata.catalog.util.EntityInterface;
import org.openmetadata.catalog.util.EntityUtil;
@Slf4j
public final class Entity {
private static final Map<String, EntityDAO<?>> DAO_MAP = new HashMap<>();
private static final Map<String, EntityRepository<?>> ENTITY_REPOSITORY_MAP = new HashMap<>();
@ -86,7 +91,7 @@ public final class Entity {
DAO_MAP.put(entity, dao);
ENTITY_REPOSITORY_MAP.put(entity, entityRepository);
CANONICAL_ENTITY_NAME_MAP.put(entity.toLowerCase(Locale.ROOT), entity);
System.out.println("Registering entity " + entity);
log.info("Registering entity {}", entity);
}
public static EntityReference getEntityReference(String entity, UUID id) throws IOException {
@ -138,12 +143,45 @@ public final class Entity {
return null;
}
String entityName = getEntityNameFromObject(entity);
EntityRepository<T> entityRepository = getEntityRepository(entityName);
return entityRepository.getEntityInterface(entity);
}
/**
* Retrieve the entity using id from given entity reference and fields
*
* @return entity object eg: {@link org.openmetadata.catalog.entity.data.Table}, {@link
* org.openmetadata.catalog.entity.data.Topic}, etc
*/
public static <T> T getEntity(EntityReference entityReference, EntityUtil.Fields fields)
throws IOException, ParseException {
if (entityReference == null) {
return null;
}
EntityRepository<?> entityRepository = Entity.getEntityRepository(entityReference.getType());
@SuppressWarnings("unchecked")
T entity = (T) entityRepository.get(null, entityReference.getId().toString(), fields);
if (entity == null) {
throw EntityNotFoundException.byMessage(
CatalogExceptionMessage.entityNotFound(entityReference.getType(), entityReference.getId()));
}
return entity;
}
/**
* Retrieve the corresponding entity repository for a given entity name.
*
* @param entityName type of entity, eg: {@link Entity#TABLE}, {@link Entity#TOPIC}, etc
* @return entity repository corresponding to the entity name
*/
public static <T> EntityRepository<T> getEntityRepository(@NonNull String entityName) {
@SuppressWarnings("unchecked")
EntityRepository<T> entityRepository = (EntityRepository<T>) ENTITY_REPOSITORY_MAP.get(entityName);
if (entityRepository == null) {
throw EntityNotFoundException.byMessage(CatalogExceptionMessage.entityTypeNotFound(entityName));
}
return entityRepository.getEntityInterface(entity);
return entityRepository;
}
public static void deleteEntity(String entity, UUID entityId, boolean recursive) throws IOException {

View File

@ -15,6 +15,7 @@ package org.openmetadata.catalog.jdbi3;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@ -24,6 +25,7 @@ import org.openmetadata.catalog.Entity;
import org.openmetadata.catalog.entity.data.Location;
import org.openmetadata.catalog.entity.policies.Policy;
import org.openmetadata.catalog.entity.policies.accessControl.Rule;
import org.openmetadata.catalog.exception.CatalogExceptionMessage;
import org.openmetadata.catalog.resources.policies.PolicyResource;
import org.openmetadata.catalog.type.ChangeDescription;
import org.openmetadata.catalog.type.EntityReference;
@ -184,6 +186,33 @@ public class PolicyRepository extends EntityRepository<Policy> {
// No validation errors, if execution reaches here.
}
private List<Policy> getAccessControlPolicies() throws IOException {
EntityUtil.Fields fields = new EntityUtil.Fields(List.of("policyType", "rules"));
List<String> jsons = daoCollection.policyDAO().listAfter(null, Integer.MAX_VALUE, "");
List<Policy> policies = new ArrayList<>(jsons.size());
for (String json : jsons) {
Policy policy = setFields(JsonUtils.readValue(json, Policy.class), fields);
if (policy.getPolicyType() != PolicyType.AccessControl) {
continue;
}
policies.add(policy);
}
return policies;
}
public List<Rule> getAccessControlPolicyRules() throws IOException {
List<Policy> policies = getAccessControlPolicies();
List<Rule> rules = new ArrayList<>();
for (Policy policy : policies) {
List<Object> ruleObjects = policy.getRules();
for (Object ruleObject : ruleObjects) {
Rule rule = JsonUtils.readValue(JsonUtils.getJsonStructure(ruleObject).toString(), Rule.class);
rules.add(rule);
}
}
return rules;
}
private void setLocation(Policy policy, EntityReference location) {
if (location == null || location.getId() == null) {
return;
@ -334,6 +363,10 @@ public class PolicyRepository extends EntityRepository<Policy> {
@Override
public void entitySpecificUpdate() throws IOException {
// Disallow changing policyType.
if (original.getEntity().getPolicyType() != updated.getEntity().getPolicyType()) {
throw new IllegalArgumentException(CatalogExceptionMessage.readOnlyAttribute(Entity.POLICY, "policyType"));
}
recordChange("policyUrl", original.getEntity().getPolicyUrl(), updated.getEntity().getPolicyUrl());
recordChange("enabled", original.getEntity().getEnabled(), updated.getEntity().getEnabled());
recordChange("rules", original.getEntity().getRules(), updated.getEntity().getRules());

View File

@ -13,13 +13,15 @@
package org.openmetadata.catalog.security;
import java.io.IOException;
import org.jdbi.v3.core.Jdbi;
import org.openmetadata.catalog.type.EntityReference;
import org.openmetadata.catalog.type.MetadataOperation;
public interface Authorizer {
/** Initialize the authorizer */
void init(AuthorizerConfiguration config, Jdbi jdbi);
void init(AuthorizerConfiguration config, Jdbi jdbi) throws IOException;
/**
* Check if the authenticated user has given permission on the target entity identified by the given resourceType and
@ -27,6 +29,12 @@ public interface Authorizer {
*/
boolean hasPermissions(AuthenticationContext ctx, EntityReference entityReference);
/**
* Check if the authenticated user (subject) has permission to perform the {@link MetadataOperation} on the target
* entity (object).
*/
boolean hasPermissions(AuthenticationContext ctx, EntityReference entityReference, MetadataOperation operation);
boolean isAdmin(AuthenticationContext ctx);
boolean isBot(AuthenticationContext ctx);

View File

@ -19,6 +19,7 @@ import java.io.IOException;
import java.text.ParseException;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.apache.commons.lang3.exception.ExceptionUtils;
@ -28,8 +29,11 @@ import org.openmetadata.catalog.entity.teams.User;
import org.openmetadata.catalog.exception.DuplicateEntityException;
import org.openmetadata.catalog.exception.EntityNotFoundException;
import org.openmetadata.catalog.jdbi3.CollectionDAO;
import org.openmetadata.catalog.jdbi3.PolicyRepository;
import org.openmetadata.catalog.jdbi3.UserRepository;
import org.openmetadata.catalog.security.policyevaluator.PolicyEvaluator;
import org.openmetadata.catalog.type.EntityReference;
import org.openmetadata.catalog.type.MetadataOperation;
import org.openmetadata.catalog.util.EntityUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -42,19 +46,22 @@ public class DefaultAuthorizer implements Authorizer {
private String principalDomain;
private UserRepository userRepository;
private static final String fieldsParam = "teams";
private PolicyEvaluator policyEvaluator;
private static final String fieldsParam = "roles,teams";
@Override
public void init(AuthorizerConfiguration config, Jdbi dbi) {
public void init(AuthorizerConfiguration config, Jdbi dbi) throws IOException {
LOG.debug("Initializing DefaultAuthorizer with config {}", config);
this.adminUsers = new HashSet<>(config.getAdminPrincipals());
this.botUsers = new HashSet<>(config.getBotPrincipals());
this.principalDomain = config.getPrincipalDomain();
LOG.debug("Admin users: {}", adminUsers);
CollectionDAO repo = dbi.onDemand(CollectionDAO.class);
this.userRepository = new UserRepository(repo);
CollectionDAO collectionDAO = dbi.onDemand(CollectionDAO.class);
this.userRepository = new UserRepository(collectionDAO);
mayBeAddAdminUsers();
mayBeAddBotUsers();
// Load all rules from access control policies at once during init.
this.policyEvaluator = new PolicyEvaluator(new PolicyRepository(collectionDAO).getAccessControlPolicyRules());
}
private void mayBeAddAdminUsers() {
@ -104,10 +111,8 @@ public class DefaultAuthorizer implements Authorizer {
if (owner == null) {
return true;
}
String userName = SecurityUtil.getUserName(ctx);
EntityUtil.Fields fields = new EntityUtil.Fields(FIELD_LIST, fieldsParam);
try {
User user = userRepository.getByName(null, userName, fields);
User user = getUserFromAuthenticationContext(ctx);
if (owner.getType().equals(Entity.TEAM)) {
for (EntityReference team : user.getTeams()) {
if (team.getName().equals(owner.getName())) {
@ -123,6 +128,20 @@ public class DefaultAuthorizer implements Authorizer {
}
}
@Override
public boolean hasPermissions(
AuthenticationContext ctx, EntityReference entityReference, MetadataOperation operation) {
validateAuthenticationContext(ctx);
try {
return policyEvaluator.hasPermission(
getUserFromAuthenticationContext(ctx),
Entity.getEntity(entityReference, new EntityUtil.Fields(List.of("tags"))),
operation);
} catch (IOException | EntityNotFoundException | ParseException ex) {
return false;
}
}
@Override
public boolean isAdmin(AuthenticationContext ctx) {
validateAuthenticationContext(ctx);
@ -161,6 +180,12 @@ public class DefaultAuthorizer implements Authorizer {
}
}
private User getUserFromAuthenticationContext(AuthenticationContext ctx) throws IOException, ParseException {
String userName = SecurityUtil.getUserName(ctx);
EntityUtil.Fields fields = new EntityUtil.Fields(FIELD_LIST, fieldsParam);
return userRepository.getByName(null, userName, fields);
}
private void addAdmin(String name) {
User user =
new User()

View File

@ -15,6 +15,7 @@ package org.openmetadata.catalog.security;
import org.jdbi.v3.core.Jdbi;
import org.openmetadata.catalog.type.EntityReference;
import org.openmetadata.catalog.type.MetadataOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -31,6 +32,12 @@ public class NoopAuthorizer implements Authorizer {
return true;
}
@Override
public boolean hasPermissions(
AuthenticationContext ctx, EntityReference entityReference, MetadataOperation operation) {
return true;
}
@Override
public boolean isAdmin(AuthenticationContext ctx) {
return true;

View File

@ -20,6 +20,7 @@ import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.SecurityContext;
import org.openmetadata.catalog.type.EntityReference;
import org.openmetadata.catalog.type.MetadataOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -55,6 +56,20 @@ public final class SecurityUtil {
}
}
public static void checkAdminRoleOrPermissions(
Authorizer authorizer,
SecurityContext securityContext,
EntityReference entityReference,
MetadataOperation metadataOperation) {
Principal principal = securityContext.getUserPrincipal();
AuthenticationContext authenticationCtx = SecurityUtil.getAuthenticationContext(principal);
if (!authorizer.isAdmin(authenticationCtx)
&& !authorizer.isBot(authenticationCtx)
&& !authorizer.hasPermissions(authenticationCtx, entityReference, metadataOperation)) {
throw new AuthorizationException("Principal: " + principal + " does not have permissions");
}
}
public static String getUserName(String principalName) {
return principalName == null ? null : principalName.split("[/@]")[0];
}

View File

@ -1,25 +1,27 @@
package org.openmetadata.catalog.security.policyevaluator;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import lombok.Builder;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.jeasy.rules.api.Facts;
import org.openmetadata.catalog.Entity;
import org.openmetadata.catalog.entity.teams.User;
import org.openmetadata.catalog.exception.EntityNotFoundException;
import org.openmetadata.catalog.type.EntityReference;
import org.openmetadata.catalog.type.MetadataOperation;
import org.openmetadata.catalog.type.TagLabel;
import org.openmetadata.catalog.util.EntityInterface;
@Slf4j
@Builder(setterPrefix = "with")
class AttributeBasedFacts {
private User user;
private Object entity;
private MetadataOperation operation;
@NonNull private User user;
@NonNull private Object entity;
@NonNull private MetadataOperation operation;
// Do not allow anything external or the builder itself change the value of facts.
// Individual Fact(s) within facts may be changed by the RulesEngine.
@ -29,10 +31,7 @@ class AttributeBasedFacts {
* Creates {@link Facts} with the operation, user (subject) and entity (object) attributes so that it is recognizable
* by {@link org.jeasy.rules.api.RulesEngine}
*/
public Facts getFacts() throws RuntimeException {
if (!validate()) {
throw new RuntimeException("Validation failed while building facts");
}
public Facts getFacts() {
facts.put(CommonFields.USER_ROLES, getUserRoles(user));
facts.put(CommonFields.ENTITY_TAGS, getEntityTags(entity));
facts.put(CommonFields.ENTITY_TYPE, getEntityType(entity));
@ -46,35 +45,29 @@ class AttributeBasedFacts {
return facts.get(CommonFields.ALLOW);
}
private List<String> getUserRoles(User user) {
// TODO: Fix this to fetch user's roles when roles is added as entity reference list from user schema.
return user.getTeams().stream().map(EntityReference::getName).collect(Collectors.toList());
private List<String> getUserRoles(@NonNull User user) {
return user.getRoles().stream().map(EntityReference::getName).collect(Collectors.toList());
}
private List<String> getEntityTags(Object entity) {
// TODO: Fix the entityType fetch such that it is fetched from Repository or using a Util.
// This is done here now to facilitate prototyping and unit testing.
List<TagLabel> entityTags = Collections.emptyList();
private List<String> getEntityTags(@NonNull Object entity) {
List<TagLabel> entityTags = null;
try {
entityTags = (List<TagLabel>) entity.getClass().getMethod("getTags").invoke(entity);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
log.warn("Could not obtain tags for entity with class {}", entity.getClass());
EntityInterface<?> entityInterface = Entity.getEntityInterface(entity);
entityTags = entityInterface.getTags();
} catch (EntityNotFoundException e) {
log.warn("could not obtain tags for the given entity {} - exception: {}", entity, e.toString());
}
if (entityTags == null) {
return Collections.emptyList();
}
return entityTags.stream().map(TagLabel::getTagFQN).collect(Collectors.toList());
}
private static String getEntityType(Object object) {
// TODO: Fix the entityType fetch such that it is fetched from Repository or using a Util.
// This is done here now to facilitate prototyping and unit testing.
return object.getClass().getSimpleName().toLowerCase(Locale.ROOT);
private static String getEntityType(@NonNull Object entity) {
String entityType = Entity.getEntityNameFromObject(entity);
if (entityType == null) {
log.warn("could not find entity type for the given entity {}", entity);
}
private boolean validate() {
log.debug(
"Validating attribute based facts - user: {}, entity: {}, operation: {}",
this.user,
this.entity,
this.operation);
return this.user != null && this.entity != null && this.operation != null;
return entityType;
}
}

View File

@ -27,6 +27,7 @@ import java.util.function.BiPredicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.ws.rs.WebApplicationException;
import lombok.RequiredArgsConstructor;
import org.joda.time.Period;
import org.joda.time.format.ISOPeriodFormat;
import org.openmetadata.catalog.Entity;
@ -391,6 +392,7 @@ public final class EntityUtil {
return followers;
}
@RequiredArgsConstructor
public static class Fields {
public static final Fields EMPTY_FIELDS = new Fields(null, null);
private final List<String> fieldList;