From 1b7ecf201ef2b915660ca2b5b89de5be26af04c3 Mon Sep 17 00:00:00 2001 From: Thomas Georgios Giannos Date: Wed, 13 Mar 2024 13:24:21 +0200 Subject: [PATCH 1/4] Getting annotation authors on query responses, ui update --- .../web/controllers/AnnotationController.java | 2 +- .../gr/cite/annotation/model/Annotation.java | 12 +++++ .../annotation/model/AnnotationAuthor.java | 31 ++++++++++++ .../model/builder/AnnotationBuilder.java | 47 ++++++++++++++++++- .../inbox/InboxRepositoryImpl.java | 40 ++++++++-------- .../annotation-dialog.component.html | 2 +- .../annotation-dialog.component.ts | 11 +++-- 7 files changed, 117 insertions(+), 28 deletions(-) create mode 100644 annotation-service/annotation/src/main/java/gr/cite/annotation/model/AnnotationAuthor.java diff --git a/annotation-service/annotation-web/src/main/java/gr/cite/annotation/web/controllers/AnnotationController.java b/annotation-service/annotation-web/src/main/java/gr/cite/annotation/web/controllers/AnnotationController.java index 88af9d2f1..f322246b3 100644 --- a/annotation-service/annotation-web/src/main/java/gr/cite/annotation/web/controllers/AnnotationController.java +++ b/annotation-service/annotation-web/src/main/java/gr/cite/annotation/web/controllers/AnnotationController.java @@ -72,7 +72,7 @@ public class AnnotationController { this.censorFactory.censor(AnnotationCensor.class).censor(lookup.getProject(), null); AnnotationQuery query = lookup.enrich(this.queryFactory); - List data = query.collectAs(lookup.getProject()); + List data = query.collect(); List models = this.builderFactory.builder(AnnotationBuilder.class).build(lookup.getProject(), data); long count = (lookup.getMetadata() != null && lookup.getMetadata().getCountAll()) ? query.count() : models.size(); diff --git a/annotation-service/annotation/src/main/java/gr/cite/annotation/model/Annotation.java b/annotation-service/annotation/src/main/java/gr/cite/annotation/model/Annotation.java index adec1d0a3..96a87126a 100644 --- a/annotation-service/annotation/src/main/java/gr/cite/annotation/model/Annotation.java +++ b/annotation-service/annotation/src/main/java/gr/cite/annotation/model/Annotation.java @@ -51,6 +51,10 @@ public class Annotation { public static final String _timeStamp = "timeStamp"; + private AnnotationAuthor author; + + public static final String _author = "author"; + private Instant createdAt; public static final String _createdAt = "createdAt"; @@ -111,6 +115,14 @@ public class Annotation { this.subjectId = subjectId; } + public AnnotationAuthor getAuthor() { + return author; + } + + public void setAuthor(AnnotationAuthor author) { + this.author = author; + } + public UUID getThreadId() { return threadId; } diff --git a/annotation-service/annotation/src/main/java/gr/cite/annotation/model/AnnotationAuthor.java b/annotation-service/annotation/src/main/java/gr/cite/annotation/model/AnnotationAuthor.java new file mode 100644 index 000000000..3670e51c5 --- /dev/null +++ b/annotation-service/annotation/src/main/java/gr/cite/annotation/model/AnnotationAuthor.java @@ -0,0 +1,31 @@ +package gr.cite.annotation.model; + +import java.util.UUID; + +public class AnnotationAuthor { + + private UUID id; + + public static final String _id = "id"; + + private String name; + + public static final String _name = "name"; + + 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; + } + +} diff --git a/annotation-service/annotation/src/main/java/gr/cite/annotation/model/builder/AnnotationBuilder.java b/annotation-service/annotation/src/main/java/gr/cite/annotation/model/builder/AnnotationBuilder.java index 14a677823..de611d3ce 100644 --- a/annotation-service/annotation/src/main/java/gr/cite/annotation/model/builder/AnnotationBuilder.java +++ b/annotation-service/annotation/src/main/java/gr/cite/annotation/model/builder/AnnotationBuilder.java @@ -4,7 +4,13 @@ import gr.cite.annotation.authorization.AuthorizationFlags; import gr.cite.annotation.convention.ConventionService; import gr.cite.annotation.data.AnnotationEntity; import gr.cite.annotation.model.Annotation; +import gr.cite.annotation.model.AnnotationAuthor; +import gr.cite.annotation.model.User; +import gr.cite.annotation.query.UserQuery; +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; @@ -14,15 +20,22 @@ 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 AnnotationBuilder extends BaseBuilder { + private final QueryFactory queryFactory; + + private final BuilderFactory builderFactory; + private EnumSet authorize = EnumSet.of(AuthorizationFlags.None); - public AnnotationBuilder(ConventionService conventionService) { + public AnnotationBuilder(ConventionService conventionService, QueryFactory queryFactory, BuilderFactory builderFactory) { super(conventionService, new LoggerService(LoggerFactory.getLogger(AnnotationBuilder.class))); + this.queryFactory = queryFactory; + this.builderFactory = builderFactory; } public AnnotationBuilder authorize(EnumSet values) { @@ -39,6 +52,9 @@ public class AnnotationBuilder extends BaseBuilder List models = new ArrayList<>(); + FieldSet authorFields = fields.extractPrefixed(this.asPrefix(Annotation._author)); + Map authorsMap = this.collectAuthors(authorFields, data); + if (data == null) return models; for (AnnotationEntity d : data) { @@ -55,6 +71,8 @@ public class AnnotationBuilder extends BaseBuilder m.setPayload(d.getPayload()); if (fields.hasField(this.asIndexer(Annotation._subjectId))) m.setSubjectId(d.getSubjectId()); + if (authorsMap != null && authorsMap.containsKey(d.getSubjectId())) + m.setAuthor(authorsMap.get(d.getSubjectId())); if (fields.hasField(this.asIndexer(Annotation._threadId))) m.setThreadId(d.getThreadId()); if (fields.hasField(this.asIndexer(Annotation._parentId))) @@ -75,4 +93,31 @@ public class AnnotationBuilder extends BaseBuilder return models; } + private Map collectAuthors(FieldSet fields, List data) { + if (fields.isEmpty() || data.isEmpty()) return null; + this.logger.debug("checking related - {}", User.class.getSimpleName()); + + Map itemMap = new HashMap<>(); + FieldSet clone = new BaseFieldSet(fields.getFields()).ensure(this.asIndexer(User._id), this.asIndexer(User._name)); + List userIds = data.stream() + .map(AnnotationEntity::getSubjectId) + .distinct() + .collect(Collectors.toList()); + UserQuery query = this.queryFactory.query(UserQuery.class).authorize(this.authorize).ids(userIds); + Map> users = this.builderFactory.builder(UserBuilder.class).authorize(this.authorize).asMasterKey(query, clone, User::getId); + + users.forEach((key, val) -> { + itemMap.put(key, this.authorFromUser(val.getFirst())); + }); + + return itemMap; + } + + private AnnotationAuthor authorFromUser(User user) { + AnnotationAuthor author = new AnnotationAuthor(); + author.setId(user.getId()); + author.setName(user.getName()); + return author; + } + } diff --git a/dmp-backend/core/src/main/java/eu/eudat/integrationevent/inbox/InboxRepositoryImpl.java b/dmp-backend/core/src/main/java/eu/eudat/integrationevent/inbox/InboxRepositoryImpl.java index 9db18331a..bfdcc132a 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/integrationevent/inbox/InboxRepositoryImpl.java +++ b/dmp-backend/core/src/main/java/eu/eudat/integrationevent/inbox/InboxRepositoryImpl.java @@ -51,7 +51,7 @@ public class InboxRepositoryImpl implements InboxRepository { EntityTransaction transaction = null; EntityManager entityManager = null; CandidateInfo candidate = null; - try (FakeRequestScope fakeRequestScope = new FakeRequestScope()) { + try (FakeRequestScope ignored = new FakeRequestScope()) { try { QueryFactory queryFactory = this.applicationContext.getBean(QueryFactory.class); EntityManagerFactory entityManagerFactory = this.applicationContext.getBean(EntityManagerFactory.class); @@ -108,9 +108,9 @@ public class InboxRepositoryImpl implements InboxRepository { public Boolean shouldOmit(CandidateInfo candidate, Function shouldOmit) { EntityTransaction transaction = null; EntityManager entityManager = null; - Boolean success = false; + boolean success = false; - try (FakeRequestScope fakeRequestScope = new FakeRequestScope()) { + try (FakeRequestScope ignored = new FakeRequestScope()) { try { EntityManagerFactory entityManagerFactory = this.applicationContext.getBean(EntityManagerFactory.class); @@ -123,7 +123,7 @@ public class InboxRepositoryImpl implements InboxRepository { QueueInboxEntity item = queryFactory.query(QueueInboxQuery.class).ids(candidate.getId()).first(); if (item == null) { - this.logger.warn("Could not lookup queue inbox {} to process. Continuing...", candidate.getId()); + logger.warn("Could not lookup queue inbox {} to process. Continuing...", candidate.getId()); } else { if (shouldOmit.apply(item)) { item.setStatus(QueueInboxStatus.OMITTED); @@ -136,7 +136,7 @@ public class InboxRepositoryImpl implements InboxRepository { transaction.commit(); } catch (Exception ex) { - this.logger.error("Problem executing purge. rolling back any db changes and marking error. Continuing...", ex); + logger.error("Problem executing purge. rolling back any db changes and marking error. Continuing...", ex); if (transaction != null) transaction.rollback(); success = false; @@ -145,7 +145,7 @@ public class InboxRepositoryImpl implements InboxRepository { entityManager.close(); } } catch (Exception ex) { - this.logger.error("Problem executing purge. rolling back any db changes and marking error. Continuing...", ex); + logger.error("Problem executing purge. rolling back any db changes and marking error. Continuing...", ex); } return success; } @@ -154,9 +154,9 @@ public class InboxRepositoryImpl implements InboxRepository { public boolean shouldWait(CandidateInfo candidate, Function itIsTimeFunc) { EntityTransaction transaction = null; EntityManager entityManager = null; - Boolean success = false; + boolean success = false; - try (FakeRequestScope fakeRequestScope = new FakeRequestScope()) { + try (FakeRequestScope ignored = new FakeRequestScope()) { try { EntityManagerFactory entityManagerFactory = this.applicationContext.getBean(EntityManagerFactory.class); @@ -183,7 +183,7 @@ public class InboxRepositoryImpl implements InboxRepository { } transaction.commit(); } catch (Exception ex) { - this.logger.error("Problem executing purge. rolling back any db changes and marking error. Continuing...", ex); + logger.error("Problem executing purge. rolling back any db changes and marking error. Continuing...", ex); if (transaction != null) transaction.rollback(); success = false; @@ -192,7 +192,7 @@ public class InboxRepositoryImpl implements InboxRepository { entityManager.close(); } } catch (Exception ex) { - this.logger.error("Problem executing purge. rolling back any db changes and marking error. Continuing...", ex); + logger.error("Problem executing purge. rolling back any db changes and marking error. Continuing...", ex); } return success; } @@ -201,9 +201,9 @@ public class InboxRepositoryImpl implements InboxRepository { public QueueInbox create(InboxCreatorParams inboxCreatorParams) { EntityTransaction transaction = null; EntityManager entityManager = null; - Boolean success = false; + boolean success = false; QueueInboxEntity queueMessage = null; - try (FakeRequestScope fakeRequestScope = new FakeRequestScope()) { + try (FakeRequestScope ignored = new FakeRequestScope()) { try { queueMessage = this.createQueueInboxEntity(inboxCreatorParams); EntityManagerFactory entityManagerFactory = this.applicationContext.getBean(EntityManagerFactory.class); @@ -218,7 +218,7 @@ public class InboxRepositoryImpl implements InboxRepository { transaction.commit(); } catch (Exception ex) { - this.logger.error("Problem executing purge. rolling back any db changes and marking error. Continuing...", ex); + logger.error("Problem executing purge. rolling back any db changes and marking error. Continuing...", ex); if (transaction != null) transaction.rollback(); success = false; @@ -227,7 +227,7 @@ public class InboxRepositoryImpl implements InboxRepository { entityManager.close(); } } catch (Exception ex) { - this.logger.error("Problem executing purge. rolling back any db changes and marking error. Continuing...", ex); + logger.error("Problem executing purge. rolling back any db changes and marking error. Continuing...", ex); } return queueMessage; } @@ -255,9 +255,9 @@ public class InboxRepositoryImpl implements InboxRepository { public Boolean emit(CandidateInfo candidateInfo) { EntityTransaction transaction = null; EntityManager entityManager = null; - Boolean success = false; + boolean success = false; - try (FakeRequestScope fakeRequestScope = new FakeRequestScope()) { + try (FakeRequestScope ignored = new FakeRequestScope()) { try { EntityManagerFactory entityManagerFactory = this.applicationContext.getBean(EntityManagerFactory.class); @@ -270,7 +270,7 @@ public class InboxRepositoryImpl implements InboxRepository { QueueInboxEntity queueInboxMessage = queryFactory.query(QueueInboxQuery.class).ids(candidateInfo.getId()).first(); if (queueInboxMessage == null) { - this.logger.warn("Could not lookup queue inbox {} to process. Continuing...", candidateInfo.getId()); + logger.warn("Could not lookup queue inbox {} to process. Continuing...", candidateInfo.getId()); } else { EventProcessingStatus status = this.processMessage(queueInboxMessage.getRoute(), queueInboxMessage.getMessageId().toString(), queueInboxMessage.getApplicationId(), queueInboxMessage.getMessage()); @@ -302,7 +302,7 @@ public class InboxRepositoryImpl implements InboxRepository { transaction.commit(); } catch (Exception ex) { - this.logger.error("Problem executing purge. rolling back any db changes and marking error. Continuing...", ex); + logger.error("Problem executing purge. rolling back any db changes and marking error. Continuing...", ex); if (transaction != null) transaction.rollback(); success = false; @@ -311,7 +311,7 @@ public class InboxRepositoryImpl implements InboxRepository { entityManager.close(); } } catch (Exception ex) { - this.logger.error("Problem executing purge. rolling back any db changes and marking error. Continuing...", ex); + logger.error("Problem executing purge. rolling back any db changes and marking error. Continuing...", ex); } return success; } @@ -339,7 +339,7 @@ public class InboxRepositoryImpl implements InboxRepository { } private Boolean RoutingKeyMatched(String routingKey, List topics) { - if (topics == null || topics.size() == 0) + if (topics == null || topics.isEmpty()) return false; return topics.stream().anyMatch(x -> x.equals(routingKey)); } diff --git a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.html b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.html index 53e4e48ef..babd1d0bb 100644 --- a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.html +++ b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.html @@ -46,7 +46,7 @@
{{thread.timeStamp | date:'EEEE, MMMM d, y, h:mm a'}}
{{getAnnotationProtectionType(thread)}}
{{getAnnotationProtectionType(thread)}}
-
{{thread.text}}
+
{{thread.payload}}
{{'ANNOTATION-DIALOG.THREADS.FROM-USER' | translate}} {{thread.author.name}}
diff --git a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.ts b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.ts index d723748e9..e141ab47e 100644 --- a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.ts +++ b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.ts @@ -123,12 +123,11 @@ export class AnnotationDialogComponent extends BaseComponent { lookup.project = { fields: [ nameof(x => x.id), - // nameof(x => x.threadId), - // nameof(x => x.threadId), - // nameof(x => x.timeStamp), - // nameof(x => x.author.name), + nameof(x => x.threadId), + nameof(x => x.timeStamp), + nameof(x => x.author.name), nameof(x => x.payload), - // nameof(x => x.protection), + nameof(x => x.protectionType), ] }; @@ -146,6 +145,8 @@ export class AnnotationDialogComponent extends BaseComponent { // this.annotationsPerThread[element.id.toString()] = data.items.filter(x => x.threadId === element.id && x.id !== element.id).sort((a1, a2) => new Date(a1.timeStamp).getTime() - new Date(a2.timeStamp).getTime()); // }); // // this.annotationsChanged.emit(this.threads); + + this.threads = data.items; }, error => this.onCallbackError(error), ); From cf5acb2656390d4181c0ded88f5f3edac3487e84 Mon Sep 17 00:00:00 2001 From: Thomas Georgios Giannos Date: Wed, 13 Mar 2024 15:01:56 +0200 Subject: [PATCH 2/4] Annotation dialog ui update --- .../annotation-dialog.component.html | 8 +++---- .../annotation-dialog.component.scss | 4 ++-- .../annotation-dialog.component.ts | 5 +++++ .../form-field-set.component.ts | 3 ++- dmp-frontend/src/assets/i18n/en.json | 22 +++++++++++++++++++ 5 files changed, 35 insertions(+), 7 deletions(-) diff --git a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.html b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.html index babd1d0bb..27fe99d60 100644 --- a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.html +++ b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.html @@ -10,12 +10,12 @@
- + {{threadFormGroup.get('text').getError('backendError')?.message}} {{'COMMONS.VALIDATION.REQUIRED' | translate}} - + {{'ANNOTATION-DIALOG.THREADS.PROTECTION.TITLE' | translate}} {{enumUtils.toAnnotationProtectionTypeString(enumValue)}} @@ -44,8 +44,8 @@
{{thread.timeStamp | date:'EEEE, MMMM d, y, h:mm a'}}
-
{{getAnnotationProtectionType(thread)}}
-
{{getAnnotationProtectionType(thread)}}
+
{{getAnnotationProtectionType(thread)}}
+
{{getAnnotationProtectionType(thread)}}
{{thread.payload}}
{{'ANNOTATION-DIALOG.THREADS.FROM-USER' | translate}} {{thread.author.name}} diff --git a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.scss b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.scss index d59032e36..628a0b7e1 100644 --- a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.scss +++ b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.scss @@ -3,9 +3,9 @@ } .form-container { - width: 33rem; + width: 100%; min-height: 14rem; - padding: 0.28rem 0.34rem 0.875rem 0.625rem; + padding: 1rem; } .logo { diff --git a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.ts b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.ts index e141ab47e..0ab4da412 100644 --- a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.ts +++ b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.ts @@ -191,6 +191,11 @@ export class AnnotationDialogComponent extends BaseComponent { } + getAnnotationProtectionType(thread: Annotation): string { + return this.enumUtils.toAnnotationProtectionTypeString(thread.protectionType); + } + + cancel() { this.dialogRef.close(); } diff --git a/dmp-frontend/src/app/ui/description/editor/description-form/components/form-field-set/form-field-set.component.ts b/dmp-frontend/src/app/ui/description/editor/description-form/components/form-field-set/form-field-set.component.ts index 68d735d1e..fff43d34b 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-form/components/form-field-set/form-field-set.component.ts +++ b/dmp-frontend/src/app/ui/description/editor/description-form/components/form-field-set/form-field-set.component.ts @@ -125,7 +125,8 @@ export class DescriptionFormFieldSetComponent extends BaseComponent { // showAnnotations(fieldSetId: string) { const dialogRef = this.dialog.open(AnnotationDialogComponent, { - width: '528px', + width: '40rem', + maxWidth: '90vw', data: { entityId: this.descriptionId, anchor: fieldSetId, diff --git a/dmp-frontend/src/assets/i18n/en.json b/dmp-frontend/src/assets/i18n/en.json index fc544de4f..51d493277 100644 --- a/dmp-frontend/src/assets/i18n/en.json +++ b/dmp-frontend/src/assets/i18n/en.json @@ -202,6 +202,9 @@ "NO-DATA-TO-DISPLAY": "No data to display" }, "COMMONS": { + "VALIDATION": { + "REQUIRED": "Required" + }, "LISTING-COMPONENT": { "SEARCH-FILTER-BTN": "Filter Results", "CLEAR-ALL-FILTERS": "clear all filters", @@ -373,6 +376,21 @@ "NOTIFICATIONS":"Notifications", "PREFILLING-SOURCES":"Prefilling Sources" }, + "ANNOTATION-DIALOG": { + "TITLE": "Comments", + "THREADS": { + "NEW-THREAD": "New comment", + "FROM-USER": "From", + "SEND": "Send comment", + "PROTECTION": { + "TITLE": "Visibility" + } + }, + "PROTECTION": { + "ENTITY-ACCESSORS": "Visible", + "PRIVATE": "Hidden" + } + }, "DESCRIPTION-TEMPLATE-EDITOR": { "TITLE": { "NEW": "New API Client", @@ -2241,6 +2259,10 @@ "INTERNAL-SOURCE": "Not bind with source" }, "TYPES": { + "ANNOTATION-PROTECTION-TYPE": { + "PRIVATE": "Hidden", + "ENTITY-ACCESSORS": "Visible" + }, "APP-ROLE": { "ADMIN": "Admin", "USER": "User", From cbfe4ec4f22f73c3662a831b8a6bc6c21e378f18 Mon Sep 17 00:00:00 2001 From: sgiannopoulos Date: Wed, 13 Mar 2024 17:04:17 +0200 Subject: [PATCH 3/4] authz changes --- .../AffiliatedAuthorizationRequirement.java | 40 ++++++++ .../authorization/AffiliatedResource.java | 31 ++++++ .../authorization/PermissionNameProvider.java | 40 ++++++++ .../AffiliationCacheOptions.java | 10 ++ .../AffiliationCacheService.java | 97 +++++++++++++++++++ .../AuthorizationContentResolver.java | 13 +++ .../AuthorizationContentResolverImpl.java | 77 +++++++++++++++ .../src/main/java/eu/eudat/model/Dmp.java | 11 +++ .../eu/eudat/model/builder/BaseBuilder.java | 29 ++++++ .../eu/eudat/model/builder/DmpBuilder.java | 15 ++- .../model/censorship/ReferenceCensor.java | 2 +- .../java/eu/eudat/query/DmpUserQuery.java | 2 + .../AffiliatedAuthorizationHandler.java | 69 +++++++++++++ ...stomPermissionAttributesConfiguration.java | 24 +++++ .../CustomPermissionAttributesProperties.java | 39 ++++++++ .../java/eu/eudat/authorization/DmpRole.java | 21 ++++ .../OwnedAuthorizationHandler.java | 5 + .../configurations/SecurityConfiguration.java | 14 ++- .../web/src/main/resources/config/cache.yml | 13 ++- .../src/main/resources/config/permissions.yml | 29 ++++-- 20 files changed, 565 insertions(+), 16 deletions(-) create mode 100644 dmp-backend/core/src/main/java/eu/eudat/authorization/AffiliatedAuthorizationRequirement.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/authorization/AffiliatedResource.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/authorization/PermissionNameProvider.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/authorization/authorizationcontentresolver/AffiliationCacheOptions.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/authorization/authorizationcontentresolver/AffiliationCacheService.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/authorization/authorizationcontentresolver/AuthorizationContentResolver.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/authorization/authorizationcontentresolver/AuthorizationContentResolverImpl.java create mode 100644 dmp-backend/web/src/main/java/eu/eudat/authorization/AffiliatedAuthorizationHandler.java create mode 100644 dmp-backend/web/src/main/java/eu/eudat/authorization/CustomPermissionAttributesConfiguration.java create mode 100644 dmp-backend/web/src/main/java/eu/eudat/authorization/CustomPermissionAttributesProperties.java create mode 100644 dmp-backend/web/src/main/java/eu/eudat/authorization/DmpRole.java diff --git a/dmp-backend/core/src/main/java/eu/eudat/authorization/AffiliatedAuthorizationRequirement.java b/dmp-backend/core/src/main/java/eu/eudat/authorization/AffiliatedAuthorizationRequirement.java new file mode 100644 index 000000000..dddcdde28 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/authorization/AffiliatedAuthorizationRequirement.java @@ -0,0 +1,40 @@ +package eu.eudat.authorization; + +import gr.cite.commons.web.authz.policy.AuthorizationRequirement; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class AffiliatedAuthorizationRequirement implements AuthorizationRequirement { + private final Set requiredPermissions; + private final boolean matchAll; + + public AffiliatedAuthorizationRequirement(Set requiredPermissions) { + this(false, requiredPermissions); + } + + public AffiliatedAuthorizationRequirement(String... requiredPermissions) { + this(false, requiredPermissions); + + } + + public AffiliatedAuthorizationRequirement(boolean matchAll, Set requiredPermissions) { + this.matchAll = matchAll; + this.requiredPermissions = requiredPermissions; + } + + public AffiliatedAuthorizationRequirement(boolean matchAll, String... requiredPermissions) { + this.requiredPermissions = new HashSet<>(); + this.matchAll = matchAll; + this.requiredPermissions.addAll(Arrays.stream(requiredPermissions).distinct().toList()); + } + + public Set getRequiredPermissions() { + return requiredPermissions; + } + + public boolean getMatchAll() { + return matchAll; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/authorization/AffiliatedResource.java b/dmp-backend/core/src/main/java/eu/eudat/authorization/AffiliatedResource.java new file mode 100644 index 000000000..1cd45fd45 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/authorization/AffiliatedResource.java @@ -0,0 +1,31 @@ +package eu.eudat.authorization; + +import eu.eudat.commons.enums.DmpUserRole; +import gr.cite.commons.web.authz.policy.AuthorizationResource; + +import java.util.HashSet; +import java.util.List; + +public class AffiliatedResource extends AuthorizationResource { + private HashSet dmpUserRoles; + + public AffiliatedResource() { + dmpUserRoles = new HashSet<>(); + } + + public AffiliatedResource(DmpUserRole dmpUserRole) { + this(List.of(dmpUserRole)); + } + + public AffiliatedResource(List dmpUserRoles) { + this.dmpUserRoles = new HashSet<>(dmpUserRoles); + } + + public HashSet getDmpUserRoles() { + return dmpUserRoles; + } + + public void setDmpUserRoles(HashSet dmpUserRoles) { + this.dmpUserRoles = dmpUserRoles; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/authorization/PermissionNameProvider.java b/dmp-backend/core/src/main/java/eu/eudat/authorization/PermissionNameProvider.java new file mode 100644 index 000000000..cb982f855 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/authorization/PermissionNameProvider.java @@ -0,0 +1,40 @@ +package eu.eudat.authorization; + +import eu.eudat.convention.ConventionService; +import eu.eudat.service.deposit.DepositServiceImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Service; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +@Service +@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) +public class PermissionNameProvider { + private static final Logger logger = LoggerFactory.getLogger(DepositServiceImpl.class); + private final List permissions; + + public PermissionNameProvider(ConventionService conventionService) { + this.permissions = new ArrayList<>(); + Class clazz = Permission.class; + for (Field f : clazz.getDeclaredFields()) { + if (Modifier.isStatic(f.getModifiers())) { + try { + Object value = f.get(null); + if (value != null && !conventionService.isNullOrEmpty((String)value)) this.permissions.add((String)value); + } catch (Exception e) { + logger.error("Can not load permission " + f.getName() + " " + e.getMessage()); + } + } + } + } + + public List getPermissions() { + return permissions; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/authorization/authorizationcontentresolver/AffiliationCacheOptions.java b/dmp-backend/core/src/main/java/eu/eudat/authorization/authorizationcontentresolver/AffiliationCacheOptions.java new file mode 100644 index 000000000..eaf62ca40 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/authorization/authorizationcontentresolver/AffiliationCacheOptions.java @@ -0,0 +1,10 @@ +package eu.eudat.authorization.authorizationcontentresolver; + +import gr.cite.tools.cache.CacheOptions; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "cache.affiliation") +public class AffiliationCacheOptions extends CacheOptions { +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/authorization/authorizationcontentresolver/AffiliationCacheService.java b/dmp-backend/core/src/main/java/eu/eudat/authorization/authorizationcontentresolver/AffiliationCacheService.java new file mode 100644 index 000000000..71f583cb5 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/authorization/authorizationcontentresolver/AffiliationCacheService.java @@ -0,0 +1,97 @@ +package eu.eudat.authorization.authorizationcontentresolver; + +import eu.eudat.authorization.AffiliatedResource; +import eu.eudat.convention.ConventionService; +import gr.cite.tools.cache.CacheService; +import gr.cite.tools.exception.MyApplicationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.UUID; + +@Service +public class AffiliationCacheService extends CacheService { + + public static class AffiliationCacheValue { + + public AffiliationCacheValue() { + } + + public AffiliationCacheValue(UUID userId, UUID entityId, String entityType, AffiliatedResource affiliatedResource) { + this.userId = userId; + this.entityId = entityId; + this.entityType = entityType; + this.affiliatedResource = affiliatedResource; + } + + private UUID userId; + private UUID entityId; + private String entityType; + private AffiliatedResource affiliatedResource; + + public UUID getUserId() { + return userId; + } + + public void setUserId(UUID userId) { + this.userId = userId; + } + + public UUID getEntityId() { + return entityId; + } + + public void setEntityId(UUID entityId) { + this.entityId = entityId; + } + + public String getEntityType() { + return entityType; + } + + public void setEntityType(String entityType) { + this.entityType = entityType; + } + + public AffiliatedResource getAffiliatedResource() { + return affiliatedResource; + } + + public void setAffiliatedResource(AffiliatedResource affiliatedResource) { + this.affiliatedResource = affiliatedResource; + } + } + + private final ConventionService conventionService; + @Autowired + public AffiliationCacheService(AffiliationCacheOptions options, ConventionService conventionService) { + super(options); + this.conventionService = conventionService; + } + + @Override + protected Class valueClass() { + return AffiliationCacheValue.class; + } + + @Override + public String keyOf(AffiliationCacheValue value) { + return this.buildKey(value.getUserId(), value.getEntityId(), value.getEntityType()); + } + + + public String buildKey(UUID userId, UUID entityId, String entityType) { + if (userId == null) throw new IllegalArgumentException("userId id is required"); + if (entityId == null) throw new IllegalArgumentException("entityId id is required"); + if (this.conventionService.isNullOrEmpty(entityType)) throw new IllegalArgumentException("entityType id is required"); + + HashMap keyParts = new HashMap<>(); + keyParts.put("$user$", userId.toString().replace("-", "").toLowerCase(Locale.ROOT)); + keyParts.put("$entity$", entityId.toString().replace("-", "").toLowerCase(Locale.ROOT)); + keyParts.put("$type$", entityType); + return this.generateKey(keyParts); + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/authorization/authorizationcontentresolver/AuthorizationContentResolver.java b/dmp-backend/core/src/main/java/eu/eudat/authorization/authorizationcontentresolver/AuthorizationContentResolver.java new file mode 100644 index 000000000..f44af1597 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/authorization/authorizationcontentresolver/AuthorizationContentResolver.java @@ -0,0 +1,13 @@ +package eu.eudat.authorization.authorizationcontentresolver; + +import eu.eudat.authorization.AffiliatedResource; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public interface AuthorizationContentResolver { + List getPermissionNames(); + + Map dmpAffiliation(List ids); +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/authorization/authorizationcontentresolver/AuthorizationContentResolverImpl.java b/dmp-backend/core/src/main/java/eu/eudat/authorization/authorizationcontentresolver/AuthorizationContentResolverImpl.java new file mode 100644 index 000000000..72c8bdbc3 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/authorization/authorizationcontentresolver/AuthorizationContentResolverImpl.java @@ -0,0 +1,77 @@ +package eu.eudat.authorization.authorizationcontentresolver; + +import eu.eudat.authorization.AffiliatedResource; +import eu.eudat.authorization.PermissionNameProvider; +import eu.eudat.commons.enums.IsActive; +import eu.eudat.commons.scope.user.UserScope; +import eu.eudat.data.DmpEntity; +import eu.eudat.data.DmpUserEntity; +import eu.eudat.model.DmpUser; +import eu.eudat.query.DmpUserQuery; +import gr.cite.tools.data.query.QueryFactory; +import gr.cite.tools.fieldset.BaseFieldSet; +import org.springframework.stereotype.Service; +import org.springframework.web.context.annotation.RequestScope; + +import java.util.*; + +@Service +@RequestScope +public class AuthorizationContentResolverImpl implements AuthorizationContentResolver { + private final QueryFactory queryFactory; + private final UserScope userScope; + private final AffiliationCacheService affiliationCacheService; + private final PermissionNameProvider permissionNameProvider; + public AuthorizationContentResolverImpl(QueryFactory queryFactory, UserScope userScope, AffiliationCacheService affiliationCacheService, PermissionNameProvider permissionNameProvider) { + this.queryFactory = queryFactory; + this.userScope = userScope; + this.affiliationCacheService = affiliationCacheService; + this.permissionNameProvider = permissionNameProvider; + } + + @Override + public List getPermissionNames() { + return permissionNameProvider.getPermissions(); + } + @Override + public Map dmpAffiliation(List ids){ + UUID userId = this.userScope.getUserIdSafe(); + Map affiliatedResources = new HashMap<>(); + for (UUID id : ids){ + affiliatedResources.put(id, new AffiliatedResource()); + } + if (userId == null || !userScope.isSet()) return affiliatedResources; + + List idsToResolve = this.getAffiliatedFromCache(ids, userId, affiliatedResources, DmpEntity.class.getSimpleName()); + if (idsToResolve.isEmpty()) return affiliatedResources; + + List dmpUsers = this.queryFactory.query(DmpUserQuery.class).dmpIds(ids).userIds(userId).isActives(IsActive.Active).collectAs(new BaseFieldSet().ensure(DmpUser._role).ensure(DmpUser._dmp)); + + for (DmpUserEntity dmpUser : dmpUsers){ + affiliatedResources.get(dmpUser.getDmpId()).getDmpUserRoles().add(dmpUser.getRole()); + } + + this.ensureAffiliatedInCache(idsToResolve, userId, affiliatedResources, DmpEntity.class.getSimpleName()); + return affiliatedResources; + } + + private List getAffiliatedFromCache(List ids, UUID userId, Map affiliatedResources, String entityType){ + List idsToResolve = new ArrayList<>(); + for (UUID id : ids){ + AffiliationCacheService.AffiliationCacheValue cacheValue = this.affiliationCacheService.lookup(this.affiliationCacheService.buildKey(userId, id, entityType)); + if (cacheValue != null) affiliatedResources.put(id, cacheValue.getAffiliatedResource()); + else idsToResolve.add(id); + } + return idsToResolve; + } + + private void ensureAffiliatedInCache(List idsToResolve, UUID userId, Map affiliatedResources, String entityType){ + for (UUID id : idsToResolve){ + AffiliatedResource affiliatedResource = affiliatedResources.getOrDefault(id, null); + if (affiliatedResource != null) { + AffiliationCacheService.AffiliationCacheValue cacheValue = new AffiliationCacheService.AffiliationCacheValue(userId, id, entityType, affiliatedResource); + this.affiliationCacheService.put(cacheValue); + } + } + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/Dmp.java b/dmp-backend/core/src/main/java/eu/eudat/model/Dmp.java index 1807bcc3a..9e351b515 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/model/Dmp.java +++ b/dmp-backend/core/src/main/java/eu/eudat/model/Dmp.java @@ -84,6 +84,9 @@ public class Dmp { private List entityDois; public static final String _entityDois = "entityDois"; + private List authorizationFlags; + public static final String _authorizationFlags = "authorizationFlags"; + public UUID getId() { return id; } @@ -275,4 +278,12 @@ public class Dmp { public void setEntityDois(List entityDois) { this.entityDois = entityDois; } + + public List getAuthorizationFlags() { + return authorizationFlags; + } + + public void setAuthorizationFlags(List authorizationFlags) { + this.authorizationFlags = authorizationFlags; + } } diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/builder/BaseBuilder.java b/dmp-backend/core/src/main/java/eu/eudat/model/builder/BaseBuilder.java index 445413f89..e3f74e602 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/model/builder/BaseBuilder.java +++ b/dmp-backend/core/src/main/java/eu/eudat/model/builder/BaseBuilder.java @@ -1,12 +1,18 @@ package eu.eudat.model.builder; +import eu.eudat.authorization.AffiliatedResource; +import eu.eudat.authorization.Permission; +import eu.eudat.authorization.PermissionNameProvider; import eu.eudat.convention.ConventionService; +import gr.cite.commons.web.authz.service.AuthorizationService; import gr.cite.tools.data.builder.Builder; import gr.cite.tools.data.query.QueryBase; import gr.cite.tools.exception.MyApplicationException; import gr.cite.tools.fieldset.FieldSet; import gr.cite.tools.logging.LoggerService; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.time.Instant; import java.util.*; import java.util.function.Function; @@ -92,5 +98,28 @@ public abstract class BaseBuilder implements Builder { return this.conventionService.asIndexer(names); } + + protected Set extractAuthorizationFlags(FieldSet fields, String propertyName, List permissionNames){ + if (fields == null) return new HashSet<>(); + if (permissionNames == null) return new HashSet<>(); + + FieldSet authorizationFlags = fields.extractPrefixed(this.asPrefix(propertyName)); + List permissions = new ArrayList<>(); + for (String fieldValue : authorizationFlags.getFields()) permissions.addAll(permissionNames.stream().filter(x-> x.equalsIgnoreCase(fieldValue)).toList()); + return new HashSet<>(permissions); + } + + protected List evaluateAuthorizationFlags(AuthorizationService authorizationService, Set authorizationFlags, AffiliatedResource affiliatedResource) { + List allowed = new ArrayList<>(); + if (authorizationFlags == null) return allowed; + if (authorizationService == null) return allowed; + + for (String permission : authorizationFlags) { + Boolean isAllowed = affiliatedResource == null ? authorizationService.authorize(permission) : authorizationService.authorizeAtLeastOne(List.of(affiliatedResource), permission); + if (isAllowed) allowed.add(permission); + } + return allowed; + } + } diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/builder/DmpBuilder.java b/dmp-backend/core/src/main/java/eu/eudat/model/builder/DmpBuilder.java index b1c713de4..9ab100b9e 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/model/builder/DmpBuilder.java +++ b/dmp-backend/core/src/main/java/eu/eudat/model/builder/DmpBuilder.java @@ -1,6 +1,9 @@ package eu.eudat.model.builder; +import eu.eudat.authorization.AffiliatedResource; import eu.eudat.authorization.AuthorizationFlags; +import eu.eudat.authorization.Permission; +import eu.eudat.authorization.authorizationcontentresolver.AuthorizationContentResolver; import eu.eudat.commons.JsonHandlingService; import eu.eudat.commons.enums.EntityType; import eu.eudat.commons.types.description.PropertyDefinitionEntity; @@ -12,6 +15,7 @@ import eu.eudat.model.*; import eu.eudat.model.builder.descriptionpropertiesdefinition.PropertyDefinitionBuilder; import eu.eudat.model.builder.dmpproperties.DmpPropertiesBuilder; import eu.eudat.query.*; +import gr.cite.commons.web.authz.service.AuthorizationService; import gr.cite.tools.data.builder.BuilderFactory; import gr.cite.tools.data.query.QueryFactory; import gr.cite.tools.exception.MyApplicationException; @@ -36,17 +40,21 @@ public class DmpBuilder extends BaseBuilder { private final BuilderFactory builderFactory; private final JsonHandlingService jsonHandlingService; + private final AuthorizationService authorizationService; + private final AuthorizationContentResolver authorizationContentResolver; private EnumSet authorize = EnumSet.of(AuthorizationFlags.None); @Autowired public DmpBuilder(ConventionService conventionService, QueryFactory queryFactory, - BuilderFactory builderFactory, JsonHandlingService jsonHandlingService) { + BuilderFactory builderFactory, JsonHandlingService jsonHandlingService, AuthorizationService authorizationService, AuthorizationContentResolver authorizationContentResolver) { super(conventionService, new LoggerService(LoggerFactory.getLogger(DmpBuilder.class))); this.queryFactory = queryFactory; this.builderFactory = builderFactory; this.jsonHandlingService = jsonHandlingService; + this.authorizationService = authorizationService; + this.authorizationContentResolver = authorizationContentResolver; } public DmpBuilder authorize(EnumSet values) { @@ -84,6 +92,10 @@ public class DmpBuilder extends BaseBuilder { FieldSet dmpDescriptionTemplatesFields = fields.extractPrefixed(this.asPrefix(Dmp._dmpDescriptionTemplates)); Map> dmpDescriptionTemplatesMap = this.collectDmpDescriptionTemplates(dmpDescriptionTemplatesFields, data); + Set authorizationFlags = this.extractAuthorizationFlags(fields, Dmp._authorizationFlags, this.authorizationContentResolver.getPermissionNames()); + + Map affiliatedResourceMap = authorizationFlags == null || authorizationFlags.isEmpty() ? null : this.authorizationContentResolver.dmpAffiliation(data.stream().map(DmpEntity::getId).collect(Collectors.toList())); + FieldSet propertiesFields = fields.extractPrefixed(this.asPrefix(Dmp._properties)); for (DmpEntity d : data) { Dmp m = new Dmp(); @@ -113,6 +125,7 @@ public class DmpBuilder extends BaseBuilder { DmpPropertiesEntity propertyDefinition = this.jsonHandlingService.fromJsonSafe(DmpPropertiesEntity.class, d.getProperties()); m.setProperties(this.builderFactory.builder(DmpPropertiesBuilder.class).authorize(this.authorize).build(propertiesFields, propertyDefinition)); } + if (authorizationFlags != null && !authorizationFlags.isEmpty()) m.setAuthorizationFlags(this.evaluateAuthorizationFlags(this.authorizationService, authorizationFlags, affiliatedResourceMap.getOrDefault(d.getId(), null))); models.add(m); } this.logger.debug("build {} items", Optional.of(models).map(List::size).orElse(0)); diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/censorship/ReferenceCensor.java b/dmp-backend/core/src/main/java/eu/eudat/model/censorship/ReferenceCensor.java index 859d38b2f..3aea1777b 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/model/censorship/ReferenceCensor.java +++ b/dmp-backend/core/src/main/java/eu/eudat/model/censorship/ReferenceCensor.java @@ -38,7 +38,7 @@ public class ReferenceCensor extends BaseCensor { if (fields == null || fields.isEmpty()) return; - this.authService.authorizeForce(Permission.BrowseReference); + this.authService.authorizeForce(Permission.BrowseReference, Permission.DeferredAffiliation); FieldSet definitionFields = fields.extractPrefixed(this.asIndexerPrefix(Reference._definition)); this.censorFactory.censor(DefinitionCensor.class).censor(definitionFields, userId); FieldSet dmpReferencesFields = fields.extractPrefixed(this.asIndexerPrefix(Reference._dmpReferences)); diff --git a/dmp-backend/core/src/main/java/eu/eudat/query/DmpUserQuery.java b/dmp-backend/core/src/main/java/eu/eudat/query/DmpUserQuery.java index 3c90783ad..777855b1f 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/query/DmpUserQuery.java +++ b/dmp-backend/core/src/main/java/eu/eudat/query/DmpUserQuery.java @@ -259,6 +259,8 @@ public class DmpUserQuery extends QueryBase { else if (item.match(DmpUser._updatedAt)) return DmpUserEntity._updatedAt; else if (item.match(DmpUser._isActive)) return DmpUserEntity._isActive; else if (item.match(DmpUser._hash)) return DmpUserEntity._updatedAt; + else if (item.match(DmpUser._dmp)) return DmpUserEntity._dmpId; + else if (item.match(DmpUser._user)) return DmpUserEntity._userId; else return null; } diff --git a/dmp-backend/web/src/main/java/eu/eudat/authorization/AffiliatedAuthorizationHandler.java b/dmp-backend/web/src/main/java/eu/eudat/authorization/AffiliatedAuthorizationHandler.java new file mode 100644 index 000000000..2307ab5f8 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/authorization/AffiliatedAuthorizationHandler.java @@ -0,0 +1,69 @@ +package eu.eudat.authorization; + +import eu.eudat.commons.enums.DmpUserRole; +import gr.cite.commons.web.authz.handler.AuthorizationHandler; +import gr.cite.commons.web.authz.handler.AuthorizationHandlerContext; +import gr.cite.commons.web.authz.policy.AuthorizationRequirement; +import gr.cite.commons.web.oidc.principal.MyPrincipal; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashSet; + +@Component("affiliatedAuthorizationHandler") +public class AffiliatedAuthorizationHandler extends AuthorizationHandler { + + private final CustomPermissionAttributesConfiguration myConfiguration; + + @Autowired + public AffiliatedAuthorizationHandler(CustomPermissionAttributesConfiguration myConfiguration) { + this.myConfiguration = myConfiguration; + } + + @Override + public int handleRequirement(AuthorizationHandlerContext context, Object resource, AuthorizationRequirement requirement) { + AffiliatedAuthorizationRequirement req = (AffiliatedAuthorizationRequirement) requirement; + if (req.getRequiredPermissions() == null) + return ACCESS_NOT_DETERMINED; + + AffiliatedResource rs = (AffiliatedResource) resource; + + boolean isAuthenticated = ((MyPrincipal) context.getPrincipal()).isAuthenticated(); + if (!isAuthenticated) + return ACCESS_NOT_DETERMINED; + + if (myConfiguration.getMyPolicies() == null) + return ACCESS_NOT_DETERMINED; + + int hits = 0; + HashSet roles = rs != null && rs.getDmpUserRoles() != null ? rs.getDmpUserRoles() : null; + + for (String permission : req.getRequiredPermissions()) { + CustomPermissionAttributesProperties.MyPermission policy = myConfiguration.getMyPolicies().get(permission); + boolean hasPermission = policy != null && hasPermission(policy.getDmp(), roles); + if (hasPermission) hits += 1; + } + if ((req.getMatchAll() && req.getRequiredPermissions().size() == hits) || (!req.getMatchAll() && hits > 0)) + return ACCESS_GRANTED; + + return ACCESS_NOT_DETERMINED; + } + + private Boolean hasPermission(DmpRole dmpRole, HashSet roles) { + if (roles == null) + return Boolean.FALSE; + if (dmpRole == null || dmpRole.getRoles() == null) + return Boolean.FALSE; + for (DmpUserRole role : dmpRole.getRoles()) { + if (roles.contains(role)) + return Boolean.TRUE; + } + return Boolean.FALSE; + } + + @Override + public Class supporting() { + return AffiliatedAuthorizationRequirement.class; + } + +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/authorization/CustomPermissionAttributesConfiguration.java b/dmp-backend/web/src/main/java/eu/eudat/authorization/CustomPermissionAttributesConfiguration.java new file mode 100644 index 000000000..8190c54d4 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/authorization/CustomPermissionAttributesConfiguration.java @@ -0,0 +1,24 @@ +package eu.eudat.authorization; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; + +@Configuration +@EnableConfigurationProperties(CustomPermissionAttributesProperties.class) +public class CustomPermissionAttributesConfiguration { + + private final CustomPermissionAttributesProperties properties; + + @Autowired + public CustomPermissionAttributesConfiguration(CustomPermissionAttributesProperties properties) { + this.properties = properties; + } + + public HashMap getMyPolicies() { + return properties.getPolicies(); + } + +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/authorization/CustomPermissionAttributesProperties.java b/dmp-backend/web/src/main/java/eu/eudat/authorization/CustomPermissionAttributesProperties.java new file mode 100644 index 000000000..d73bccd79 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/authorization/CustomPermissionAttributesProperties.java @@ -0,0 +1,39 @@ +package eu.eudat.authorization; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.ConstructorBinding; + +import java.util.HashMap; + +@ConfigurationProperties(prefix = "permissions") +@ConditionalOnProperty(prefix = "permissions", name = "enabled", havingValue = "true") +public class CustomPermissionAttributesProperties { + + private final HashMap policies; + + @ConstructorBinding + public CustomPermissionAttributesProperties(HashMap policies) { + this.policies = policies; + } + + public HashMap getPolicies() { + return policies; + } + + public static class MyPermission { + + private final DmpRole dmp; + + @ConstructorBinding + public MyPermission(DmpRole dmp) { + this.dmp = dmp; + } + + + public DmpRole getDmp() { + return dmp; + } + } + +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/authorization/DmpRole.java b/dmp-backend/web/src/main/java/eu/eudat/authorization/DmpRole.java new file mode 100644 index 000000000..7389398ce --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/authorization/DmpRole.java @@ -0,0 +1,21 @@ +package eu.eudat.authorization; + + +import eu.eudat.commons.enums.DmpUserRole; +import org.springframework.boot.context.properties.bind.ConstructorBinding; + +import java.util.Set; + +public class DmpRole { + private final Set roles; + + @ConstructorBinding + public DmpRole(Set roles) { + this.roles = roles; + } + + public Set getRoles() { + return roles; + } + +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/authorization/OwnedAuthorizationHandler.java b/dmp-backend/web/src/main/java/eu/eudat/authorization/OwnedAuthorizationHandler.java index f6ef8f821..5eac83967 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/authorization/OwnedAuthorizationHandler.java +++ b/dmp-backend/web/src/main/java/eu/eudat/authorization/OwnedAuthorizationHandler.java @@ -1,5 +1,6 @@ package eu.eudat.authorization; +import eu.eudat.commons.enums.DmpUserRole; import eu.eudat.commons.scope.user.UserScope; import gr.cite.commons.web.authz.handler.AuthorizationHandler; import gr.cite.commons.web.authz.handler.AuthorizationHandlerContext; @@ -8,6 +9,9 @@ import gr.cite.commons.web.oidc.principal.MyPrincipal; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.util.HashSet; +import java.util.List; + @Component("ownedAuthorizationHandler") public class OwnedAuthorizationHandler extends AuthorizationHandler { @@ -40,3 +44,4 @@ public class OwnedAuthorizationHandler extends AuthorizationHandler authenticationManagerResolver; private final Filter apiKeyFilter; private final OwnedAuthorizationHandler ownedAuthorizationHandler; + private final AffiliatedAuthorizationHandler affiliatedAuthorizationHandler; @Autowired public SecurityConfiguration(WebSecurityProperties webSecurityProperties, @Qualifier("tokenAuthenticationResolver") AuthenticationManagerResolver authenticationManagerResolver, @Qualifier("apiKeyFilter") Filter apiKeyFilter, - @Qualifier("ownedAuthorizationHandler") OwnedAuthorizationHandler ownedAuthorizationHandler) { + @Qualifier("ownedAuthorizationHandler") OwnedAuthorizationHandler ownedAuthorizationHandler, + @Qualifier("affiliatedAuthorizationHandler") AffiliatedAuthorizationHandler affiliatedAuthorizationHandler) { this.webSecurityProperties = webSecurityProperties; this.authenticationManagerResolver = authenticationManagerResolver; this.apiKeyFilter = apiKeyFilter; this.ownedAuthorizationHandler = ownedAuthorizationHandler; + this.affiliatedAuthorizationHandler = affiliatedAuthorizationHandler; } @Bean @@ -80,7 +81,7 @@ public class SecurityConfiguration { //If not set / set to null, only the default authorization handlers will be used @Override public List> addCustomHandlers() { - return List.of( ownedAuthorizationHandler); + return List.of(affiliatedAuthorizationHandler, ownedAuthorizationHandler); } //Here you can register your custom authorization requirements (if any) @@ -116,6 +117,9 @@ public class SecurityConfiguration { if (OwnedResource.class.equals(type)) { return new OwnedAuthorizationRequirement(); } + if (AffiliatedResource.class.equals(type)) { + return new AffiliatedAuthorizationRequirement(matchAll, permissions); + } throw new IllegalArgumentException("resource"); } }; diff --git a/dmp-backend/web/src/main/resources/config/cache.yml b/dmp-backend/web/src/main/resources/config/cache.yml index c717f36b0..8fbdce800 100644 --- a/dmp-backend/web/src/main/resources/config/cache.yml +++ b/dmp-backend/web/src/main/resources/config/cache.yml @@ -34,6 +34,14 @@ cache: expireAfterWriteMinutes: 10 expireAfterAccessMinutes: 10 refreshAfterWriteMinutes: 10 + - names: [ "affiliation" ] + allowNullValues: true + initialCapacity: 100 + maximumSize: 500 + enableRecordStats: false + expireAfterWriteMinutes: 1 + expireAfterAccessMinutes: 1 + refreshAfterWriteMinutes: 1 - names: [ "dashboardStatisticsByUserId" ] allowNullValues: true initialCapacity: 100 @@ -79,4 +87,7 @@ cache: keyPattern: base:v0 token-exchange-key: name: tokenExchangeKey - keyPattern: resolve_$keyhash$:v0 \ No newline at end of file + keyPattern: resolve_$keyhash$:v0 + affiliation: + name: affiliation + keyPattern: affiliation_$entity$_$user$_$type$:v0 \ No newline at end of file diff --git a/dmp-backend/web/src/main/resources/config/permissions.yml b/dmp-backend/web/src/main/resources/config/permissions.yml index 1e3f21f34..db2d3d94a 100644 --- a/dmp-backend/web/src/main/resources/config/permissions.yml +++ b/dmp-backend/web/src/main/resources/config/permissions.yml @@ -206,9 +206,6 @@ permissions: BrowseDmpAssociatedUser: roles: - Admin - - DescriptionTemplateEditor - - Manager - - User claims: [ ] clients: [ ] allowAnonymous: false @@ -217,6 +214,9 @@ permissions: BrowseDescriptionTemplateType: roles: - Admin + - User + - Manager + - DescriptionTemplateEditor clients: [ ] allowAnonymous: false allowAuthenticated: false @@ -321,6 +321,12 @@ permissions: EditDmp: roles: - Admin + dmp: + roles: + - Owner + - User + - DescriptionContributor + - Reviewer clients: [ ] allowAnonymous: false allowAuthenticated: false @@ -463,7 +469,6 @@ permissions: BrowseReference: roles: - Admin - - User clients: [ ] allowAnonymous: false allowAuthenticated: false @@ -527,6 +532,9 @@ permissions: BrowseSupportiveMaterial: roles: - Admin + - User + - Manager + - DescriptionTemplateEditor clients: [ ] allowAnonymous: yes allowAuthenticated: yes @@ -548,6 +556,9 @@ permissions: BrowseReferenceType: roles: - Admin + - User + - Manager + - DescriptionTemplateEditor clients: [ ] allowAnonymous: false allowAuthenticated: false @@ -691,25 +702,24 @@ permissions: # Lock Permissions BrowseLock: roles: - - User - Admin clients: [ ] allowAnonymous: false allowAuthenticated: false EditLock: roles: - - User + - Admin clients: [ ] allowAnonymous: false allowAuthenticated: false DeleteLock: roles: - - User + - Admin claims: [ ] clients: [ ] allowAnonymous: false allowAuthenticated: false - # Lock Permissions + # Contact Permissions SendContactSupport: roles: [] clients: [ ] @@ -740,6 +750,9 @@ permissions: BrowsePrefillingSource: roles: - Admin + - DescriptionTemplateEditor + - Manager + - User clients: [ ] allowAnonymous: false allowAuthenticated: false From db68cd32e11bb9706b9719e1908eb46bc63d9971 Mon Sep 17 00:00:00 2001 From: sgiannopoulos Date: Wed, 13 Mar 2024 17:04:42 +0200 Subject: [PATCH 4/4] migration fixes --- .../eudat/migration/FunderMigrationService.java | 2 +- .../eudat/migration/GrantMigrationService.java | 5 +++-- .../eudat/migration/ProjectMigrationService.java | 4 ++-- .../migration/StorageFileMigrationService.java | 16 ++++++++-------- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/FunderMigrationService.java b/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/FunderMigrationService.java index 0f5b55ccc..be7373323 100644 --- a/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/FunderMigrationService.java +++ b/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/FunderMigrationService.java @@ -56,7 +56,7 @@ public class FunderMigrationService { ReferenceEntity data = new ReferenceEntity(); data.setId(item.getId()); data.setLabel(item.getLabel()); - data.setIsActive(Funder.Status.fromInteger(item.getStatus()).equals(Funder.Status.ACTIVE) ? IsActive.Active : IsActive.Inactive); + data.setIsActive(IsActive.Active); data.setTypeId(ReferenceTypeIds.Funder); data.setCreatedAt(item.getCreated().toInstant()); data.setUpdatedAt(item.getModified().toInstant()); diff --git a/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/GrantMigrationService.java b/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/GrantMigrationService.java index 704339566..5025b6667 100644 --- a/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/GrantMigrationService.java +++ b/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/GrantMigrationService.java @@ -21,7 +21,8 @@ import java.util.List; import java.util.Locale; @Service -public class GrantMigrationService { +public class +GrantMigrationService { private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(GrantMigrationService.class)); private static final int PageSize = 500; @@ -63,7 +64,7 @@ public class GrantMigrationService { data.setLabel(item.getLabel()); data.setDescription(item.getDescription()); data.setAbbreviation(item.getAbbreviation()); - data.setIsActive(Grant.Status.fromInteger(item.getStatus()).equals(Grant.Status.ACTIVE) ? IsActive.Active : IsActive.Inactive); + data.setIsActive(IsActive.Active); data.setTypeId(ReferenceTypeIds.Grants); data.setCreatedAt(item.getCreated().toInstant()); data.setUpdatedAt(item.getModified().toInstant()); diff --git a/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/ProjectMigrationService.java b/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/ProjectMigrationService.java index 10b1e2c71..0ac989cf9 100644 --- a/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/ProjectMigrationService.java +++ b/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/ProjectMigrationService.java @@ -64,8 +64,8 @@ public class ProjectMigrationService { data.setLabel(item.getLabel()); data.setAbbreviation(item.getAbbreviation()); data.setDescription(item.getDescription()); - data.setIsActive(Project.Status.ACTIVE.equals(Project.Status.fromInteger(item.getStatus())) ? IsActive.Active : IsActive.Inactive); - data.setTypeId(ReferenceTypeIds.DataRepositories); + data.setIsActive(IsActive.Active); + data.setTypeId(ReferenceTypeIds.Project); data.setCreatedAt(item.getCreated().toInstant()); data.setUpdatedAt(item.getModified().toInstant()); if (item.getCreationUser() != null) data.setCreatedById(item.getCreationUser().getId()); diff --git a/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/StorageFileMigrationService.java b/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/StorageFileMigrationService.java index 632063ef2..e1aa920f1 100644 --- a/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/StorageFileMigrationService.java +++ b/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/StorageFileMigrationService.java @@ -63,9 +63,9 @@ public class StorageFileMigrationService { logger.debug("Migrate FileUpload " + page * PageSize + " of " + total); for (FileUpload item : items) { entityManager.detach(item); - File file = new File(this.environment.getProperty("file.storage") + item.getId().toString()); - if (!file.exists()) file = new File(this.environment.getProperty("temp.temp") + item.getId().toString()); - if (!file.exists()) throw new MyApplicationException("Storage file not exist " + item.getId().toString()); +// File file = new File(this.environment.getProperty("file.storage") + item.getId().toString()); +// if (!file.exists()) file = new File(this.environment.getProperty("temp.temp") + item.getId().toString()); +// if (!file.exists()) throw new MyApplicationException("Storage file not exist " + item.getId().toString()); StorageFileEntity data = new StorageFileEntity(); data.setId(item.getId()); @@ -80,11 +80,11 @@ public class StorageFileMigrationService { data.setPurgedAt(null); if (item.getCreator() == null) data.setOwnerId(item.getCreator().getId()); - File destinationFile = new File(Paths.get(this.environment.getProperty("file.mainstorage"), data.getFileRef()).toString()); - boolean fileCopied = FileCopyUtils.copy(file, destinationFile) > 0; - if (!fileCopied) throw new MyApplicationException("Storage file not copied " + data.getId().toString()); - - filesToDelete.add(file); +// File destinationFile = new File(Paths.get(this.environment.getProperty("file.mainstorage"), data.getFileRef()).toString()); +// boolean fileCopied = FileCopyUtils.copy(file, destinationFile) > 0; +// if (!fileCopied) throw new MyApplicationException("Storage file not copied " + data.getId().toString()); +// +// filesToDelete.add(file); this.entityManager.persist(data); }