Fixes #3702 Save user in authentication context to avoid multiple database lookups (#3703)

This commit is contained in:
Suresh Srinivas 2022-03-28 13:43:38 -07:00 committed by GitHub
parent 66e41523c6
commit 7af4248eaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 87 additions and 72 deletions

View File

@ -19,6 +19,9 @@ import java.util.UUID;
public final class CatalogExceptionMessage { public final class CatalogExceptionMessage {
public static final String ENTITY_ALREADY_EXISTS = "Entity already exists"; public static final String ENTITY_ALREADY_EXISTS = "Entity already exists";
public static final String FERNET_KEY_NULL = "Fernet key is null"; public static final String FERNET_KEY_NULL = "Fernet key is null";
public static final String FIELD_NOT_TOKENIZED = "Field is not tokenized";
public static final String FIELD_ALREADY_TOKENIZED = "Field is already tokenized";
public static final String INVALID_ENTITY_LINK = "Entity link must have both {arrayFieldName} and {arrayFieldValue}";
private CatalogExceptionMessage() {} private CatalogExceptionMessage() {}
@ -66,18 +69,6 @@ public final class CatalogExceptionMessage {
return String.format("Invalid service type `%s` for %s. Expected %s.", serviceType, entityType, expected); return String.format("Invalid service type `%s` for %s. Expected %s.", serviceType, entityType, expected);
} }
public static String invalidEntityLink() {
return "Entity link must have both {arrayFieldName} and {arrayFieldValue}";
}
public static String isNotTokenized() {
return "The field is not tokenized";
}
public static String isAlreadyTokenized() {
return "The field is already tokenized";
}
public static String glossaryTermMismatch(String parentId, String glossaryId) { public static String glossaryTermMismatch(String parentId, String glossaryId) {
return String.format( return String.format(
"Invalid queryParameters - glossary term `parent` %s is not in the `glossary` %s", parentId, glossaryId); "Invalid queryParameters - glossary term `parent` %s is not in the `glossary` %s", parentId, glossaryId);

View File

@ -14,8 +14,8 @@
package org.openmetadata.catalog.fernet; package org.openmetadata.catalog.fernet;
import static org.openmetadata.catalog.exception.CatalogExceptionMessage.FERNET_KEY_NULL; import static org.openmetadata.catalog.exception.CatalogExceptionMessage.FERNET_KEY_NULL;
import static org.openmetadata.catalog.exception.CatalogExceptionMessage.isAlreadyTokenized; import static org.openmetadata.catalog.exception.CatalogExceptionMessage.FIELD_ALREADY_TOKENIZED;
import static org.openmetadata.catalog.exception.CatalogExceptionMessage.isNotTokenized; import static org.openmetadata.catalog.exception.CatalogExceptionMessage.FIELD_NOT_TOKENIZED;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.macasaet.fernet.Key; import com.macasaet.fernet.Key;
@ -86,7 +86,7 @@ public class Fernet {
public String encrypt(@NonNull String secret) { public String encrypt(@NonNull String secret) {
if (secret.startsWith(FERNET_PREFIX)) { if (secret.startsWith(FERNET_PREFIX)) {
throw new IllegalArgumentException(isAlreadyTokenized()); throw new IllegalArgumentException(FIELD_ALREADY_TOKENIZED);
} }
if (isKeyDefined()) { if (isKeyDefined()) {
Key key = new Key(fernetKey.split(",")[0]); Key key = new Key(fernetKey.split(",")[0]);
@ -109,7 +109,7 @@ public class Fernet {
List<Key> keys = Arrays.stream(fernetKey.split(",")).map(Key::new).collect(Collectors.toList()); List<Key> keys = Arrays.stream(fernetKey.split(",")).map(Key::new).collect(Collectors.toList());
return token.validateAndDecrypt(keys, validator); return token.validateAndDecrypt(keys, validator);
} }
throw new IllegalArgumentException(isNotTokenized()); throw new IllegalArgumentException(FIELD_NOT_TOKENIZED);
} }
public static String decryptIfTokenized(String tokenized) { public static String decryptIfTokenized(String tokenized) {

View File

@ -760,7 +760,8 @@ public interface CollectionDAO {
default void deleteAllByPrefix(String fqnPrefix, int relation) { default void deleteAllByPrefix(String fqnPrefix, int relation) {
String prefix = String.format("%s%s%%", fqnPrefix, Entity.SEPARATOR); String prefix = String.format("%s%s%%", fqnPrefix, Entity.SEPARATOR);
String cond = String.format("WHERE (toFQN LIKE %s OR fromFQN LIKE %s) AND relation = %s)", prefix, relation); String cond =
String.format("WHERE (toFQN LIKE %s OR fromFQN LIKE %s) AND relation = %s)", prefix, prefix, relation);
deleteAllByPrefix(cond); deleteAllByPrefix(cond);
} }

View File

@ -13,7 +13,7 @@
package org.openmetadata.catalog.resources.feeds; package org.openmetadata.catalog.resources.feeds;
import static org.openmetadata.catalog.exception.CatalogExceptionMessage.invalidEntityLink; import static org.openmetadata.catalog.exception.CatalogExceptionMessage.INVALID_ENTITY_LINK;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -80,7 +80,7 @@ public final class MessageParser {
if (arrayFieldValue != null) { if (arrayFieldValue != null) {
if (arrayFieldName == null) { if (arrayFieldName == null) {
throw new IllegalArgumentException(invalidEntityLink()); throw new IllegalArgumentException(INVALID_ENTITY_LINK);
} }
this.linkType = LinkType.ENTITY_ARRAY_FIELD; this.linkType = LinkType.ENTITY_ARRAY_FIELD;
this.fullyQualifiedFieldType = String.format("%s.%s.member", entityType, fieldName); this.fullyQualifiedFieldType = String.format("%s.%s.member", entityType, fieldName);

View File

@ -14,10 +14,14 @@
package org.openmetadata.catalog.security; package org.openmetadata.catalog.security;
import java.security.Principal; import java.security.Principal;
import org.openmetadata.catalog.entity.teams.User;
import org.openmetadata.catalog.util.EntityUtil.Fields;
/** Holds context information of authenticated user, which will be used for authorization. */ /** Holds context information of authenticated user, which will be used for authorization. */
public final class AuthenticationContext { public final class AuthenticationContext {
private final Principal principal; private final Principal principal;
private User user;
private Fields userFields;
public AuthenticationContext(Principal principal) { public AuthenticationContext(Principal principal) {
this.principal = principal; this.principal = principal;
@ -31,4 +35,20 @@ public final class AuthenticationContext {
public String toString() { public String toString() {
return "AuthenticationContext{" + ", principal=" + principal + '}'; return "AuthenticationContext{" + ", principal=" + principal + '}';
} }
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public Fields getUserFields() {
return userFields;
}
public void setUserFields(Fields userFields) {
this.userFields = userFields;
}
} }

View File

@ -38,7 +38,6 @@ import org.openmetadata.catalog.security.policyevaluator.PolicyEvaluator;
import org.openmetadata.catalog.type.EntityReference; import org.openmetadata.catalog.type.EntityReference;
import org.openmetadata.catalog.type.Include; import org.openmetadata.catalog.type.Include;
import org.openmetadata.catalog.type.MetadataOperation; import org.openmetadata.catalog.type.MetadataOperation;
import org.openmetadata.catalog.util.EntityUtil;
import org.openmetadata.catalog.util.EntityUtil.Fields; import org.openmetadata.catalog.util.EntityUtil.Fields;
import org.openmetadata.catalog.util.RestUtil; import org.openmetadata.catalog.util.RestUtil;
@ -52,7 +51,8 @@ public class DefaultAuthorizer implements Authorizer {
private UserRepository userRepository; private UserRepository userRepository;
private PolicyEvaluator policyEvaluator; private PolicyEvaluator policyEvaluator;
private static final String FIELDS_PARAM = "roles,teams"; private Fields fieldsTeams;
private Fields fieldsRolesAndTeams;
@Override @Override
public void init(AuthorizerConfiguration config, Jdbi dbi) throws IOException { public void init(AuthorizerConfiguration config, Jdbi dbi) throws IOException {
@ -71,6 +71,8 @@ public class DefaultAuthorizer implements Authorizer {
mayBeAddAdminUsers(); mayBeAddAdminUsers();
mayBeAddBotUsers(); mayBeAddBotUsers();
this.policyEvaluator = PolicyEvaluator.getInstance(); this.policyEvaluator = PolicyEvaluator.getInstance();
this.fieldsTeams = userRepository.getFields("teams");
this.fieldsRolesAndTeams = userRepository.getFields("roles,teams");
} }
private void mayBeAddAdminUsers() { private void mayBeAddAdminUsers() {
@ -125,40 +127,29 @@ public class DefaultAuthorizer implements Authorizer {
@Override @Override
public boolean hasPermissions(AuthenticationContext ctx, EntityReference owner) { public boolean hasPermissions(AuthenticationContext ctx, EntityReference owner) {
validateAuthenticationContext(ctx);
// Since we have roles and operations. An Admin could enable updateDescription, tags, ownership permissions to // Since we have roles and operations. An Admin could enable updateDescription, tags, ownership permissions to
// a role and assign that to the users who can update the entities. With this we can look at the owner as a strict // a role and assign that to the users who can update the entities. With this we can look at the owner as a strict
// requirement to manage entities. So if owner is null we will not allow users to update entities. They can get a // requirement to manage entities. So if owner is null we will not allow users to update entities. They can get a
// role that allows them to update the entity. // role that allows them to update the entity.
if (owner == null) { return isOwner(ctx, owner);
return false;
}
try {
User user = getUserFromAuthenticationContext(ctx);
return isOwnedByUser(user, owner);
} catch (IOException | EntityNotFoundException | ParseException ex) {
return false;
}
} }
@Override @Override
public boolean hasPermissions( public boolean hasPermissions(
AuthenticationContext ctx, EntityReference entityReference, MetadataOperation operation) { AuthenticationContext ctx, EntityReference entityReference, MetadataOperation operation) {
validateAuthenticationContext(ctx); validate(ctx);
try { try {
User user = getUserFromAuthenticationContext(ctx); User user = getUserFromAuthenticationContext(ctx, fieldsRolesAndTeams);
if (entityReference == null) { if (entityReference == null) {
// In some cases there is no specific entity being acted upon. Eg: Lineage. // In some cases there is no specific entity being acted upon. Eg: Lineage.
return policyEvaluator.hasPermission(user, null, operation); return policyEvaluator.hasPermission(user, null, operation);
} }
Object entity = Object entity = Entity.getEntity(entityReference, new Fields(List.of("tags", FIELD_OWNER)), Include.NON_DELETED);
Entity.getEntity(entityReference, new EntityUtil.Fields(List.of("tags", FIELD_OWNER)), Include.NON_DELETED);
EntityReference owner = Entity.getEntityInterface(entity).getOwner(); EntityReference owner = Entity.getEntityInterface(entity).getOwner();
if (Entity.shouldHaveOwner(entityReference.getType()) && owner != null && isOwnedByUser(user, owner)) { if (Entity.shouldHaveOwner(entityReference.getType()) && owner != null && isOwnedByUser(user, owner)) {
// Entity is owned by the user. return true; // Entity is owned by the user.
return true;
} }
return policyEvaluator.hasPermission(user, entity, operation); return policyEvaluator.hasPermission(user, entity, operation);
} catch (IOException | EntityNotFoundException | ParseException ex) { } catch (IOException | EntityNotFoundException | ParseException ex) {
@ -168,7 +159,7 @@ public class DefaultAuthorizer implements Authorizer {
@Override @Override
public List<MetadataOperation> listPermissions(AuthenticationContext ctx, EntityReference entityReference) { public List<MetadataOperation> listPermissions(AuthenticationContext ctx, EntityReference entityReference) {
validateAuthenticationContext(ctx); validate(ctx);
if (isAdmin(ctx) || isBot(ctx)) { if (isAdmin(ctx) || isBot(ctx)) {
// Admins and bots have permissions to do all operations. // Admins and bots have permissions to do all operations.
@ -176,12 +167,11 @@ public class DefaultAuthorizer implements Authorizer {
} }
try { try {
User user = getUserFromAuthenticationContext(ctx); User user = getUserFromAuthenticationContext(ctx, fieldsRolesAndTeams);
if (entityReference == null) { if (entityReference == null) {
return policyEvaluator.getAllowedOperations(user, null); return policyEvaluator.getAllowedOperations(user, null);
} }
Object entity = Object entity = Entity.getEntity(entityReference, new Fields(List.of("tags", FIELD_OWNER)), Include.NON_DELETED);
Entity.getEntity(entityReference, new EntityUtil.Fields(List.of("tags", FIELD_OWNER)), Include.NON_DELETED);
EntityReference owner = Entity.getEntityInterface(entity).getOwner(); EntityReference owner = Entity.getEntityInterface(entity).getOwner();
if (owner == null || isOwnedByUser(user, owner)) { if (owner == null || isOwnedByUser(user, owner)) {
// Entity does not have an owner or is owned by the user - allow all operations. // Entity does not have an owner or is owned by the user - allow all operations.
@ -196,14 +186,12 @@ public class DefaultAuthorizer implements Authorizer {
/** Checks if the user is same as owner or part of the team that is the owner. */ /** Checks if the user is same as owner or part of the team that is the owner. */
private boolean isOwnedByUser(User user, EntityReference owner) { private boolean isOwnedByUser(User user, EntityReference owner) {
if (owner.getType().equals(Entity.USER) && owner.getName().equals(user.getName())) { if (owner.getType().equals(Entity.USER) && owner.getName().equals(user.getName())) {
// Owner is same as user. return true; // Owner is same as user.
return true;
} }
if (owner.getType().equals(Entity.TEAM)) { if (owner.getType().equals(Entity.TEAM)) {
for (EntityReference userTeam : user.getTeams()) { for (EntityReference userTeam : user.getTeams()) {
if (userTeam.getName().equals(owner.getName())) { if (userTeam.getName().equals(owner.getName())) {
// Owner is a team, and the user is part of this team. return true; // Owner is a team, and the user is part of this team.
return true;
} }
} }
} }
@ -212,14 +200,10 @@ public class DefaultAuthorizer implements Authorizer {
@Override @Override
public boolean isAdmin(AuthenticationContext ctx) { public boolean isAdmin(AuthenticationContext ctx) {
validateAuthenticationContext(ctx); validate(ctx);
String userName = SecurityUtil.getUserName(ctx);
try { try {
User user = userRepository.getByName(null, userName, Fields.EMPTY_FIELDS); User user = getUserFromAuthenticationContext(ctx, Fields.EMPTY_FIELDS);
if (user.getIsAdmin() == null) { return Boolean.TRUE.equals(user.getIsAdmin());
return false;
}
return user.getIsAdmin();
} catch (IOException | EntityNotFoundException | ParseException ex) { } catch (IOException | EntityNotFoundException | ParseException ex) {
return false; return false;
} }
@ -227,14 +211,10 @@ public class DefaultAuthorizer implements Authorizer {
@Override @Override
public boolean isBot(AuthenticationContext ctx) { public boolean isBot(AuthenticationContext ctx) {
validateAuthenticationContext(ctx); validate(ctx);
String userName = SecurityUtil.getUserName(ctx);
try { try {
User user = userRepository.getByName(null, userName, Fields.EMPTY_FIELDS); User user = getUserFromAuthenticationContext(ctx, Fields.EMPTY_FIELDS);
if (user.getIsBot() == null) { return Boolean.TRUE.equals(user.getIsBot());
return false;
}
return user.getIsBot();
} catch (IOException | EntityNotFoundException | ParseException ex) { } catch (IOException | EntityNotFoundException | ParseException ex) {
return false; return false;
} }
@ -242,29 +222,41 @@ public class DefaultAuthorizer implements Authorizer {
@Override @Override
public boolean isOwner(AuthenticationContext ctx, EntityReference owner) { public boolean isOwner(AuthenticationContext ctx, EntityReference owner) {
validateAuthenticationContext(ctx); if (owner == null) {
String userName = SecurityUtil.getUserName(ctx); return false;
}
validate(ctx);
try { try {
User user = userRepository.getByName(null, userName, Fields.EMPTY_FIELDS); User user = getUserFromAuthenticationContext(ctx, fieldsTeams);
if (owner == null) {
return false;
}
return isOwnedByUser(user, owner); return isOwnedByUser(user, owner);
} catch (IOException | EntityNotFoundException | ParseException ex) { } catch (IOException | EntityNotFoundException | ParseException ex) {
return false; return false;
} }
} }
private void validateAuthenticationContext(AuthenticationContext ctx) { private void validate(AuthenticationContext ctx) {
if (ctx == null || ctx.getPrincipal() == null) { if (ctx == null || ctx.getPrincipal() == null) {
throw new AuthenticationException("No principal in AuthenticationContext"); throw new AuthenticationException("No principal in AuthenticationContext");
} }
} }
private User getUserFromAuthenticationContext(AuthenticationContext ctx) throws IOException, ParseException { private User getUserFromAuthenticationContext(AuthenticationContext ctx, Fields fields)
throws IOException, ParseException {
if (ctx.getUser() != null) {
// If a requested field is not present in the user, then add it
for (String field : fields.getList()) {
if (!ctx.getUserFields().contains(field)) {
userRepository.setFields(ctx.getUser(), userRepository.getFields(field));
ctx.getUserFields().add(fields);
}
}
return ctx.getUser();
}
String userName = SecurityUtil.getUserName(ctx); String userName = SecurityUtil.getUserName(ctx);
EntityUtil.Fields fields = userRepository.getFields(FIELDS_PARAM); User user = userRepository.getByName(null, userName, fields);
return userRepository.getByName(null, userName, fields); ctx.setUser(user);
ctx.setUserFields(fields);
return user;
} }
private void addOrUpdateUser(User user) { private void addOrUpdateUser(User user) {

View File

@ -18,7 +18,6 @@ import static org.openmetadata.catalog.type.Include.ALL;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@ -327,21 +326,33 @@ public final class EntityUtil {
public Fields(List<String> allowedFields, String fieldsParam) { public Fields(List<String> allowedFields, String fieldsParam) {
if (fieldsParam == null || fieldsParam.isEmpty()) { if (fieldsParam == null || fieldsParam.isEmpty()) {
fieldList = Collections.emptyList(); fieldList = new ArrayList<>();
return; return;
} }
fieldList = Arrays.asList(fieldsParam.replace(" ", "").split(",")); fieldList = Arrays.asList(fieldsParam.replace(" ", "").split(","));
for (String field : fieldList) { for (String field : fieldList) {
if (!allowedFields.contains(field)) { if (!allowedFields.contains(field)) {
throw new IllegalArgumentException(CatalogExceptionMessage.invalidField(field)); throw new IllegalArgumentException(CatalogExceptionMessage.invalidField(field));
} }
} }
} }
@Override
public String toString() {
return fieldList.toString();
}
public void add(Fields fields) {
fieldList.addAll(fields.fieldList);
}
public boolean contains(String field) { public boolean contains(String field) {
return fieldList.contains(field); return fieldList.contains(field);
} }
public List<String> getList() {
return fieldList;
}
} }
public static List<UUID> getIDList(List<EntityReference> refList) { public static List<UUID> getIDList(List<EntityReference> refList) {