From c9640bcb5444e443bcfc54e174e8ef3be26ba3d4 Mon Sep 17 00:00:00 2001 From: sgiannopoulos Date: Fri, 5 Jan 2024 11:05:40 +0200 Subject: [PATCH] add dmp associated user query --- .../java/eu/eudat/audit/AuditableAction.java | 1 + .../eu/eudat/authorization/Permission.java | 2 +- .../eu/eudat/model/DmpAssociatedUser.java | 39 +++++++ .../builder/DmpAssociatedUserBuilder.java | 102 ++++++++++++++++++ .../censorship/DmpAssociatedUserCensor.java | 39 +++++++ .../main/java/eu/eudat/query/UserQuery.java | 37 ++++++- .../eudat/controllers/v2/UserController.java | 21 ++++ .../src/main/resources/config/permissions.yml | 10 ++ 8 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 dmp-backend/core/src/main/java/eu/eudat/model/DmpAssociatedUser.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/model/builder/DmpAssociatedUserBuilder.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/model/censorship/DmpAssociatedUserCensor.java diff --git a/dmp-backend/core/src/main/java/eu/eudat/audit/AuditableAction.java b/dmp-backend/core/src/main/java/eu/eudat/audit/AuditableAction.java index 4b70428be..2d15d5232 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/audit/AuditableAction.java +++ b/dmp-backend/core/src/main/java/eu/eudat/audit/AuditableAction.java @@ -86,6 +86,7 @@ public class AuditableAction { public static final EventId User_MergeConfirm = new EventId(11011, "User_MergeConfirm"); public static final EventId User_RemoveCredentialRequest = new EventId(11012, "User_RemoveCredentialRequest"); public static final EventId User_RemoveCredentialConfirm = new EventId(11013, "User_RemoveCredentialConfirm"); + public static final EventId User_DmpAssociatedQuery = new EventId(11014, "User_DmpAssociatedQuery"); public static final EventId Tenant_Query = new EventId(12000, "Tenant_Query"); public static final EventId Tenant_Lookup = new EventId(12001, "Tenant_Lookup"); diff --git a/dmp-backend/core/src/main/java/eu/eudat/authorization/Permission.java b/dmp-backend/core/src/main/java/eu/eudat/authorization/Permission.java index e0dcef0e2..1ddf0698d 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/authorization/Permission.java +++ b/dmp-backend/core/src/main/java/eu/eudat/authorization/Permission.java @@ -56,7 +56,7 @@ public final class Permission { public static String EditUser = "EditUser"; public static String DeleteUser = "DeleteUser"; public static String ExportUsers = "ExportUsers"; - + public static String BrowseDmpAssociatedUser = "BrowseDmpAssociatedUser"; //StorageFile diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/DmpAssociatedUser.java b/dmp-backend/core/src/main/java/eu/eudat/model/DmpAssociatedUser.java new file mode 100644 index 000000000..eb51408bb --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/DmpAssociatedUser.java @@ -0,0 +1,39 @@ +package eu.eudat.model; + +import java.util.UUID; + +public class DmpAssociatedUser { + + private UUID id; + public static final String _id = "id"; + + private String name; + public static final String _name = "name"; + + private String email; + public static final String _email = "email"; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/builder/DmpAssociatedUserBuilder.java b/dmp-backend/core/src/main/java/eu/eudat/model/builder/DmpAssociatedUserBuilder.java new file mode 100644 index 000000000..a5643cd6f --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/builder/DmpAssociatedUserBuilder.java @@ -0,0 +1,102 @@ +package eu.eudat.model.builder; + +import eu.eudat.authorization.AuthorizationFlags; +import eu.eudat.commons.JsonHandlingService; +import eu.eudat.commons.enums.ContactInfoType; +import eu.eudat.commons.types.user.AdditionalInfoEntity; +import eu.eudat.convention.ConventionService; +import eu.eudat.data.UserEntity; +import eu.eudat.model.*; +import eu.eudat.model.referencetypedefinition.ReferenceTypeSourceBaseConfiguration; +import eu.eudat.query.TenantUserQuery; +import eu.eudat.query.UserContactInfoQuery; +import eu.eudat.query.UserCredentialQuery; +import eu.eudat.query.UserRoleQuery; +import gr.cite.tools.data.builder.BuilderFactory; +import gr.cite.tools.data.query.QueryFactory; +import gr.cite.tools.exception.MyApplicationException; +import gr.cite.tools.fieldset.BaseFieldSet; +import gr.cite.tools.fieldset.FieldSet; +import gr.cite.tools.logging.DataLogEntry; +import gr.cite.tools.logging.LoggerService; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class DmpAssociatedUserBuilder extends BaseBuilder { + + private final QueryFactory queryFactory; + + private final BuilderFactory builderFactory; + + private EnumSet authorize = EnumSet.of(AuthorizationFlags.None); + + @Autowired + public DmpAssociatedUserBuilder(ConventionService conventionService, + QueryFactory queryFactory, + BuilderFactory builderFactory) { + super(conventionService, new LoggerService(LoggerFactory.getLogger(DmpAssociatedUserBuilder.class))); + this.queryFactory = queryFactory; + this.builderFactory = builderFactory; + } + + public DmpAssociatedUserBuilder authorize(EnumSet values) { + this.authorize = values; + return this; + } + + @Override + public List build(FieldSet fields, List data) throws MyApplicationException { + this.logger.debug("building for {} items requesting {} fields", Optional.ofNullable(data).map(List::size).orElse(0), Optional.ofNullable(fields).map(FieldSet::getFields).map(Set::size).orElse(0)); + this.logger.trace(new DataLogEntry("requested fields", fields)); + if (fields == null || data == null || fields.isEmpty()) + return new ArrayList<>(); + + List models = new ArrayList<>(); + + Map> contactsMap = this.collectUserContactInfos(new BaseFieldSet().ensure(UserContactInfo._value).ensure(UserContactInfo._type).ensure(UserContactInfo._ordinal), data); + + for (UserEntity d : data) { + DmpAssociatedUser m = new DmpAssociatedUser(); + if (fields.hasField(this.asIndexer(User._id))) m.setId(d.getId()); + if (fields.hasField(this.asIndexer(User._name))) m.setName(d.getName()); + if (contactsMap != null && contactsMap.containsKey(d.getId())){ + List contactInfos = contactsMap.get(d.getId()); + if (contactInfos != null) { + contactInfos.sort(Comparator.comparing(UserContactInfo::getOrdinal)); + m.setEmail(contactInfos.stream().filter(x -> ContactInfoType.Email.equals(x.getType())).map(UserContactInfo::getValue).findFirst().orElse(null)); + } + } + models.add(m); + } + this.logger.debug("build {} items", Optional.of(models).map(List::size).orElse(0)); + + return models; + } + + private Map> collectUserContactInfos(FieldSet fields, List data) throws MyApplicationException { + if (fields.isEmpty() || data.isEmpty()) return null; + this.logger.debug("checking related - {}", UserContactInfo.class.getSimpleName()); + + Map> itemMap; + FieldSet clone = new BaseFieldSet(fields.getFields()).ensure(this.asIndexer(UserContactInfo._user, User._id)); + UserContactInfoQuery query = this.queryFactory.query(UserContactInfoQuery.class).authorize(this.authorize).userIds(data.stream().map(UserEntity::getId).distinct().collect(Collectors.toList())); + itemMap = this.builderFactory.builder(UserContactInfoBuilder.class).authorize(this.authorize).asMasterKey(query, clone, x -> x.getUser().getId()); + + if (!fields.hasField(this.asIndexer(UserContactInfo._user, User._id))) { + itemMap.values().stream().flatMap(List::stream).filter(x -> x != null && x.getUser() != null).peek(x -> { + x.getUser().setId(null); + }); + } + + return itemMap; + } + +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/censorship/DmpAssociatedUserCensor.java b/dmp-backend/core/src/main/java/eu/eudat/model/censorship/DmpAssociatedUserCensor.java new file mode 100644 index 000000000..3305fccb7 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/censorship/DmpAssociatedUserCensor.java @@ -0,0 +1,39 @@ +package eu.eudat.model.censorship; + +import eu.eudat.authorization.OwnedResource; +import eu.eudat.authorization.Permission; +import eu.eudat.convention.ConventionService; +import gr.cite.commons.web.authz.service.AuthorizationService; +import gr.cite.tools.fieldset.FieldSet; +import gr.cite.tools.logging.DataLogEntry; +import gr.cite.tools.logging.LoggerService; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.UUID; + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class DmpAssociatedUserCensor extends BaseCensor { + + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(DmpAssociatedUserCensor.class)); + + protected final AuthorizationService authService; + + + public DmpAssociatedUserCensor(ConventionService conventionService, AuthorizationService authService) { + super(conventionService); + this.authService = authService; + } + + public void censor(FieldSet fields, UUID userId) { + logger.debug(new DataLogEntry("censoring fields", fields)); + if (fields == null || fields.isEmpty()) + return; + this.authService.authorizeAtLeastOneForce(userId != null ? List.of(new OwnedResource(userId)) : null, Permission.BrowseDmpAssociatedUser); + } + +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/query/UserQuery.java b/dmp-backend/core/src/main/java/eu/eudat/query/UserQuery.java index 415e8dc79..d7bdccb1f 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/query/UserQuery.java +++ b/dmp-backend/core/src/main/java/eu/eudat/query/UserQuery.java @@ -5,22 +5,23 @@ import eu.eudat.authorization.Permission; import eu.eudat.commons.enums.IsActive; import eu.eudat.commons.scope.user.UserScope; import eu.eudat.data.*; -import eu.eudat.model.DmpDescriptionTemplate; +import eu.eudat.model.Language; import eu.eudat.model.User; import eu.eudat.model.PublicUser; -import eu.eudat.model.UserRole; import eu.eudat.query.utils.BuildSubQueryInput; import eu.eudat.query.utils.QueryUtilsService; import gr.cite.commons.web.authz.service.AuthorizationService; import gr.cite.tools.data.query.FieldResolver; import gr.cite.tools.data.query.QueryBase; import gr.cite.tools.data.query.QueryContext; +import gr.cite.tools.exception.MyNotFoundException; import jakarta.persistence.Tuple; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Subquery; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Scope; +import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.stereotype.Component; import java.time.Instant; @@ -30,6 +31,7 @@ import java.util.*; @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class UserQuery extends QueryBase { private String like; + private Boolean dmpAssociated; private Collection ids; private Collection emails; private Collection excludedIds; @@ -117,6 +119,11 @@ public class UserQuery extends QueryBase { return this; } + public UserQuery dmpAssociated(Boolean dmpAssociated) { + this.dmpAssociated = dmpAssociated; + return this; + } + public UserQuery authorize(EnumSet values) { this.authorize = values; return this; @@ -186,7 +193,7 @@ public class UserQuery extends QueryBase { new BuildSubQueryInput.Builder<>(UserContactInfoQuery.class, UUID.class, queryContext) .keyPathFunc((subQueryRoot) -> subQueryRoot.get(UserContactInfoEntity._id)) .filterFunc((subQueryRoot, cb) -> { - CriteriaBuilder.In inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(UserContactInfoEntity._value)); + CriteriaBuilder.In inClause = cb.in(subQueryRoot.get(UserContactInfoEntity._value)); for (String item : this.emails) inClause.value(item); return inClause; @@ -199,6 +206,30 @@ public class UserQuery extends QueryBase { QueryContext subQuery = this.applySubQuery(this.userRoleQuery, queryContext, UUID.class, userRoleEntityRoot -> userRoleEntityRoot.get(UserRoleEntity._userId)); predicates.add(queryContext.CriteriaBuilder.in(queryContext.Root.get(UserEntity._id)).value(subQuery.Query)); } + if (this.dmpAssociated){ + UUID userId; + if (this.userScope.isSet()) userId = this.userScope.getUserIdSafe(); + else throw new MyNotFoundException("Only user scoped allowed"); + + Subquery dmpUserDmpQuery = queryUtilsService.buildSubQuery(new BuildSubQueryInput<>( + new BuildSubQueryInput.Builder<>(DmpUserQuery.class, UUID.class, queryContext) + .keyPathFunc((subQueryRoot) -> subQueryRoot.get(DmpUserEntity._dmpId)) + .filterFunc((subQueryRoot, cb) -> cb.and( + cb.equal(subQueryRoot.get(DmpUserEntity._userId), userId), + cb.equal(subQueryRoot.get(DmpUserEntity._isActive), IsActive.Active) + )) + )); + + Subquery dmpUserUserQuery = queryUtilsService.buildSubQuery(new BuildSubQueryInput<>( + new BuildSubQueryInput.Builder<>(DmpUserQuery.class, UUID.class, queryContext) + .keyPathFunc((subQueryRoot) -> subQueryRoot.get(DmpUserEntity._userId)) + .filterFunc((subQueryRoot, cb) -> cb.and( + cb.in(subQueryRoot.get(DmpUserEntity._dmpId)).value(dmpUserDmpQuery) , + cb.equal(subQueryRoot.get(DmpUserEntity._isActive), IsActive.Active) + )) + )); + predicates.add(queryContext.CriteriaBuilder.in(queryContext.Root.get(UserEntity._id)).value(dmpUserUserQuery)); + } if (!predicates.isEmpty()) { diff --git a/dmp-backend/web/src/main/java/eu/eudat/controllers/v2/UserController.java b/dmp-backend/web/src/main/java/eu/eudat/controllers/v2/UserController.java index 21298133b..2e02478c8 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/controllers/v2/UserController.java +++ b/dmp-backend/web/src/main/java/eu/eudat/controllers/v2/UserController.java @@ -3,12 +3,16 @@ package eu.eudat.controllers.v2; import com.fasterxml.jackson.core.JsonProcessingException; import eu.eudat.audit.AuditableAction; import eu.eudat.authorization.AuthorizationFlags; +import eu.eudat.commons.enums.IsActive; import eu.eudat.commons.scope.user.UserScope; import eu.eudat.commons.validation.ValidationFilterAnnotation; import eu.eudat.data.UserEntity; +import eu.eudat.model.DmpAssociatedUser; import eu.eudat.model.User; import eu.eudat.model.UserRole; +import eu.eudat.model.builder.DmpAssociatedUserBuilder; import eu.eudat.model.builder.UserBuilder; +import eu.eudat.model.censorship.DmpAssociatedUserCensor; import eu.eudat.model.censorship.UserCensor; import eu.eudat.model.persist.UserPersist; import eu.eudat.model.persist.UserRolePatchPersist; @@ -107,6 +111,23 @@ public class UserController { return new QueryResult<>(models, count); } + @PostMapping("dmp-associated/query") + public QueryResult queryDmpAssociated(@RequestBody UserLookup lookup) throws MyApplicationException, MyForbiddenException { + logger.debug("querying {}", User.class.getSimpleName()); + + this.censorFactory.censor(DmpAssociatedUserCensor.class).censor(lookup.getProject(), null); + + UserQuery query = lookup.enrich(this.queryFactory).dmpAssociated(true).isActive(IsActive.Active); + + List data = query.collectAs(lookup.getProject()); + List models = this.builderFactory.builder(DmpAssociatedUserBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).build(lookup.getProject(), data); + long count = (lookup.getMetadata() != null && lookup.getMetadata().getCountAll()) ? query.count() : models.size(); + + this.auditService.track(AuditableAction.User_DmpAssociatedQuery, "lookup", lookup); + + return new QueryResult<>(models, count); + } + @GetMapping("{id}") public User get(@PathVariable("id") UUID id, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException { logger.debug(new MapLogEntry("retrieving" + User.class.getSimpleName()).And("id", id).And("fields", fieldSet)); diff --git a/dmp-backend/web/src/main/resources/config/permissions.yml b/dmp-backend/web/src/main/resources/config/permissions.yml index 85f3a80d4..3d12b8c86 100644 --- a/dmp-backend/web/src/main/resources/config/permissions.yml +++ b/dmp-backend/web/src/main/resources/config/permissions.yml @@ -217,6 +217,16 @@ permissions: clients: [ ] allowAnonymous: false allowAuthenticated: false + BrowseDmpAssociatedUser: + roles: + - Admin + - DescriptionTemplateEditor + - Manager + - User + claims: [ ] + clients: [ ] + allowAnonymous: false + allowAuthenticated: false # DescriptionTemplateType BrowseDescriptionTemplateType: roles: