From 7bd0397f29766132d0bd3e2b00a91d4d14db89b6 Mon Sep 17 00:00:00 2001 From: "Khaled Y.M." Date: Thu, 19 Jun 2025 14:00:48 +0100 Subject: [PATCH] Grandular account and app updates --- .../HibernateAccountsRepository.java | 95 +++++++++++++++++- .../persistence/HibernateAppsRepository.java | 37 ++++++- .../HibernateAccountsRepositoryTest.java | 97 +++++++++++++++++-- .../HibernateAppsRepositoryTest.java | 74 ++++++++++++-- .../HibernatePermissionsRepositoryTest.java | 5 +- .../memory/dal/MockAccountsRepository.java | 92 +++++++++++++++++- .../dal/MockApplicationsRepository.java | 40 ++++++++ .../common/facade/ReactiveMongoFacade.java | 13 +++ .../persistence/MongoAccountsRepository.java | 96 +++++++++++++++++- .../MongoApplicationsRepository.java | 48 +++++++++ .../MongoAccountsRepositoryTest.java | 74 ++++++++++++-- 11 files changed, 632 insertions(+), 39 deletions(-) diff --git a/dal/hibernate-dal/hibernate-persistence/src/main/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernateAccountsRepository.java b/dal/hibernate-dal/hibernate-persistence/src/main/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernateAccountsRepository.java index 60bf58b..55a8222 100644 --- a/dal/hibernate-dal/hibernate-persistence/src/main/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernateAccountsRepository.java +++ b/dal/hibernate-dal/hibernate-persistence/src/main/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernateAccountsRepository.java @@ -4,6 +4,8 @@ import com.nexblocks.authguard.dal.hibernate.common.AbstractHibernateRepository; import com.nexblocks.authguard.dal.hibernate.common.ReactiveQueryExecutor; import com.nexblocks.authguard.dal.model.AccountDO; +import com.nexblocks.authguard.dal.model.PasswordDO; +import com.nexblocks.authguard.dal.model.PermissionDO; import com.nexblocks.authguard.dal.model.UserIdentifierDO; import com.nexblocks.authguard.dal.persistence.AccountsRepository; import com.nexblocks.authguard.service.exceptions.ServiceConflictException; @@ -33,14 +35,12 @@ public class HibernateAccountsRepository extends AbstractHibernateRepository> findByIdentifier(final String identifier, final DOMAIN_FIELD, domain); } + @Override + public Uni addAccountPermissions(final AccountDO account, final List permissions) { + String sql = "INSERT INTO account_permissions (AccountDO_id, permissions_id) " + + "VALUES (:accountId, :permissionId)"; + + return queryExecutor.getSessionFactory().withTransaction(session -> { + List> insertUnis = permissions.stream() + .map(permission -> session.createNativeQuery(sql) + .setParameter("accountId", account.getId()) + .setParameter("permissionId", permission.getId()) + .executeUpdate() + .replaceWithVoid() + ) + .toList(); + + return Uni.combine().all().unis(insertUnis) + .with(list -> account); + }); + } + + @Override + public Uni removeAccountPermissions(final AccountDO account, final List permissions) { + String sql = "DELETE FROM account_permissions " + + "WHERE AccountDO_id = :accountId and permissions_id in :permissionIds"; + + return queryExecutor.getSessionFactory().withTransaction(session -> session.createNativeQuery(sql) + .setParameter("accountId", account.getId()) + .setParameter("permissionIds", permissions.stream().map(PermissionDO::getId).toList()) + .executeUpdate() + .map(ignored -> account)); + } + + @Override + public Uni addUserIdentifier(final AccountDO account, final UserIdentifierDO identifier) { + return queryExecutor.getSessionFactory().withTransaction(session -> { + return session.find(AccountDO.class, account.getId()) + .flatMap(retrieved -> { + retrieved.getIdentifiers().add(identifier); + + return session.merge(retrieved); + }); + }); + } + + @Override + public Uni removeUserIdentifier(final AccountDO account, final UserIdentifierDO identifier) { + return queryExecutor.getSessionFactory().withTransaction(session -> { + return session.find(AccountDO.class, account.getId()) + .flatMap(retrieved -> { + retrieved.setIdentifiers(retrieved.getIdentifiers() + .stream() + .filter(existing -> !existing.getIdentifier().equals(identifier.getIdentifier())) + .collect(Collectors.toSet())); + + return session.merge(retrieved); + }); + }); + } + + @Override + public Uni replaceIdentifierInPlace(final AccountDO accountDO, final String oldIdentifier, + final UserIdentifierDO newIdentifier) { + return queryExecutor.getSessionFactory().withTransaction(session -> { + return session.find(AccountDO.class, accountDO.getId()) + .flatMap(retrieved -> { + // remove the old one + retrieved.setIdentifiers(retrieved.getIdentifiers() + .stream() + .filter(existing -> !existing.getIdentifier().equals(oldIdentifier)) + .collect(Collectors.toSet())); + + // add the new one + retrieved.getIdentifiers().add(newIdentifier); + + return session.merge(retrieved); + }); + }); + } + + @Override + public Uni updateUserPassword(final AccountDO account, final PasswordDO hashedPassword) { + return queryExecutor.getSessionFactory().withTransaction(session -> { + return session.find(AccountDO.class, account.getId()) + .flatMap(retrieved -> { + retrieved.setHashedPassword(hashedPassword); + + return session.merge(retrieved); + }); + }); + } + @Override public Uni> getByRole(final String role, final String domain) { return queryExecutor.getAList(GET_BY_ROLE, AccountDO.class, diff --git a/dal/hibernate-dal/hibernate-persistence/src/main/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernateAppsRepository.java b/dal/hibernate-dal/hibernate-persistence/src/main/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernateAppsRepository.java index bcf21cc..d4daf03 100644 --- a/dal/hibernate-dal/hibernate-persistence/src/main/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernateAppsRepository.java +++ b/dal/hibernate-dal/hibernate-persistence/src/main/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernateAppsRepository.java @@ -3,17 +3,15 @@ import com.google.inject.Inject; import com.nexblocks.authguard.dal.hibernate.common.AbstractHibernateRepository; import com.nexblocks.authguard.dal.hibernate.common.CommonFields; -import com.nexblocks.authguard.dal.hibernate.common.QueryExecutor; import com.nexblocks.authguard.dal.hibernate.common.ReactiveQueryExecutor; import com.nexblocks.authguard.dal.model.AppDO; +import com.nexblocks.authguard.dal.model.PermissionDO; import com.nexblocks.authguard.dal.persistence.ApplicationsRepository; import com.nexblocks.authguard.dal.persistence.Page; import io.smallrye.mutiny.Uni; import java.util.List; import java.util.Optional; -import io.smallrye.mutiny.Uni; -import java.util.function.Function; public class HibernateAppsRepository extends AbstractHibernateRepository implements ApplicationsRepository { @@ -49,4 +47,37 @@ public Uni> getAllForAccount(final long accountId, final Page .setParameter(ACCOUNT_ID_FIELD, accountId) .setParameter(CURSOR_FIELD, page.getCursor()), page.getCount()); } + + @Override + public Uni addAppPermissions(final AppDO app, final List permissions) { + String sql = "INSERT INTO apps_permissions (AppDO_id, permissions_id) " + + "VALUES (:appId, :permissionId)"; + + return queryExecutor.getSessionFactory().withTransaction(session -> { + List> insertUnis = permissions.stream() + .map(permission -> session.createNativeQuery(sql) + .setParameter("appId", app.getId()) + .setParameter("permissionId", permission.getId()) + .executeUpdate() + .replaceWithVoid() + ) + .toList(); + + return Uni.combine().all().unis(insertUnis) + .with(list -> app); + }); + + } + + @Override + public Uni removeAppPermissions(final AppDO app, final List permissions) { + String sql = "DELETE FROM apps_permissions " + + "WHERE AppDO_id = :appId and permissions_id in :permissionIds"; + + return queryExecutor.getSessionFactory().withTransaction(session -> session.createNativeQuery(sql) + .setParameter("appId", app.getId()) + .setParameter("permissionIds", permissions.stream().map(PermissionDO::getId).toList()) + .executeUpdate() + .map(ignored -> app)); + } } diff --git a/dal/hibernate-dal/hibernate-persistence/src/test/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernateAccountsRepositoryTest.java b/dal/hibernate-dal/hibernate-persistence/src/test/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernateAccountsRepositoryTest.java index 351bfef..d3e7fd4 100644 --- a/dal/hibernate-dal/hibernate-persistence/src/test/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernateAccountsRepositoryTest.java +++ b/dal/hibernate-dal/hibernate-persistence/src/test/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernateAccountsRepositoryTest.java @@ -3,16 +3,12 @@ import com.nexblocks.authguard.dal.hibernate.common.ReactiveQueryExecutor; import com.nexblocks.authguard.dal.hibernate.common.SessionProvider; import com.nexblocks.authguard.dal.model.*; -import com.nexblocks.authguard.service.exceptions.ServiceConflictException; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import java.time.Instant; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -21,6 +17,7 @@ @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class HibernateAccountsRepositoryTest { private HibernateAccountsRepository repository; + private HibernatePermissionsRepository permissionsRepository; protected HibernateUserIdentifiersRepository userIdentifiersRepository; @BeforeAll @@ -36,6 +33,7 @@ protected void initialize(final SessionProvider sessionProvider) { repository = new HibernateAccountsRepository(queryExecutor); userIdentifiersRepository = new HibernateUserIdentifiersRepository(queryExecutor); + permissionsRepository = new HibernatePermissionsRepository(queryExecutor); } @Test @@ -56,6 +54,7 @@ public void saveAndGetById() { UserIdentifierDO.builder() .identifier(email.getEmail()) .type(UserIdentifierDO.Type.EMAIL) + .active(true) .build() )) .build(); @@ -86,6 +85,7 @@ public void getByExternalId() { UserIdentifierDO.builder() .identifier(email.getEmail()) .type(UserIdentifierDO.Type.EMAIL) + .active(true) .build() )) .build(); @@ -116,6 +116,7 @@ public void getByEmail() { UserIdentifierDO.builder() .identifier(email.getEmail()) .type(UserIdentifierDO.Type.EMAIL) + .active(true) .build() )) .build(); @@ -141,6 +142,7 @@ public void findByIdentifier() { .identifier(identifier) .type(UserIdentifierDO.Type.USERNAME) .domain("main") + .active(true) .build())) .hashedPassword(PasswordDO.builder() .password("password") @@ -231,6 +233,7 @@ public void updatePassword() { .identifiers(Collections.singleton(UserIdentifierDO.builder() .identifier(identifier) .type(UserIdentifierDO.Type.USERNAME) + .active(true) .build())) .hashedPassword(PasswordDO.builder() .password("password") @@ -275,10 +278,13 @@ public void removeIdentifier() { .roles(Collections.emptySet()) .permissions(Collections.emptySet()) .metadata(Collections.emptyMap()) - .identifiers(Collections.singleton(UserIdentifierDO.builder() - .identifier(identifier) - .type(UserIdentifierDO.Type.USERNAME) - .build())) + .identifiers(new HashSet<>(Arrays.asList( + UserIdentifierDO.builder() + .identifier(identifier) + .type(UserIdentifierDO.Type.USERNAME) + .active(true) + .build() + ))) .hashedPassword(PasswordDO.builder() .password("password") .salt("salt") @@ -332,6 +338,7 @@ public void updateIdentifier() { .identifiers(Collections.singleton(UserIdentifierDO.builder() .identifier(identifier) .type(UserIdentifierDO.Type.USERNAME) + .active(true) .build())) .hashedPassword(PasswordDO.builder() .password("password") @@ -589,4 +596,76 @@ public void saveDuplicateNullPhoneNumbers() { repository.save(first).subscribeAsCompletionStage().join(); repository.save(second).subscribeAsCompletionStage().join(); } + + @Test + public void addAndRemoveAccountPermission() { + long id = Math.abs(UUID.randomUUID().getMostSignificantBits()); + String identifier = "addAccountPermission"; + + AccountDO account = AccountDO.builder() + .id(id) + .roles(Collections.emptySet()) + .permissions(Collections.emptySet()) + .metadata(Collections.emptyMap()) + .domain("main") + .identifiers(Collections.singleton(UserIdentifierDO.builder() + .identifier(identifier) + .type(UserIdentifierDO.Type.USERNAME) + .domain("main") + .active(true) + .build())) + .hashedPassword(PasswordDO.builder() + .password("password") + .salt("salt") + .build()) + .build(); + + AccountDO persisted = repository.save(account).subscribeAsCompletionStage().join(); + + PermissionDO firstPermission = permissionsRepository.save( + PermissionDO.builder() + .id(Math.abs(UUID.randomUUID().getMostSignificantBits())) + .domain("main") + .forAccounts(true) + .permissionGroup("account_tests") + .name("read") + .build() + ) + .subscribeAsCompletionStage() + .join(); + + PermissionDO secondPermission = permissionsRepository.save( + PermissionDO.builder() + .id(Math.abs(UUID.randomUUID().getMostSignificantBits())) + .domain("main") + .forAccounts(true) + .permissionGroup("account_tests") + .name("write") + .build() + ) + .subscribeAsCompletionStage() + .join(); + + // add + repository.addAccountPermissions(persisted, Arrays.asList(firstPermission, secondPermission)) + .subscribeAsCompletionStage() + .join(); + + Optional afterAdd = repository.getById(persisted.getId()).subscribeAsCompletionStage().join(); + + assertThat(afterAdd).isNotEmpty(); + assertThat(afterAdd.get().getPermissions()) + .containsExactlyInAnyOrder(firstPermission, secondPermission); + + // remove one + repository.removeAccountPermissions(persisted, Collections.singletonList(firstPermission)) + .subscribeAsCompletionStage() + .join(); + + Optional afterRemove = repository.getById(persisted.getId()).subscribeAsCompletionStage().join(); + + assertThat(afterRemove).isNotEmpty(); + assertThat(afterRemove.get().getPermissions()) + .containsExactlyInAnyOrder(secondPermission); + } } \ No newline at end of file diff --git a/dal/hibernate-dal/hibernate-persistence/src/test/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernateAppsRepositoryTest.java b/dal/hibernate-dal/hibernate-persistence/src/test/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernateAppsRepositoryTest.java index 1965c39..775739a 100644 --- a/dal/hibernate-dal/hibernate-persistence/src/test/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernateAppsRepositoryTest.java +++ b/dal/hibernate-dal/hibernate-persistence/src/test/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernateAppsRepositoryTest.java @@ -1,25 +1,22 @@ package com.nexblocks.authguard.dal.hibernate.persistence; -import com.nexblocks.authguard.dal.hibernate.common.QueryExecutor; import com.nexblocks.authguard.dal.hibernate.common.ReactiveQueryExecutor; import com.nexblocks.authguard.dal.hibernate.common.SessionProvider; import com.nexblocks.authguard.dal.model.AppDO; +import com.nexblocks.authguard.dal.model.PermissionDO; import com.nexblocks.authguard.dal.persistence.LongPage; -import com.nexblocks.authguard.dal.persistence.Page; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import static org.assertj.core.api.Assertions.assertThat; @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class HibernateAppsRepositoryTest { private HibernateAppsRepository repository; + private HibernatePermissionsRepository permissionsRepository; @BeforeAll public void setup() { @@ -28,7 +25,10 @@ public void setup() { } protected void initialize(final SessionProvider sessionProvider) { - repository = new HibernateAppsRepository(new ReactiveQueryExecutor(sessionProvider)); + ReactiveQueryExecutor queryExecutor = new ReactiveQueryExecutor(sessionProvider); + + repository = new HibernateAppsRepository(queryExecutor); + permissionsRepository = new HibernatePermissionsRepository(queryExecutor); } @Test @@ -81,4 +81,64 @@ public void getAllForAccount() { assertThat(retrieved).containsOnly(persisted); } + + @Test + public void addAndRemoveAppPermission() { + long id = Math.abs(UUID.randomUUID().getMostSignificantBits()); + + AppDO app = AppDO.builder() + .id(id) + .roles(Collections.emptySet()) + .permissions(Collections.emptySet()) + .domain("main") + .build(); + + AppDO persisted = repository.save(app).subscribeAsCompletionStage().join(); + + PermissionDO firstPermission = permissionsRepository.save( + PermissionDO.builder() + .id(Math.abs(UUID.randomUUID().getMostSignificantBits())) + .domain("main") + .forApplications(true) + .permissionGroup("app_tests") + .name("read") + .build() + ) + .subscribeAsCompletionStage() + .join(); + + PermissionDO secondPermission = permissionsRepository.save( + PermissionDO.builder() + .id(Math.abs(UUID.randomUUID().getMostSignificantBits())) + .domain("main") + .forAccounts(true) + .permissionGroup("app_tests") + .name("write") + .build() + ) + .subscribeAsCompletionStage() + .join(); + + // add + repository.addAppPermissions(persisted, Arrays.asList(firstPermission, secondPermission)) + .subscribeAsCompletionStage() + .join(); + + Optional afterAdd = repository.getById(persisted.getId()).subscribeAsCompletionStage().join(); + + assertThat(afterAdd).isNotEmpty(); + assertThat(afterAdd.get().getPermissions()) + .containsExactlyInAnyOrder(firstPermission, secondPermission); + + // remove one + repository.removeAppPermissions(persisted, Collections.singletonList(firstPermission)) + .subscribeAsCompletionStage() + .join(); + + Optional afterRemove = repository.getById(persisted.getId()).subscribeAsCompletionStage().join(); + + assertThat(afterRemove).isNotEmpty(); + assertThat(afterRemove.get().getPermissions()) + .containsExactlyInAnyOrder(secondPermission); + } } \ No newline at end of file diff --git a/dal/hibernate-dal/hibernate-persistence/src/test/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernatePermissionsRepositoryTest.java b/dal/hibernate-dal/hibernate-persistence/src/test/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernatePermissionsRepositoryTest.java index 0dc8801..fc19bfa 100644 --- a/dal/hibernate-dal/hibernate-persistence/src/test/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernatePermissionsRepositoryTest.java +++ b/dal/hibernate-dal/hibernate-persistence/src/test/java/com/nexblocks/authguard/dal/hibernate/persistence/HibernatePermissionsRepositoryTest.java @@ -1,11 +1,9 @@ package com.nexblocks.authguard.dal.hibernate.persistence; -import com.nexblocks.authguard.dal.hibernate.common.QueryExecutor; import com.nexblocks.authguard.dal.hibernate.common.ReactiveQueryExecutor; import com.nexblocks.authguard.dal.hibernate.common.SessionProvider; import com.nexblocks.authguard.dal.model.PermissionDO; import com.nexblocks.authguard.dal.persistence.LongPage; -import com.nexblocks.authguard.dal.persistence.Page; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -53,7 +51,8 @@ public void search() { @Test public void getAll() { - assertThat(repository.getAll("main", LongPage.of(null, 20)).subscribeAsCompletionStage().join()).containsOnly(first, second); + assertThat(repository.getAll("main", LongPage.of(null, 20)).subscribeAsCompletionStage().join()) + .isNotEmpty(); } @Test diff --git a/dal/memory-dal/src/main/java/com/nexblocks/authguard/dal/memory/dal/MockAccountsRepository.java b/dal/memory-dal/src/main/java/com/nexblocks/authguard/dal/memory/dal/MockAccountsRepository.java index 7099b9e..2975abb 100644 --- a/dal/memory-dal/src/main/java/com/nexblocks/authguard/dal/memory/dal/MockAccountsRepository.java +++ b/dal/memory-dal/src/main/java/com/nexblocks/authguard/dal/memory/dal/MockAccountsRepository.java @@ -2,18 +2,18 @@ import com.google.inject.Singleton; import com.nexblocks.authguard.dal.model.AccountDO; +import com.nexblocks.authguard.dal.model.PasswordDO; +import com.nexblocks.authguard.dal.model.PermissionDO; import com.nexblocks.authguard.dal.model.UserIdentifierDO; import com.nexblocks.authguard.dal.persistence.AccountsRepository; import com.nexblocks.authguard.service.exceptions.ServiceConflictException; +import com.nexblocks.authguard.service.exceptions.ServiceNotFoundException; import com.nexblocks.authguard.service.exceptions.codes.ErrorCode; import io.smallrye.mutiny.Uni; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import io.smallrye.mutiny.Uni; +import java.util.*; import java.util.stream.Collectors; +import java.util.stream.Stream; @Singleton public class MockAccountsRepository extends AbstractRepository implements AccountsRepository { @@ -86,6 +86,88 @@ public Uni> findByIdentifier(final String identifier, final .findFirst()); } + @Override + public Uni addAccountPermissions(final AccountDO account, final List permissions) { + return getById(account.getId()) + .map(opt -> opt.orElseThrow(() -> new ServiceNotFoundException(ErrorCode.ACCOUNT_DOES_NOT_EXIST, ""))) + .flatMap(retrieved -> { + Set combinedPermissions = Stream.concat(account.getPermissions().stream(), permissions.stream()) + .collect(Collectors.toSet()); + + retrieved.setPermissions(combinedPermissions); + + return super.update(retrieved).map(Optional::get); + }); + } + + @Override + public Uni removeAccountPermissions(final AccountDO account, final List permissions) { + return getById(account.getId()) + .map(opt -> opt.orElseThrow(() -> new ServiceNotFoundException(ErrorCode.ACCOUNT_DOES_NOT_EXIST, ""))) + .flatMap(retrieved -> { + Set permissionsFullNames = permissions.stream() + .map(permission -> permission.getPermissionGroup() + ":" + permission.getName()) + .collect(Collectors.toSet()); + + Set filteredPermissions = retrieved.getPermissions().stream() + .filter(permission -> !permissionsFullNames.contains(permission.getPermissionGroup() + ":" + permission.getName())) + .collect(Collectors.toSet()); + + retrieved.setPermissions(filteredPermissions); + + return super.update(retrieved).map(Optional::get); + }); + } + + @Override + public Uni addUserIdentifier(final AccountDO account, final UserIdentifierDO identifier) { + return getById(account.getId()) + .map(opt -> opt.orElseThrow(() -> new ServiceNotFoundException(ErrorCode.ACCOUNT_DOES_NOT_EXIST, ""))) + .flatMap(retrieved -> { + retrieved.getIdentifiers().add(identifier); + + return super.update(retrieved).map(Optional::get); + }); + } + + @Override + public Uni removeUserIdentifier(final AccountDO account, final UserIdentifierDO identifier) { + return getById(account.getId()) + .map(opt -> opt.orElseThrow(() -> new ServiceNotFoundException(ErrorCode.ACCOUNT_DOES_NOT_EXIST, ""))) + .flatMap(retrieved -> { + retrieved.getIdentifiers() + .removeIf(stored -> stored.getIdentifier().equals(identifier.getIdentifier())); + + return super.update(retrieved).map(Optional::get); + }); + } + + @Override + public Uni replaceIdentifierInPlace(final AccountDO account, final String oldIdentifier, + final UserIdentifierDO newIdentifier) { + return getById(account.getId()) + .map(opt -> opt.orElseThrow(() -> new ServiceNotFoundException(ErrorCode.ACCOUNT_DOES_NOT_EXIST, ""))) + .flatMap(retrieved -> { + retrieved.getIdentifiers() + .removeIf(stored -> stored.getIdentifier().equals(oldIdentifier)); + + retrieved.getIdentifiers().add(newIdentifier); + + return super.update(retrieved).map(Optional::get); + }); + } + + @Override + public Uni updateUserPassword(final AccountDO account, final PasswordDO hashedPassword) { + return getById(account.getId()) + .map(opt -> opt.orElseThrow(() -> new ServiceNotFoundException(ErrorCode.ACCOUNT_DOES_NOT_EXIST, ""))) + .flatMap(retrieved -> { + retrieved.setHashedPassword(hashedPassword); + + return super.update(retrieved).map(Optional::get); + }); + } + private boolean hasIdentifier(final AccountDO credentials, final String identifier, final String domain) { return credentials.getIdentifiers().stream() .filter(userIdentifier -> userIdentifier.getDomain().equals(domain)) diff --git a/dal/memory-dal/src/main/java/com/nexblocks/authguard/dal/memory/dal/MockApplicationsRepository.java b/dal/memory-dal/src/main/java/com/nexblocks/authguard/dal/memory/dal/MockApplicationsRepository.java index 154ff68..9610f87 100644 --- a/dal/memory-dal/src/main/java/com/nexblocks/authguard/dal/memory/dal/MockApplicationsRepository.java +++ b/dal/memory-dal/src/main/java/com/nexblocks/authguard/dal/memory/dal/MockApplicationsRepository.java @@ -2,13 +2,20 @@ import com.google.inject.Singleton; import com.nexblocks.authguard.dal.model.AppDO; +import com.nexblocks.authguard.dal.model.PermissionDO; import com.nexblocks.authguard.dal.persistence.ApplicationsRepository; import com.nexblocks.authguard.dal.persistence.Page; import java.util.List; import java.util.Optional; + +import com.nexblocks.authguard.service.exceptions.ServiceNotFoundException; +import com.nexblocks.authguard.service.exceptions.codes.ErrorCode; import io.smallrye.mutiny.Uni; + +import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; @Singleton public class MockApplicationsRepository extends AbstractRepository implements ApplicationsRepository { @@ -29,4 +36,37 @@ public Uni> getAllForAccount(final long accountId, final Page .limit(page.getCount()) .collect(Collectors.toList())); } + + @Override + public Uni addAppPermissions(final AppDO app, final List permissions) { + return getById(app.getId()) + .map(opt -> opt.orElseThrow(() -> new ServiceNotFoundException(ErrorCode.ACCOUNT_DOES_NOT_EXIST, ""))) + .flatMap(retrieved -> { + Set combinedPermissions = Stream.concat(app.getPermissions().stream(), permissions.stream()) + .collect(Collectors.toSet()); + + retrieved.setPermissions(combinedPermissions); + + return super.update(retrieved).map(Optional::get); + }); + } + + @Override + public Uni removeAppPermissions(final AppDO app, final List permissions) { + return getById(app.getId()) + .map(opt -> opt.orElseThrow(() -> new ServiceNotFoundException(ErrorCode.APP_DOES_NOT_EXIST, ""))) + .flatMap(retrieved -> { + Set permissionsFullNames = permissions.stream() + .map(permission -> permission.getPermissionGroup() + ":" + permission.getName()) + .collect(Collectors.toSet()); + + Set filteredPermissions = retrieved.getPermissions().stream() + .filter(permission -> !permissionsFullNames.contains(permission.getPermissionGroup() + ":" + permission.getName())) + .collect(Collectors.toSet()); + + retrieved.setPermissions(filteredPermissions); + + return super.update(retrieved).map(Optional::get); + }); + } } diff --git a/dal/mongo-dal/mongo-common/src/main/java/com/nexblocks/authguard/dal/mongo/common/facade/ReactiveMongoFacade.java b/dal/mongo-dal/mongo-common/src/main/java/com/nexblocks/authguard/dal/mongo/common/facade/ReactiveMongoFacade.java index c7c88d7..c7be8bd 100644 --- a/dal/mongo-dal/mongo-common/src/main/java/com/nexblocks/authguard/dal/mongo/common/facade/ReactiveMongoFacade.java +++ b/dal/mongo-dal/mongo-common/src/main/java/com/nexblocks/authguard/dal/mongo/common/facade/ReactiveMongoFacade.java @@ -38,6 +38,10 @@ public Uni> findById(final long id) { return findOne(Filters.eq("_id", id)); } + public Uni findByIdUnchecked(final long id) { + return findOneUnchecked(Filters.eq("_id", id)); + } + public Uni> findOne(final Bson filter) { final Publisher publisher = collection.find(filter) .first(); @@ -48,6 +52,15 @@ public Uni> findOne(final Bson filter) { .map(Optional::ofNullable); } + public Uni findOneUnchecked(final Bson filter) { + final Publisher publisher = collection.find(filter) + .first(); + + final SubscribeSingleResult subscriber = SubscribeSingleResult.toPublisher(publisher); + + return Uni.createFrom().completionStage(subscriber.getFuture()); + } + public Uni> replaceById(final long id, final T document) { return replaceOne(Filters.eq("_id", id), document); } diff --git a/dal/mongo-dal/mongo-persistence/src/main/java/com/nexblocks/authguard/dal/mongo/persistence/MongoAccountsRepository.java b/dal/mongo-dal/mongo-persistence/src/main/java/com/nexblocks/authguard/dal/mongo/persistence/MongoAccountsRepository.java index 338fa4a..6301c42 100644 --- a/dal/mongo-dal/mongo-persistence/src/main/java/com/nexblocks/authguard/dal/mongo/persistence/MongoAccountsRepository.java +++ b/dal/mongo-dal/mongo-persistence/src/main/java/com/nexblocks/authguard/dal/mongo/persistence/MongoAccountsRepository.java @@ -5,17 +5,23 @@ import com.mongodb.MongoWriteException; import com.mongodb.client.model.Filters; import com.nexblocks.authguard.dal.model.AccountDO; +import com.nexblocks.authguard.dal.model.PasswordDO; +import com.nexblocks.authguard.dal.model.PermissionDO; +import com.nexblocks.authguard.dal.model.UserIdentifierDO; import com.nexblocks.authguard.dal.mongo.common.AbstractMongoRepository; import com.nexblocks.authguard.dal.mongo.common.setup.MongoClientWrapper; import com.nexblocks.authguard.dal.mongo.config.Defaults; import com.nexblocks.authguard.dal.persistence.AccountsRepository; import com.nexblocks.authguard.service.exceptions.ServiceConflictException; +import com.nexblocks.authguard.service.exceptions.ServiceNotFoundException; import com.nexblocks.authguard.service.exceptions.codes.ErrorCode; import io.smallrye.mutiny.Uni; import java.util.List; import java.util.Optional; -import io.smallrye.mutiny.Uni; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class MongoAccountsRepository extends AbstractMongoRepository implements AccountsRepository { private static final String COLLECTION_KEY = "accounts"; @@ -80,6 +86,94 @@ public Uni> findByIdentifier(final String identifier, final )); } + @Override + public Uni addAccountPermissions(final AccountDO account, final List permissions) { + return getById(account.getId()) + .map(opt -> opt + .map(retrieved -> { + Set combinedPermissions = Stream.concat(account.getPermissions().stream(), permissions.stream()) + .collect(Collectors.toSet()); + + retrieved.setPermissions(combinedPermissions); + + return retrieved; + }) + .orElseThrow(() -> new ServiceNotFoundException(ErrorCode.ACCOUNT_DOES_NOT_EXIST, "Account does not exist"))) + .flatMap(updated -> facade.replaceById(account.getId(), updated)) + .map(opt -> opt.orElseThrow(() -> new ServiceNotFoundException(ErrorCode.ACCOUNT_DOES_NOT_EXIST, "Account does not exist"))); + } + + @Override + public Uni removeAccountPermissions(final AccountDO account, final List permissions) { + return getById(account.getId()) + .map(opt -> opt + .map(retrieved -> { + Set permissionsFullNames = permissions.stream() + .map(permission -> permission.getPermissionGroup() + ":" + permission.getName()) + .collect(Collectors.toSet()); + + Set filteredPermissions = retrieved.getPermissions().stream() + .filter(permission -> !permissionsFullNames.contains(permission.getPermissionGroup() + ":" + permission.getName())) + .collect(Collectors.toSet()); + + retrieved.setPermissions(filteredPermissions); + + return retrieved; + }) + .orElseThrow(() -> new ServiceNotFoundException(ErrorCode.ACCOUNT_DOES_NOT_EXIST, "Account does not exist"))) + .flatMap(updated -> facade.replaceById(account.getId(), updated)) + .map(opt -> opt.orElseThrow(() -> new ServiceNotFoundException(ErrorCode.ACCOUNT_DOES_NOT_EXIST, "Account does not exist"))); + } + + @Override + public Uni addUserIdentifier(final AccountDO account, final UserIdentifierDO identifier) { + return facade.findByIdUnchecked(account.getId()) + .flatMap(retrieved -> { + retrieved.getIdentifiers().add(identifier); + + return facade.replaceById(account.getId(), retrieved) + .map(opt -> opt.orElseThrow(() -> new ServiceNotFoundException(ErrorCode.ACCOUNT_DOES_NOT_EXIST, "Account does not exist"))); + }); + } + + @Override + public Uni removeUserIdentifier(final AccountDO account, final UserIdentifierDO identifier) { + return facade.findByIdUnchecked(account.getId()) + .flatMap(retrieved -> { + retrieved.getIdentifiers() + .removeIf(stored -> stored.getIdentifier().equals(identifier.getIdentifier())); + + return facade.replaceById(account.getId(), retrieved) + .map(opt -> opt.orElseThrow(() -> new ServiceNotFoundException(ErrorCode.ACCOUNT_DOES_NOT_EXIST, "Account does not exist"))); + }); + } + + @Override + public Uni replaceIdentifierInPlace(final AccountDO account, final String oldIdentifier, + final UserIdentifierDO newIdentifier) { + return facade.findByIdUnchecked(account.getId()) + .flatMap(retrieved -> { + retrieved.getIdentifiers() + .removeIf(stored -> stored.getIdentifier().equals(oldIdentifier)); + + retrieved.getIdentifiers().add(newIdentifier); + + return facade.replaceById(account.getId(), retrieved) + .map(opt -> opt.orElseThrow(() -> new ServiceNotFoundException(ErrorCode.ACCOUNT_DOES_NOT_EXIST, "Account does not exist"))); + }); + } + + @Override + public Uni updateUserPassword(final AccountDO account, final PasswordDO hashedPassword) { + return facade.findByIdUnchecked(account.getId()) + .flatMap(retrieved -> { + retrieved.setHashedPassword(hashedPassword); + + return facade.replaceById(account.getId(), retrieved) + .map(opt -> opt.orElseThrow(() -> new ServiceNotFoundException(ErrorCode.ACCOUNT_DOES_NOT_EXIST, "Account does not exist"))); + }); + } + private RuntimeException mapWriteErrors(final MongoWriteException e) { if (e.getError().getCategory() == ErrorCategory.DUPLICATE_KEY) { return mapDuplicateError(e); diff --git a/dal/mongo-dal/mongo-persistence/src/main/java/com/nexblocks/authguard/dal/mongo/persistence/MongoApplicationsRepository.java b/dal/mongo-dal/mongo-persistence/src/main/java/com/nexblocks/authguard/dal/mongo/persistence/MongoApplicationsRepository.java index 6b6ada4..8960985 100644 --- a/dal/mongo-dal/mongo-persistence/src/main/java/com/nexblocks/authguard/dal/mongo/persistence/MongoApplicationsRepository.java +++ b/dal/mongo-dal/mongo-persistence/src/main/java/com/nexblocks/authguard/dal/mongo/persistence/MongoApplicationsRepository.java @@ -3,6 +3,7 @@ import com.google.inject.Inject; import com.mongodb.client.model.Filters; import com.nexblocks.authguard.dal.model.AppDO; +import com.nexblocks.authguard.dal.model.PermissionDO; import com.nexblocks.authguard.dal.mongo.common.AbstractMongoRepository; import com.nexblocks.authguard.dal.mongo.common.setup.MongoClientWrapper; import com.nexblocks.authguard.dal.mongo.config.Defaults; @@ -11,6 +12,12 @@ import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.nexblocks.authguard.service.exceptions.ServiceNotFoundException; +import com.nexblocks.authguard.service.exceptions.codes.ErrorCode; import io.smallrye.mutiny.Uni; public class MongoApplicationsRepository extends AbstractMongoRepository implements ApplicationsRepository { @@ -33,4 +40,45 @@ public Uni> getAllForAccount(final long accountId, final Page Filters.gt("_id", page.getCursor()) ), page.getCount()); } + + @Override + public Uni addAppPermissions(final AppDO app, final List permissions) { + return getById(app.getId()) + .map(opt -> opt + .map(retrieved -> { + Set combinedPermissions = Stream.concat(app.getPermissions().stream(), permissions.stream()) + .collect(Collectors.toSet()); + + retrieved.setPermissions(combinedPermissions); + + return retrieved; + }) + .orElseThrow(() -> new ServiceNotFoundException(ErrorCode.APP_DOES_NOT_EXIST, "App does not exist"))) + .flatMap(updated -> facade.replaceById(app.getId(), updated)) + .map(opt -> opt.orElseThrow(() -> new ServiceNotFoundException(ErrorCode.APP_DOES_NOT_EXIST, "App does not exist"))); + + } + + @Override + public Uni removeAppPermissions(final AppDO app, final List permissions) { + return getById(app.getId()) + .map(opt -> opt + .map(retrieved -> { + Set permissionsFullNames = permissions.stream() + .map(permission -> permission.getPermissionGroup() + ":" + permission.getName()) + .collect(Collectors.toSet()); + + Set filteredPermissions = retrieved.getPermissions().stream() + .filter(permission -> !permissionsFullNames.contains(permission.getPermissionGroup() + ":" + permission.getName())) + .collect(Collectors.toSet()); + + retrieved.setPermissions(filteredPermissions); + + return retrieved; + }) + .orElseThrow(() -> new ServiceNotFoundException(ErrorCode.APP_DOES_NOT_EXIST, "App does not exist"))) + .flatMap(updated -> facade.replaceById(app.getId(), updated)) + .map(opt -> opt.orElseThrow(() -> new ServiceNotFoundException(ErrorCode.APP_DOES_NOT_EXIST, "App does not exist"))); + + } } diff --git a/dal/mongo-dal/mongo-persistence/src/test/java/com/nexblocks/authguard/dal/mongo/persistence/MongoAccountsRepositoryTest.java b/dal/mongo-dal/mongo-persistence/src/test/java/com/nexblocks/authguard/dal/mongo/persistence/MongoAccountsRepositoryTest.java index 9159424..4baa8d5 100644 --- a/dal/mongo-dal/mongo-persistence/src/test/java/com/nexblocks/authguard/dal/mongo/persistence/MongoAccountsRepositoryTest.java +++ b/dal/mongo-dal/mongo-persistence/src/test/java/com/nexblocks/authguard/dal/mongo/persistence/MongoAccountsRepositoryTest.java @@ -1,11 +1,7 @@ package com.nexblocks.authguard.dal.mongo.persistence; -import com.nexblocks.authguard.dal.model.AccountDO; -import com.nexblocks.authguard.dal.model.EmailDO; -import com.nexblocks.authguard.dal.model.PhoneNumberDO; -import com.nexblocks.authguard.dal.model.UserIdentifierDO; +import com.nexblocks.authguard.dal.model.*; import com.nexblocks.authguard.dal.mongo.common.setup.MongoClientWrapper; -//import com.nexblocks.authguard.dal.mongo.persistence.bootstrap.IndicesBootstrap; import com.nexblocks.authguard.dal.mongo.persistence.bootstrap.IndicesBootstrap; import com.nexblocks.authguard.service.exceptions.ServiceConflictException; import org.junit.jupiter.api.BeforeAll; @@ -13,10 +9,7 @@ import org.junit.jupiter.api.TestInstance; import java.time.Instant; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -379,4 +372,67 @@ public void updateDuplicateEmails() { assertThatThrownBy(() -> repository.update(second).subscribeAsCompletionStage().join()) .hasCauseInstanceOf(ServiceConflictException.class); } + + @Test + public void addAndRemoveAccountPermission() { + long id = Math.abs(UUID.randomUUID().getMostSignificantBits()); + String identifier = "addAccountPermission"; + + AccountDO account = AccountDO.builder() + .id(id) + .roles(Collections.emptySet()) + .permissions(Collections.emptySet()) + .metadata(Collections.emptyMap()) + .domain("main") + .identifiers(Collections.singleton(UserIdentifierDO.builder() + .identifier(identifier) + .type(UserIdentifierDO.Type.USERNAME) + .domain("main") + .build())) + .hashedPassword(PasswordDO.builder() + .password("password") + .salt("salt") + .build()) + .build(); + + AccountDO persisted = repository.save(account).subscribeAsCompletionStage().join(); + + PermissionDO firstPermission = PermissionDO.builder() + .id(Math.abs(UUID.randomUUID().getMostSignificantBits())) + .domain("main") + .forAccounts(true) + .permissionGroup("tests") + .name("read") + .build(); + + PermissionDO secondPermission = PermissionDO.builder() + .id(Math.abs(UUID.randomUUID().getMostSignificantBits())) + .domain("main") + .forAccounts(true) + .permissionGroup("tests") + .name("write") + .build(); + + // add + repository.addAccountPermissions(persisted, Arrays.asList(firstPermission, secondPermission)) + .subscribeAsCompletionStage() + .join(); + + Optional afterAdd = repository.getById(persisted.getId()).subscribeAsCompletionStage().join(); + + assertThat(afterAdd).isNotEmpty(); + assertThat(afterAdd.get().getPermissions()) + .containsExactlyInAnyOrder(firstPermission, secondPermission); + + // remove one + repository.removeAccountPermissions(persisted, Collections.singletonList(firstPermission)) + .subscribeAsCompletionStage() + .join(); + + Optional afterRemove = repository.getById(persisted.getId()).subscribeAsCompletionStage().join(); + + assertThat(afterRemove).isNotEmpty(); + assertThat(afterRemove.get().getPermissions()) + .containsExactlyInAnyOrder(secondPermission); + } } \ No newline at end of file