diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/CatalogApplication.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/CatalogApplication.java index 9d3462ea10d..df5b0339d44 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/CatalogApplication.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/CatalogApplication.java @@ -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 { @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 { 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) { diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/Entity.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/Entity.java index 0e876806a63..e52ec7a2c3b 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/Entity.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/Entity.java @@ -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> DAO_MAP = new HashMap<>(); private static final Map> 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 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 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 EntityRepository getEntityRepository(@NonNull String entityName) { @SuppressWarnings("unchecked") EntityRepository entityRepository = (EntityRepository) 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 { diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/PolicyRepository.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/PolicyRepository.java index 9c89c42d11a..5bd0d793b5f 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/PolicyRepository.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/PolicyRepository.java @@ -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 { // No validation errors, if execution reaches here. } + private List getAccessControlPolicies() throws IOException { + EntityUtil.Fields fields = new EntityUtil.Fields(List.of("policyType", "rules")); + List jsons = daoCollection.policyDAO().listAfter(null, Integer.MAX_VALUE, ""); + List 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 getAccessControlPolicyRules() throws IOException { + List policies = getAccessControlPolicies(); + List rules = new ArrayList<>(); + for (Policy policy : policies) { + List 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 { @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()); diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/Authorizer.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/Authorizer.java index 478dac57437..9edc61ebe3c 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/Authorizer.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/Authorizer.java @@ -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); diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/DefaultAuthorizer.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/DefaultAuthorizer.java index 7430b8b3169..fdc8d8b7695 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/DefaultAuthorizer.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/DefaultAuthorizer.java @@ -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() diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/NoopAuthorizer.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/NoopAuthorizer.java index 4e91b15a135..bbcca5f0fea 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/NoopAuthorizer.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/NoopAuthorizer.java @@ -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; diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/SecurityUtil.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/SecurityUtil.java index 973215623a4..f152749e368 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/SecurityUtil.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/SecurityUtil.java @@ -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]; } diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/policyevaluator/AttributeBasedFacts.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/policyevaluator/AttributeBasedFacts.java index cd01cd13ed8..6f50adaaaa2 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/policyevaluator/AttributeBasedFacts.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/security/policyevaluator/AttributeBasedFacts.java @@ -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 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 getUserRoles(@NonNull User user) { + return user.getRoles().stream().map(EntityReference::getName).collect(Collectors.toList()); } - private List 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 entityTags = Collections.emptyList(); + private List getEntityTags(@NonNull Object entity) { + List entityTags = null; try { - entityTags = (List) 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 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; + 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); + } + return entityType; } } diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/util/EntityUtil.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/util/EntityUtil.java index 45cd8ece079..9833a00e139 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/util/EntityUtil.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/util/EntityUtil.java @@ -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 fieldList;