diff --git a/charts/account-service/templates/authentication-policy.yaml b/charts/account-service/templates/authentication-policy.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8fdcf60b6660bc4d40271e720fb2c244bae626fe --- /dev/null +++ b/charts/account-service/templates/authentication-policy.yaml @@ -0,0 +1,19 @@ +apiVersion: "authentication.istio.io/v1alpha1" +kind: "Policy" +metadata: + name: {{ template "account-service.fullname" . }} + labels: + app: {{ template "account-service.name" . }} + chart: {{ template "account-service.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + targets: + - name: {{ template "account-service.name" . }} + origins: + - jwt: + issuer: https://auth.nynja.biz/ + jwksUri: http://auth-service.auth.svc.cluster.local:8008/keys/public + audiences: + - dGVzdEluc3RhbmNl:NynjaApp:NynjaOrg + principalBinding: USE_ORIGIN diff --git a/charts/account-service/templates/deployment.yaml b/charts/account-service/templates/deployment.yaml index 8a286f602203e1c0af9b5d705b15abfd9cda0267..05a885c5a826e4bc8f9d34943012d5fc00f7d11b 100644 --- a/charts/account-service/templates/deployment.yaml +++ b/charts/account-service/templates/deployment.yaml @@ -42,9 +42,11 @@ spec: periodSeconds: 5 timeoutSeconds: 5 livenessProbe: - httpGet: - path: /actuator/info - port: {{ .Values.ports.containerPort.http }} + exec: + command: + - /bin/sh + - -c + - curl --silent http://localhost:{{ .Values.ports.containerPort.http }}/actuator/info successThreshold: 1 failureThreshold: 10 initialDelaySeconds: 60 diff --git a/pom.xml b/pom.xml index 7a41f5d74bc1599557bcc375794b4a8ce12f2456..fa3ba37e94e34c43440b270365f6316cde55d59b 100644 --- a/pom.xml +++ b/pom.xml @@ -111,6 +111,18 @@ + + libs-snapshot-local.biz.nynja.protos + bridge-service-intracoldev + 1.0-SNAPSHOT + + + com.google.protobuf + protobuf-java + + + + com.googlecode.libphonenumber libphonenumber diff --git a/releases/staging/account-service.yaml b/releases/staging/account-service.yaml index 28b8dfb94f77cb4e4ca422481d1e68ce71503679..6adc18ac0870f0b407e1f34c4220f6469a8a39de 100644 --- a/releases/staging/account-service.yaml +++ b/releases/staging/account-service.yaml @@ -31,6 +31,7 @@ spec: http: 8080 grpc: 6565 + ## CORS policy needs clean up!!! This is temporary solution to allow UI team and others that depend on stable environment to work on staging. ## # CORS policy corsPolicy: allowOrigin: diff --git a/src/main/java/biz/nynja/account/configuration/ProfileDataConfiguration.java b/src/main/java/biz/nynja/account/configuration/ProfileDataConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..bcb6ad994769cffa4939d867747e73cfeb4ddff6 --- /dev/null +++ b/src/main/java/biz/nynja/account/configuration/ProfileDataConfiguration.java @@ -0,0 +1,21 @@ +/** + * Copyright (C) 2018 Nynja Inc. All rights reserved. + */ +package biz.nynja.account.configuration; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "profile-data") +public class ProfileDataConfiguration { + private int maxAuthenticationprovidersPerProfile; + + public int getMaxAuthenticationprovidersPerProfile() { + return maxAuthenticationprovidersPerProfile; + } + + public void setMaxAuthenticationprovidersPerProfile(int maxAuthenticationprovidersPerProfile) { + this.maxAuthenticationprovidersPerProfile = maxAuthenticationprovidersPerProfile; + } +} diff --git a/src/main/java/biz/nynja/account/grid/ag/AdminServiceImpl.java b/src/main/java/biz/nynja/account/grid/ag/AdminServiceImpl.java index 1c9272e0fc94755d62b6f767c2394d25c8a3e32c..87fb0faea24abf67638d6b85d69d971f52ee54c6 100644 --- a/src/main/java/biz/nynja/account/grid/ag/AdminServiceImpl.java +++ b/src/main/java/biz/nynja/account/grid/ag/AdminServiceImpl.java @@ -3,6 +3,7 @@ */ package biz.nynja.account.grid.ag; +import org.apache.commons.lang3.StringUtils; import org.lognet.springboot.grpc.GRpcService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,6 +14,8 @@ import biz.nynja.account.admin.grpc.AccountsCount; import biz.nynja.account.admin.grpc.AdminAccountServiceGrpc; import biz.nynja.account.admin.grpc.CreateAccountRequest; import biz.nynja.account.admin.grpc.EmptyRequest; +import biz.nynja.account.admin.grpc.FilterModel; +import biz.nynja.account.admin.grpc.FilterModel.FilterType; import biz.nynja.account.admin.grpc.GetAllAccountsRequest; import biz.nynja.account.grpc.AccountResponse; import biz.nynja.account.grpc.CompletePendingAccountCreationRequest; @@ -52,10 +55,11 @@ public class AdminServiceImpl extends AdminAccountServiceGrpc.AdminAccountServic logger.info("Getting accounts ..."); logger.debug("Getting accounts: {} ...", request); - Validation validation = validateRowInput(request.getStartRow(), request.getEndRow()); + Validation validation = validateInput(request); if (validation.hasErrors()) { responseObserver.onNext(AccountsAdminResponse.newBuilder().setError(ErrorResponse.newBuilder() - .setCause(validation.getCause().get()).setMessage(validation.getErrorMessage())).build()); + .setCause(validation.getCause().get()).setMessage(validation.getErrors().get(0).getMessage())) + .build()); responseObserver.onCompleted(); return; } @@ -67,18 +71,50 @@ public class AdminServiceImpl extends AdminAccountServiceGrpc.AdminAccountServic responseObserver.onCompleted(); } - private Validation validateRowInput(int startRow, int endRow) { + private Validation validateInput(GetAllAccountsRequest request) { Validation validation = new Validation(); - if (startRow < 1) { - validation.addError(new ValidationError( - "startRow parameter (" + startRow - + ") should be more than 1 and less or equal to endRow parameter (" + endRow + ").", + if (request.getStartRow() < 1) { + validation.addError(new ValidationError("startRow parameter (" + request.getStartRow() + + ") should be more than 1 and less or equal to endRow parameter (" + request.getStartRow() + ").", Cause.ADMIN_INVALID_START_ROW)); - } else if (endRow < startRow || (endRow > accountRepository.count() + 100)) { - validation.addError(new ValidationError("endRow parameter (" + endRow + ") should be more than start row (" - + startRow + ") and less than all users plus maximum pagination of 100." - + (accountRepository.count() + 100), Cause.ADMIN_INVALID_END_ROW)); } + if (request.getEndRow() < request.getStartRow() || (request.getEndRow() > accountRepository.count() + 100)) { + validation.addError( + new ValidationError("endRow parameter (" + request.getEndRow() + ") should be more than start row (" + + request.getStartRow() + ") and less than all users plus maximum pagination of 100." + + (accountRepository.count() + 100), Cause.ADMIN_INVALID_END_ROW)); + } + + for (FilterModel model : request.getFilterModelList()) { + if ("creationTimestamp".equals(model.getColId()) || "lastUpdateTimestamp".equals(model.getColId())) { + try { + Long.valueOf(model.getFilterValue()); + if (!StringUtils.isEmpty(model.getAdditionalFilterValue())) { + Long.valueOf(model.getAdditionalFilterValue()); + + if (Long.valueOf(model.getFilterValue()) > Long.valueOf(model.getAdditionalFilterValue())) { + validation.addError(new ValidationError( + "When filtering by creationTimestamp and lastUpdateTimestamp filter value should be lower than additional filter value.", + Cause.ADMIN_INVALID_FILTER_CRITERIA)); + } + } + } catch (NumberFormatException e) { + validation.addError(new ValidationError( + "When filtering by creationTimestamp and lastUpdateTimestamp filter values should be valid long numbers.", + Cause.ADMIN_INVALID_FILTER_CRITERIA)); + } + + if ((FilterType.EQUALS.equals(model.getFilterType()) + || FilterType.NOTEQUAL.equals(model.getFilterType())) + && (StringUtils.isEmpty(model.getFilterValue()) + || StringUtils.isEmpty(model.getAdditionalFilterValue()))) { + validation.addError(new ValidationError( + "When filtering by creationTimestamp and lastUpdateTimestamp with type of equals or nonequal both values should be present.", + Cause.ADMIN_INVALID_FILTER_CRITERIA)); + } + } + } + return validation; } diff --git a/src/main/java/biz/nynja/account/grid/ag/AgGridService.java b/src/main/java/biz/nynja/account/grid/ag/AgGridService.java index 4d706528a68f93b809cc5bdd3776ae916f34439c..88f24196de8d3364ffd0b81a33b973ed3b9c8c99 100644 --- a/src/main/java/biz/nynja/account/grid/ag/AgGridService.java +++ b/src/main/java/biz/nynja/account/grid/ag/AgGridService.java @@ -167,7 +167,7 @@ public class AgGridService { } else { helper = getFilteredResults(pageable, filterModel); accounts.retainAll(helper.getAccounts().getContent()); - if (count < helper.getCount()) { + if (helper.getCount() < count) { count = helper.getCount(); } } diff --git a/src/main/java/biz/nynja/account/repositories/AccountRepositoryAdditional.java b/src/main/java/biz/nynja/account/repositories/AccountRepositoryAdditional.java index c6f039ded6af908b5e99180e2582a4636263996f..800a1cde24c7dd695aa9f8077dbcc9d9c0a7239d 100644 --- a/src/main/java/biz/nynja/account/repositories/AccountRepositoryAdditional.java +++ b/src/main/java/biz/nynja/account/repositories/AccountRepositoryAdditional.java @@ -12,6 +12,7 @@ import org.springframework.stereotype.Repository; import biz.nynja.account.grpc.CompletePendingAccountCreationRequest; import biz.nynja.account.grpc.UpdateAccountRequest; import biz.nynja.account.grpc.ErrorResponse.Cause; +import biz.nynja.account.grpc.SearchableOption; import biz.nynja.account.models.Account; import biz.nynja.account.models.AccountByProfileId; import biz.nynja.account.models.AuthenticationProvider; @@ -36,6 +37,10 @@ public interface AccountRepositoryAdditional { PendingAccountByAuthenticationProvider findSameAuthenticationProviderInPendingAccount(AuthenticationProvider authProvider); + boolean accountIdAlreadyUsed(UUID accountId); + + boolean profileIdAlreadyUsed(UUID profileId); + boolean authenticationProviderAlreadyUsedInProfile(AuthenticationProvider authProvider); boolean addAuthenticationProvider(UUID profileId, AuthenticationProvider authProvider); @@ -58,4 +63,6 @@ public interface AccountRepositoryAdditional { Optional searchAccountByLoginOption(AuthenticationProvider loginOption) throws IncorrectAccountCountException; void removeNullsForSearchableOption(); + + boolean updateSearchableOption(UUID profileId, String authProviderType, String authProvider, SearchableOption searchableOption); } diff --git a/src/main/java/biz/nynja/account/repositories/AccountRepositoryAdditionalImpl.java b/src/main/java/biz/nynja/account/repositories/AccountRepositoryAdditionalImpl.java index f0e92ca8806625adebe20bf6438ae6ada9a55574..da4d68398489b25f26a50c15ce89e844396fa510 100644 --- a/src/main/java/biz/nynja/account/repositories/AccountRepositoryAdditionalImpl.java +++ b/src/main/java/biz/nynja/account/repositories/AccountRepositoryAdditionalImpl.java @@ -15,6 +15,9 @@ import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import biz.nynja.account.repositories.batch.SagaTransaction; +import biz.nynja.account.repositories.batch.Transaction; +import biz.nynja.account.services.erlang.ErlangAccountBridge; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.cassandra.core.CassandraBatchOperations; @@ -36,6 +39,7 @@ import biz.nynja.account.grpc.CompletePendingAccountCreationRequest; import biz.nynja.account.grpc.ContactType; import biz.nynja.account.grpc.ErrorResponse.Cause; import biz.nynja.account.grpc.Role; +import biz.nynja.account.grpc.SearchableOption; import biz.nynja.account.grpc.UpdateAccountRequest; import biz.nynja.account.models.Account; import biz.nynja.account.models.AccountBuilder; @@ -70,7 +74,7 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio private final PendingAccountRepository pendingAccountRepository; private final AccountServiceHelper accountServiceHelper; private final Session session; - private final ErlangAccountHttpBridge erlangAccountBridge; + private final ErlangAccountBridge erlangAccountBridge; private final PermissionsValidator permissionsValidator; public AccountRepositoryAdditionalImpl(PendingAccountConfiguration pendingAccountConfiguration, @@ -81,7 +85,7 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio ProfileByAuthenticationProviderRepository profileByAuthenticationProviderRepository, PendingAccountByAuthenticationProviderRepository pendingAccountByAuthenticationProviderRepository, PendingAccountRepository pendingAccountRepository, AccountServiceHelper accountServiceHelper, - Session session, ErlangAccountHttpBridge erlangAccountBridge, PermissionsValidator permissionsValidator, + Session session, ErlangAccountBridge erlangAccountBridge, PermissionsValidator permissionsValidator, AccountDataConfiguration accountDataConfiguration) { this.pendingAccountConfiguration = pendingAccountConfiguration; this.cassandraTemplate = cassandraTemplate; @@ -102,7 +106,7 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio } public Account completePendingAccountCreation(CompletePendingAccountCreationRequest request) { - CassandraBatchOperations batchOperations = cassandraTemplate.batchOps(); + Transaction sagaTransaction = new SagaTransaction(cassandraTemplate); PendingAccount pendingAccount = pendingAccountRepository .findByAccountId(UUID.fromString(request.getAccountId())); if (pendingAccount == null) { @@ -119,14 +123,29 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio pendingAccount.getAuthenticationProviderType(), pendingAccount.getAuthenticationProvider()); return null; } + if (accountRepository.findByAccountId(pendingAccount.getAccountId()) != null) { + logger.error("The account id {} is already used.", pendingAccount.getAccountId()); + pendingAccountRepository.deleteById(pendingAccount.getAccountId()); + return null; + } + if (profileRepository.findByProfileId(pendingAccount.getProfileId()) != null) { + logger.error("The profile id {} is already used.", pendingAccount.getProfileId()); + pendingAccountRepository.deleteById(pendingAccount.getAccountId()); + return null; + } Long timeCreated = Instant.now().toEpochMilli(); WriteResult wr; try { - newAccountInsert(batchOperations, request, pendingAccount, timeCreated); - newProfileByAuthenticationProviderInsert(batchOperations, pendingAccount); - newProfileInsert(batchOperations, request, pendingAccount, timeCreated); - wr = batchOperations.execute(); + Account account = newAccountInsert(sagaTransaction, request, pendingAccount, timeCreated); + newProfileByAuthenticationProviderInsert(sagaTransaction, pendingAccount); + Profile profile = newProfileInsert(sagaTransaction, request, pendingAccount, timeCreated); + wr = sagaTransaction.execute(); + if (!erlangAccountBridge.createProfile(profile, account)) { + logger.error("Internal error with erlang"); + sagaTransaction.rollBack(); + return null; + } } catch (IllegalArgumentException | IllegalStateException e) { logger.info("Exception while completing pending account creation."); logger.debug("Exception while completing pending account creation: {} ...", e.getMessage()); @@ -174,7 +193,7 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio } public Account updateAccount(UpdateAccountRequest request) { - CassandraBatchOperations batchOperations = cassandraTemplate.batchOps(); + Transaction sagaTransaction = new SagaTransaction(cassandraTemplate); Account existingAccount = accountRepository.findByAccountId(UUID.fromString(request.getAccountId())); if (existingAccount == null) { logger.error("Existing account with the provided id {} was not found.", request.getAccountId()); @@ -185,12 +204,17 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio WriteResult wr = null; try { try { - updateAccountData(batchOperations, request, existingAccount, timeUpdated); + updateAccountData(sagaTransaction, request, existingAccount, timeUpdated); } catch (DateTimeException e) { logger.error("Exception with birthday date while updating account with id {}", request.getAccountId()); return null; } - wr = batchOperations.execute(); + wr = sagaTransaction.execute(); + if (!erlangAccountBridge.updateAccount(existingAccount)) { + logger.error("Internal error with erlang"); + sagaTransaction.rollBack(); + return null; + } } catch (IllegalArgumentException | IllegalStateException e) { logger.error("Exception while updating account with id {}.", request.getAccountId()); logger.debug("Exception while updating account: {}.", e.getMessage()); @@ -208,7 +232,7 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio } public boolean deleteAccount(UUID accountId) { - CassandraBatchOperations batchOperations = cassandraTemplate.batchOps(); + Transaction sagaTransaction = new SagaTransaction(cassandraTemplate); Account existingAccount = accountRepository.findByAccountId(accountId); if (!doExistAccountAndProfileToDelete(accountId)) { return false; @@ -224,23 +248,34 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio boolean alsoDeleteProfile = existingAccountsForProfile.size() == 1; WriteResult wr = null; try { - deleteAccountData(batchOperations, existingAccount); - deleteProfileByAuthenticationProvider(batchOperations, existingAccount.getProfileId(), - AuthenticationProvider.createAuthenticationProviderFromStringsWithDefaultSearchableOption( - existingAccount.getAuthenticationProviderType(), - existingAccount.getAuthenticationProvider())); + deleteAccountData(sagaTransaction, existingAccount); if (alsoDeleteProfile) { - if (existingProfile.getAuthenticationProviders() != null) { - deleteAuthenticationProvidersFromProfile(batchOperations, existingProfile.getProfileId(), + if (existingProfile.getAuthenticationProviders() != null + && existingProfile.getAuthenticationProviders().size() > 0) { + deleteAuthenticationProvidersFromProfile(sagaTransaction, existingProfile.getProfileId(), existingProfile.getAuthenticationProviders()); } - deleteProfileData(batchOperations, existingProfile); + deleteProfileData(sagaTransaction, existingProfile); } else { - if (!removeCreationProvider(batchOperations, existingAccount, existingProfile)) { + if (existingAccount.getAuthenticationProviderType() != null + && existingAccount.getAuthenticationProvider() != null) { + deleteProfileByAuthenticationProvider(sagaTransaction, existingAccount.getProfileId(), + AuthenticationProvider.createAuthenticationProviderFromStringsWithDefaultSearchableOption( + existingAccount.getAuthenticationProviderType(), + existingAccount.getAuthenticationProvider())); + } + if (!removeCreationProvider(sagaTransaction, existingAccount, existingProfile)) { + logger.error("Error deleting account {}", existingAccount.getAccountId()); return false; } } - wr = batchOperations.execute(); + wr = sagaTransaction.execute(); + + if (!erlangAccountBridge.deleteAccount(existingProfile.getProfileId(), accountId)) { + logger.error("Internal error with erlang"); + sagaTransaction.rollBack(); + return false; + } } catch (IllegalArgumentException | IllegalStateException e) { logger.error("Exception while deleting account: {}.", e.getMessage()); return false; @@ -265,14 +300,22 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio // update authentication providers of the profile by removing the one of the deleted account (if not // already manually removed by the user) Long timeUpdated = Instant.now().toEpochMilli(); - if (existingProfile.getAuthenticationProviders() != null) { + if (existingProfile.getAuthenticationProviders() != null + && existingProfile.getAuthenticationProviders().size() > 0) { Set existingAuthenticationProvidersSet = new HashSet( existingProfile.getAuthenticationProviders()); + if (existingAccount.getAuthenticationProviderType() == null + && existingAccount.getAuthenticationProvider() == null) { + // The creation provider is already deleted + return true; + } Optional existingCreationProviderToDelete = AuthenticationProvider .foundExistingAuthenticationProviderByTypeAndValue(existingAccount.getAuthenticationProviderType(), existingAccount.getAuthenticationProvider(), existingAuthenticationProvidersSet); if (!existingCreationProviderToDelete.isPresent()) { - logger.error("Error removing creation provider from account {}.", existingAccount.getAccountId()); + logger.error( + "The creation provider for account {} was not found in the list of the profile's authentication providers.", + existingAccount.getAccountId()); return false; } @@ -283,6 +326,7 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio if (existingAuthenticationProvidersSet.size() > 0) { updateAuthProvidersInProfileWhenDeletingAccount(batchOperations, existingProfile, existingAuthenticationProvidersSet, timeUpdated); + return true; } else { logger.error( "Error deleting account. At least one authentication provider should exist in profile: {}.", @@ -290,8 +334,11 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio return false; } } + logger.error("Error removing creation provider from account {}.", existingAccount.getAccountId()); + return false; } - return true; + logger.error("No authentication providers found for profile {}", existingProfile.getProfileId()); + return false; } private void updateAccountData(CassandraBatchOperations batchOps, UpdateAccountRequest request, @@ -414,8 +461,9 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio WriteResult wr = null; try { - deleteProfileAccountsWhenDeletingProfile(profileId, batchOperations, existingAccountsForProfile); - if (existingProfile.getAuthenticationProviders() != null) { + deleteProfileAccountsWhenDeletingProfile(batchOperations, existingAccountsForProfile); + if (existingProfile.getAuthenticationProviders() != null + && existingProfile.getAuthenticationProviders().size() > 0) { deleteAuthenticationProvidersFromProfile(batchOperations, existingProfile.getProfileId(), existingProfile.getAuthenticationProviders()); } @@ -432,15 +480,11 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio return Optional.of(Cause.ERROR_DELETING_PROFILE); } - private void deleteProfileAccountsWhenDeletingProfile(UUID profileId, CassandraBatchOperations batchOperations, + private void deleteProfileAccountsWhenDeletingProfile(CassandraBatchOperations batchOperations, List existingAccountsForProfile) { for (AccountByProfileId accountByProfileId : existingAccountsForProfile) { Account existingAccount = accountRepository.findByAccountId(accountByProfileId.getAccountId()); deleteAccountData(batchOperations, existingAccount); - deleteProfileByAuthenticationProvider(batchOperations, profileId, - AuthenticationProvider.createAuthenticationProviderFromStringsWithDefaultSearchableOption( - existingAccount.getAuthenticationProviderType(), - existingAccount.getAuthenticationProvider())); } } @@ -454,6 +498,20 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio return false; } + public boolean accountIdAlreadyUsed(UUID accountId) { + if (accountRepository.findByAccountId(accountId) != null) { + return true; + } + return false; + } + + public boolean profileIdAlreadyUsed(UUID profileId) { + if (profileRepository.findByProfileId(profileId) != null) { + return true; + } + return false; + } + @Override public boolean addAuthenticationProvider(UUID profileId, AuthenticationProvider authProvider) { BatchStatement batch = new BatchStatement(); @@ -747,6 +805,68 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio return false; } + @Override + public boolean updateSearchableOption(UUID profileId, String authProviderType, String authProvider, + SearchableOption searchableOption) { + + if (!haveNeededSearchableOptionData(profileId, authProviderType, authProvider, searchableOption)) { + return false; + } + + Profile existingProfile = profileRepository.findByProfileId(profileId); + if (existingProfile == null) { + logger.error( + "Error updating searchable option in profile {}. Existing profile with the provided id {} was not found.", + profileId); + return false; + } + + ProfileByAuthenticationProvider existingProfileByAuthenticationProvider = profileByAuthenticationProviderRepository + .findByAuthenticationProviderAndAuthenticationProviderType(authProvider, authProviderType); + if (existingProfileByAuthenticationProvider == null) { + logger.error( + "Error updating searchable option in profileByAuthenticationProvider. Existing profileByAuthenticationProvider for {}:{} was not found.", + authProviderType, authProvider); + return false; + } + + CassandraBatchOperations batchOperations = cassandraTemplate.batchOps(); + WriteResult wr = null; + try { + updateSearchableInProfileByAuthenticationProvider(batchOperations, existingProfileByAuthenticationProvider, + searchableOption); + if (!updateSearchableInProfile(batchOperations, existingProfile, authProviderType, authProvider, + searchableOption)) { + return false; + } + wr = batchOperations.execute(); + } catch (IllegalArgumentException | IllegalStateException e) { + logger.debug("Exception while updating searchable option for auth provider {}:{} in profile{}: {}.", + authProviderType, authProvider, profileId, e.getMessage()); + } + if (wr != null && wr.wasApplied()) { + logger.info("Successfully updated searchable option in profile {}.", profileId); + return true; + } + return false; + } + + private boolean haveNeededSearchableOptionData(UUID profileId, String authProviderType, String authProvider, + SearchableOption searchableOption) { + if (authProviderType == null || authProvider == null || authProviderType.isEmpty() || authProvider.isEmpty()) { + logger.error( + "Error updating searchable option in profile {}. Auth provider info (type and/or identifier) is missing.", + profileId); + return false; + } + if (searchableOption == null) { + logger.error("Error updating searchable option {}:{} in profile {}. Can not use null for searchableOption.", + authProviderType, authProvider, profileId); + return false; + } + return true; + } + @Override public Optional getAccountByLoginOption(AuthenticationProvider loginOption) throws IncorrectAccountCountException { @@ -887,11 +1007,12 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio CassandraBatchOperations batchOperations = cassandraTemplate.batchOps(); WriteResult wr; try { - setDefaultSearchableInProfileByAuthenticationProvider(batchOperations, - profilesByAuthenticationProvider.get(i)); - if (!setDefaultSearchableInProfile(batchOperations, profileToUpdate, + updateSearchableInProfileByAuthenticationProvider(batchOperations, + profilesByAuthenticationProvider.get(i), SearchableOption.SEARCH_ENABLED); + if (!updateSearchableInProfile(batchOperations, profileToUpdate, profilesByAuthenticationProvider.get(i).getAuthenticationProviderType(), - profilesByAuthenticationProvider.get(i).getAuthenticationProvider())) { + profilesByAuthenticationProvider.get(i).getAuthenticationProvider(), + SearchableOption.SEARCH_ENABLED)) { logger.error( "Error replacing null with default searchable option for profile {}: auth provider {}:{}.", profilesByAuthenticationProvider.get(i).getProfileId(), @@ -915,14 +1036,14 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio } } - private void setDefaultSearchableInProfileByAuthenticationProvider(CassandraBatchOperations batchOps, - ProfileByAuthenticationProvider profileByAuthenticationProvider) { - profileByAuthenticationProvider.setSearchable(Boolean.TRUE); + private void updateSearchableInProfileByAuthenticationProvider(CassandraBatchOperations batchOps, + ProfileByAuthenticationProvider profileByAuthenticationProvider, SearchableOption searchableOption) { + profileByAuthenticationProvider.setSearchable(AuthenticationProvider.isSearchDisabled(searchableOption)); batchOps.update(profileByAuthenticationProvider); } - private boolean setDefaultSearchableInProfile(CassandraBatchOperations batchOps, Profile profileToUpdate, - String authProviderType, String authProvider) { + private boolean updateSearchableInProfile(CassandraBatchOperations batchOps, Profile profileToUpdate, + String authProviderType, String authProvider, SearchableOption searchableOption) { Set authProviderSet = profileToUpdate.getAuthenticationProviders(); if (authProviderSet == null) { logger.error("No existing auth providers found for profile {}.", profileToUpdate.getProfileId()); @@ -930,7 +1051,8 @@ public class AccountRepositoryAdditionalImpl implements AccountRepositoryAdditio } AuthenticationProvider authProviderToUpdate = AuthenticationProvider - .createAuthenticationProviderWithSearchableOption(authProviderType, authProvider, Boolean.TRUE); + .createAuthenticationProviderWithSearchableOption(authProviderType, authProvider, + AuthenticationProvider.isSearchDisabled(searchableOption)); Optional currentAuthProvider = AuthenticationProvider .foundExistingAuthenticationProviderByTypeAndValue(authProviderType, authProvider, authProviderSet); if (!currentAuthProvider.isPresent()) { diff --git a/src/main/java/biz/nynja/account/services/AccountServiceImpl.java b/src/main/java/biz/nynja/account/services/AccountServiceImpl.java index ae63d023c7b8016a3c5cd227aacd77527cceb196..72542ac18d938c727e46fa841fa3ff562b2775ba 100644 --- a/src/main/java/biz/nynja/account/services/AccountServiceImpl.java +++ b/src/main/java/biz/nynja/account/services/AccountServiceImpl.java @@ -18,6 +18,8 @@ import org.lognet.springboot.grpc.GRpcService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import biz.nynja.account.configuration.AccountDataConfiguration; +import biz.nynja.account.configuration.ProfileDataConfiguration; import biz.nynja.account.grpc.AccountByAccountIdRequest; import biz.nynja.account.grpc.AccountResponse; import biz.nynja.account.grpc.AccountServiceGrpc; @@ -50,6 +52,7 @@ import biz.nynja.account.grpc.SearchResultDetails; import biz.nynja.account.grpc.StatusResponse; import biz.nynja.account.grpc.UpdateAccountRequest; import biz.nynja.account.grpc.UpdateAuthenticationProviderRequest; +import biz.nynja.account.grpc.UpdateSearchableOptionRequest; import biz.nynja.account.models.Account; import biz.nynja.account.models.AccountByProfileId; import biz.nynja.account.models.AccountByQrCode; @@ -99,6 +102,7 @@ public class AccountServiceImpl extends AccountServiceGrpc.AccountServiceImplBas private final AccountCreator accountCreator; private final ProfileProvider profileProvider; private final PermissionsValidator permissionsValidator; + private final ProfileDataConfiguration profileDataConfiguration; public AccountServiceImpl(AccountRepositoryAdditional accountRepositoryAdditional, ProfileRepository profileRepository, @@ -107,7 +111,7 @@ public class AccountServiceImpl extends AccountServiceGrpc.AccountServiceImplBas AccountByUsernameRepository accountByUsernameRepository, AccountProvider accountProvider, AccountByProfileIdRepository accountByProfileIdRepository, PhoneNumberNormalizer phoneNumberNormalizer, AccountCreator accountCreator, ProfileProvider profileProvider, - PermissionsValidator permissionsValidator) { + PermissionsValidator permissionsValidator, ProfileDataConfiguration profileDataConfiguration) { this.accountRepositoryAdditional = accountRepositoryAdditional; this.profileRepository = profileRepository; this.profileByAutheticationProviderRepository = profileByAutheticationProviderRepository; @@ -119,6 +123,7 @@ public class AccountServiceImpl extends AccountServiceGrpc.AccountServiceImplBas this.accountCreator = accountCreator; this.profileProvider = profileProvider; this.permissionsValidator = permissionsValidator; + this.profileDataConfiguration = profileDataConfiguration; } @Override @@ -420,7 +425,7 @@ public class AccountServiceImpl extends AccountServiceGrpc.AccountServiceImplBas @Permitted(role = RoleConstants.USER) public void getAccountByAccountId(AccountByAccountIdRequest request, StreamObserver responseObserver) { - logger.info("Getting accounts by account id: {}", request.getAccountId()); + logger.info("Getting account by account id: {}", request.getAccountId()); if ((request.getAccountId() == null) || (request.getAccountId().isEmpty())) { logAndBuildGrpcAccountResponse(responseObserver, AccountResponse.newBuilder(), "Missing account id", "", @@ -471,7 +476,7 @@ public class AccountServiceImpl extends AccountServiceGrpc.AccountServiceImplBas public void completePendingAccountCreation(CompletePendingAccountCreationRequest request, StreamObserver responseObserver) { - logger.info("Complete pending account creation..."); + logger.info("Complete pending account creation for account id {} ...", request.getAccountId()); logger.debug("Complete pending account creation...: {} ...", request); AccountResponse response = accountCreator.retrieveCompletePendingAccountResponse(request); @@ -492,6 +497,12 @@ public class AccountServiceImpl extends AccountServiceGrpc.AccountServiceImplBas Cause.MISSING_ACCOUNT_ID); return; } + if (!util.isValidUuid(request.getAccountId())) { + logAndBuildGrpcAccountResponse(responseObserver, AccountResponse.newBuilder(), "Invalid account id: {}", + request.getAccountId(), Cause.INVALID_ACCOUNT_ID); + return; + } + if (!permissionsValidator.isRpcAllowed(request.getAccountId())) { logAndBuildGrpcAccountResponse(responseObserver, AccountResponse.newBuilder(), "Can not update account {}.", request.getAccountId(), Cause.ERROR_PERMISSION_DENIED); @@ -538,17 +549,17 @@ public class AccountServiceImpl extends AccountServiceGrpc.AccountServiceImplBas Cause.MISSING_ACCOUNT_ID); return; } - if (!permissionsValidator.isRpcAllowed(request.getAccountId())) { - logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), "Can not delete account {}.", - request.getAccountId(), Cause.ERROR_PERMISSION_DENIED); - return; - } - if (!util.isValidUuid(request.getAccountId())) { + logger.info("Can not delete account. Invalid account id: {}", request.getAccountId()); logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), "Invalid account id: {}", request.getAccountId(), Cause.INVALID_ACCOUNT_ID); return; } + if (!permissionsValidator.isRpcAllowed(request.getAccountId())) { + logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), "Can not delete account {}.", + request.getAccountId(), Cause.ERROR_PERMISSION_DENIED); + return; + } boolean wasAccountDeleted = accountRepositoryAdditional.deleteAccount(UUID.fromString(request.getAccountId())); if (wasAccountDeleted) { @@ -608,6 +619,11 @@ public class AccountServiceImpl extends AccountServiceGrpc.AccountServiceImplBas Cause.MISSING_PROFILE_ID); return; } + if (!util.isValidUuid(request.getProfileId())) { + logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), "Invalid profile id: {}", + request.getProfileId(), Cause.INVALID_PROFILE_ID); + return; + } List existingAccountsForProfile = accountByProfileIdRepository .findAllByProfileId(UUID.fromString(request.getProfileId())); @@ -646,6 +662,13 @@ public class AccountServiceImpl extends AccountServiceGrpc.AccountServiceImplBas return; } + // Make sure there will be no more than providers in this profile + if(profile.getAuthenticationProviders().size() >= profileDataConfiguration.getMaxAuthenticationprovidersPerProfile()) { + logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), + "Max number of authentication providers reached for profile id {}.", request.getProfileId(), Cause.MAX_PROVIDERS_PER_PROFILE_REACHED); + return; + } + // Make sure that the requested authentication provider is not already used in the system. ProfileByAuthenticationProvider profileByAuthProvider = profileByAutheticationProviderRepository .findByAuthenticationProviderAndAuthenticationProviderType( @@ -690,6 +713,11 @@ public class AccountServiceImpl extends AccountServiceGrpc.AccountServiceImplBas validationResult.get().getValue(), "", validationResult.get().getKey()); return; } + if (!util.isValidUuid(request.getAccountId())) { + logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), "Invalid account id: {}", + request.getAccountId(), Cause.INVALID_ACCOUNT_ID); + return; + } if (!permissionsValidator.isRpcAllowed(request.getAccountId())) { logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), @@ -743,6 +771,11 @@ public class AccountServiceImpl extends AccountServiceGrpc.AccountServiceImplBas validationResult.get().getRight(), validationResult.get().getLeft()); return; } + if (!util.isValidUuid(request.getAccountId())) { + logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), "Invalid account id: {}", + request.getAccountId(), Cause.INVALID_ACCOUNT_ID); + return; + } if (!permissionsValidator.isRpcAllowed(request.getAccountId())) { logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), @@ -783,6 +816,11 @@ public class AccountServiceImpl extends AccountServiceGrpc.AccountServiceImplBas Cause.MISSING_PROFILE_ID); return; } + if (!util.isValidUuid(request.getProfileId())) { + logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), "Invalid profile id: {}", + request.getProfileId(), Cause.INVALID_PROFILE_ID); + return; + } if (request.getAuthenticationProvider().getAuthenticationTypeValue() == 0) { logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), "Missing auth provider type.", @@ -924,8 +962,8 @@ public class AccountServiceImpl extends AccountServiceGrpc.AccountServiceImplBas @Permitted(role = RoleConstants.USER) public void editContactInfoForAccount(EditContactInfoRequest request, StreamObserver responseObserver) { - logger.info("Removing contact info from account requested."); - logger.debug("Removing contact info from account requested: {}", request); + logger.info("Editing contact info for account requested."); + logger.debug("Editing contact info for account requested: {}", request); Optional> validationResultEditContactInfo = account.validateEditContactInfoRequest( request.getAccountId(), request.getOldContactInfo(), request.getEditedContactInfo()); @@ -935,6 +973,11 @@ public class AccountServiceImpl extends AccountServiceGrpc.AccountServiceImplBas validationResultEditContactInfo.get().getKey()); return; } + if (!util.isValidUuid(request.getAccountId())) { + logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), "Invalid account id: {}", + request.getAccountId(), Cause.INVALID_ACCOUNT_ID); + return; + } if (!permissionsValidator.isRpcAllowed(request.getAccountId())) { logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), @@ -1017,6 +1060,11 @@ public class AccountServiceImpl extends AccountServiceGrpc.AccountServiceImplBas validationResultUpdateAuthProvider.get().getKey()); return; } + if (!util.isValidUuid(request.getProfileId())) { + logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), "Invalid profile id: {}.", + request.getProfileId(), validationResultUpdateAuthProvider.get().getKey()); + return; + } List existingAccountsForProfile = accountByProfileIdRepository .findAllByProfileId(UUID.fromString(request.getProfileId())); @@ -1184,4 +1232,50 @@ public class AccountServiceImpl extends AccountServiceGrpc.AccountServiceImplBas responseObserver.onCompleted(); } + @Override + @PerformPermissionCheck + @Permitted(role = RoleConstants.ACCOUNT_ADMIN) + @Permitted(role = RoleConstants.USER) + public void updateSearchableOption(UpdateSearchableOptionRequest request, + StreamObserver responseObserver) { + logger.info("Updating searchable option for profile {} requested.", request.getProfileId()); + logger.debug("Updating searchable option for profile requested: {}", request); + + Validation validation = authenticationProvider.validateUpdateSearchableOptionRequest(request.getProfileId(), + request.getAuthenticationType(), request.getAuthenticationIdentifier()); + if (validation.hasErrors()) { + logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), validation.getErrorMessage(), + "", validation.getCause().get()); + return; + } + if (!util.isValidUuid(request.getProfileId())) { + logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), "Invalid profile id: {}", + request.getProfileId(), Cause.INVALID_PROFILE_ID); + return; + } + + List existingAccountsForProfile = accountByProfileIdRepository + .findAllByProfileId(UUID.fromString(request.getProfileId())); + + if (!permissionsValidator.isRpcAllowed(existingAccountsForProfile)) { + logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), + "Can not update searchable option for authentication provider in profile {}.", + request.getProfileId(), Cause.ERROR_PERMISSION_DENIED); + return; + } + + if (!accountRepositoryAdditional.updateSearchableOption(UUID.fromString(request.getProfileId()), + request.getAuthenticationType(), request.getAuthenticationIdentifier(), request.getSearchOption())) { + logAndBuildGrpcStatusResponse(responseObserver, StatusResponse.newBuilder(), + "Can not update searchable option for authentication provider in profile {}.", + request.getProfileId(), Cause.ERROR_UPDATING_SEARCHABLE_OPTION); + return; + } + + logger.info("Successfully updated authentication provider {}:{} with searchable option {} in profile {}", + request.getAuthenticationType(), request.getAuthenticationIdentifier(), request.getSearchOption(), + request.getProfileId()); + responseObserver.onNext(StatusResponse.newBuilder().setStatus("SUCCESS").build()); + responseObserver.onCompleted(); + } } diff --git a/src/main/java/biz/nynja/account/services/decomposition/AccountCreator.java b/src/main/java/biz/nynja/account/services/decomposition/AccountCreator.java index d4d507d174c6c80839ea4b2896b47529824e4d46..9123af320b46845f216fd640adba58e85373a228 100644 --- a/src/main/java/biz/nynja/account/services/decomposition/AccountCreator.java +++ b/src/main/java/biz/nynja/account/services/decomposition/AccountCreator.java @@ -80,6 +80,17 @@ public class AccountCreator { pendingAccount.setAccountId(UUID.randomUUID()); pendingAccount.setProfileId(UUID.randomUUID()); + if (pendingAccountRepository.findByAccountId(pendingAccount.getAccountId()) != null + || accountRepositoryAdditional.accountIdAlreadyUsed(pendingAccount.getAccountId())) { + logger.error("Can not create pending account. The account id {} is already used.", pendingAccount.getAccountId()); + return CreatePendingAccountResponse.newBuilder() + .setError(ErrorResponse.newBuilder().setCause(Cause.INTERNAL_SERVER_ERROR)).build(); + } + if (accountRepositoryAdditional.profileIdAlreadyUsed(pendingAccount.getProfileId())) { + logger.error("Can not create pending account. The profile id {} is already used.", pendingAccount.getProfileId()); + return CreatePendingAccountResponse.newBuilder() + .setError(ErrorResponse.newBuilder().setCause(Cause.INTERNAL_SERVER_ERROR)).build(); + } pendingAccount.setCreationTimestamp(Instant.now().toEpochMilli()); try { diff --git a/src/main/java/biz/nynja/account/services/decomposition/AccountProvider.java b/src/main/java/biz/nynja/account/services/decomposition/AccountProvider.java index e912b9c96fbb62bc4240ac0b7448110ce9231c19..0e5bd087ca0b14507469a98646be31415c51f53a 100644 --- a/src/main/java/biz/nynja/account/services/decomposition/AccountProvider.java +++ b/src/main/java/biz/nynja/account/services/decomposition/AccountProvider.java @@ -164,7 +164,6 @@ public class AccountProvider { } public Optional getAccountByAccountId(AccountByAccountIdRequest request) { - logger.info("Getting accounts by account id: {}", request.getAccountId()); Account account = accountRepository.findByAccountId(UUID.fromString(request.getAccountId())); if (account == null || account.getAccountId() == null) { return Optional.empty(); diff --git a/src/main/java/biz/nynja/account/services/decomposition/ProfileProvider.java b/src/main/java/biz/nynja/account/services/decomposition/ProfileProvider.java index 9c138bfb73fc3f8bb0f6de6fe0d9e367d40d70c9..a4436e304c112ac928ae8fd6bb181172bde898da 100644 --- a/src/main/java/biz/nynja/account/services/decomposition/ProfileProvider.java +++ b/src/main/java/biz/nynja/account/services/decomposition/ProfileProvider.java @@ -24,7 +24,6 @@ public class ProfileProvider { } public Optional getProfileByProfileId(ProfileByProfileIdRequest request) { - logger.info("Getting profile by profile id: {}", request.getProfileId()); Profile profile = profileRepository.findByProfileId(UUID.fromString(request.getProfileId())); if (profile == null || profile.getProfileId() == null) { return Optional.empty(); diff --git a/src/main/java/biz/nynja/account/services/erlang/ErlangAccountBridge.java b/src/main/java/biz/nynja/account/services/erlang/ErlangAccountBridge.java index 803db0ed0bfad8c77cd95b663424be7e786790a9..f30b96de8e78a525154c2a33ca21fdac1631e159 100644 --- a/src/main/java/biz/nynja/account/services/erlang/ErlangAccountBridge.java +++ b/src/main/java/biz/nynja/account/services/erlang/ErlangAccountBridge.java @@ -6,18 +6,18 @@ package biz.nynja.account.services.erlang; import biz.nynja.account.models.Account; import biz.nynja.account.models.Profile; +import java.util.List; import java.util.UUID; - public interface ErlangAccountBridge { boolean createProfile(Profile profile, Account defaultAccount); - boolean deleteProfile(UUID profileId); + boolean deleteProfile(UUID profileId, List accountsIds); boolean createAccount(Account account); boolean updateAccount(Account account); - boolean deleteAccount(UUID accountId); + boolean deleteAccount(UUID profileId, UUID accountId); } diff --git a/src/main/java/biz/nynja/account/services/erlang/ErlangAccountHttpBridge.java b/src/main/java/biz/nynja/account/services/erlang/ErlangAccountHttpBridge.java index aa88d2f435dbf10bc6552b1611965abf54070b8b..24574f35ea38ecc756361d9b13a652bfef73f863 100644 --- a/src/main/java/biz/nynja/account/services/erlang/ErlangAccountHttpBridge.java +++ b/src/main/java/biz/nynja/account/services/erlang/ErlangAccountHttpBridge.java @@ -1,11 +1,12 @@ /** - * Copyright (C) 2018 Nynja Inc. All rights reserved. + * Copyright (C) 2018 Nynja Inc. All rights reserved. */ package biz.nynja.account.services.erlang; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.List; import java.util.UUID; import org.slf4j.Logger; @@ -22,13 +23,11 @@ import biz.nynja.account.models.Account; import biz.nynja.account.models.Profile; import biz.nynja.account.services.erlang.connector.HttpClient; - - @Service // TODO: 11/19/2018 change boolean response when ENC will implement return error part public class ErlangAccountHttpBridge implements ErlangAccountBridge { private static final Logger logger = LoggerFactory.getLogger(ErlangAccountHttpBridge.class); - + private static String PROTOCOL = "https://"; private final JsonParser parser = new JsonParser(); private final ObjectMapper mapper = new ObjectMapper(); @@ -39,6 +38,7 @@ public class ErlangAccountHttpBridge implements ErlangAccountBridge { public ErlangAccountHttpBridge(HttpClient httpClient, ErlangBridgeConfiguration erlangBridgeConfiguration) throws MalformedURLException { + this.httpClient = httpClient; this.erlangBridgeConfiguration = erlangBridgeConfiguration; if (!erlangBridgeConfiguration.isEnabled()) { @@ -46,9 +46,9 @@ public class ErlangAccountHttpBridge implements ErlangAccountBridge { profileURL = null; return; } - accountURL = new URL( + accountURL = new URL(PROTOCOL + erlangBridgeConfiguration.getHost() + ":" + erlangBridgeConfiguration.getPort() + "/swm/account"); - profileURL = new URL( + profileURL = new URL(PROTOCOL + erlangBridgeConfiguration.getHost() + ":" + erlangBridgeConfiguration.getPort() + "/swm/profile"); } @@ -66,7 +66,7 @@ public class ErlangAccountHttpBridge implements ErlangAccountBridge { } @Override - public boolean deleteProfile(UUID profileId) { + public boolean deleteProfile(UUID profileId, List accountsIds) { if (!erlangBridgeConfiguration.isEnabled()) return true; JsonObject profileObject = new JsonObject(); @@ -77,7 +77,7 @@ public class ErlangAccountHttpBridge implements ErlangAccountBridge { return false; } - return false; + return true; } @Override @@ -99,7 +99,7 @@ public class ErlangAccountHttpBridge implements ErlangAccountBridge { } @Override - public boolean deleteAccount(UUID accountId) { + public boolean deleteAccount(UUID profileId, UUID accountId) { if (!erlangBridgeConfiguration.isEnabled()) return true; JsonObject accountObject = new JsonObject(); @@ -109,7 +109,7 @@ public class ErlangAccountHttpBridge implements ErlangAccountBridge { } catch (IOException e) { return false; } - return false; + return true; } private JsonObject prepareProfileJsonObject(Profile profile, Account account) { diff --git a/src/main/java/biz/nynja/account/services/erlang/ErlangAccountMqttBridge.java b/src/main/java/biz/nynja/account/services/erlang/ErlangAccountMqttBridge.java new file mode 100644 index 0000000000000000000000000000000000000000..3d0832c20d3c8ecf13cf99f0ae15024249800a99 --- /dev/null +++ b/src/main/java/biz/nynja/account/services/erlang/ErlangAccountMqttBridge.java @@ -0,0 +1,128 @@ +/** + * Copyright (C) 2018 Nynja Inc. All rights reserved. + */ +package biz.nynja.account.services.erlang; + +import java.net.MalformedURLException; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +import biz.nynja.account.configuration.ErlangBridgeConfiguration; +import biz.nynja.account.models.Account; +import biz.nynja.account.models.Profile; +import biz.nynja.account.services.erlang.interceptor.TokenInterceptorConstants; +import biz.nynja.bridge.grpc.AccountBridgeGrpc; +import biz.nynja.bridge.grpc.AccountData; +import biz.nynja.bridge.grpc.BridgeSuccessResponse; +import biz.nynja.bridge.grpc.ProfileData; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import io.grpc.stub.MetadataUtils; + +@Component +@Primary +public class ErlangAccountMqttBridge implements ErlangAccountBridge { + + private final ErlangBridgeConfiguration erlangBridgeConfiguration; + + public ErlangAccountMqttBridge(ErlangBridgeConfiguration erlangBridgeConfiguration) throws MalformedURLException { + this.erlangBridgeConfiguration = erlangBridgeConfiguration; + } + + @Override + public boolean createProfile(Profile profile, Account account) { + + if (!erlangBridgeConfiguration.isEnabled()) + return true; + ProfileData profileData = buildProfileData(profile, account); + // todo update after testing with real connection + BridgeSuccessResponse response = buildGrpcConnection().createProfile(profileData); + return true; + } + + @Override + public boolean deleteProfile(UUID profileId, List accountsIds) { + if (!erlangBridgeConfiguration.isEnabled()) + return true; + BridgeSuccessResponse response = buildGrpcConnection() + .deleteProfile(buildDeleteProfileData(profileId, (UUID[]) accountsIds.toArray())); + return true; + } + + @Override + public boolean createAccount(Account account) { + if (!erlangBridgeConfiguration.isEnabled()) + return true; + BridgeSuccessResponse response = buildGrpcConnection().createAccount(buildAccountData(account)); + + return true; + } + + @Override + public boolean updateAccount(Account account) { + if (!erlangBridgeConfiguration.isEnabled()) + return true; + + return true; + } + + @Override + public boolean deleteAccount(UUID profileId, UUID accountId) { + if (!erlangBridgeConfiguration.isEnabled()) + return true; + BridgeSuccessResponse response = buildGrpcConnection() + .deleteAccount(buildDeleteProfileData(profileId, accountId)); + return true; + } + + private ProfileData buildProfileData(Profile profile, Account account) { + return ProfileData.newBuilder().setProfileId(profile.getProfileId().toString()) + .setDefaultAccount(buildAccountData(account)) + .setLastUpdateTimestamp(profile.getCreationTimestamp().toString()).build(); + } + + private AccountData buildAccountData(Account account) { + return AccountData.newBuilder().setAccountId(account.getAccountId().toString()) + .setFirstName(account.getFirstName()).setLastName(account.getLastName()) + .setProfileId(account.getProfileId().toString()) + .setUsername(account.getUsername()).setAvatar(account.getAvatar()) + .setLastUpdateTimestamp( + Objects.isNull(account.getLastUpdateTimestamp()) ? Long.toString(System.currentTimeMillis()) + : account.getLastUpdateTimestamp().toString()) + .build(); + } + + // Erlang protocol + private ProfileData buildDeleteProfileData(UUID profileId, UUID... accountsId) { + return ProfileData.newBuilder().setProfileId(profileId.toString()) + .addAllAccountsIds(Arrays.stream(accountsId).map(UUID::toString).collect(Collectors.toList())).build(); + } + + private AccountBridgeGrpc.AccountBridgeBlockingStub buildGrpcConnection() { + ManagedChannel managedChannel = ManagedChannelBuilder.forAddress(erlangBridgeConfiguration.getHost(), + Integer.parseInt(erlangBridgeConfiguration.getPort())).usePlaintext().build(); + AccountBridgeGrpc.AccountBridgeBlockingStub bridgeServiceBlockingStub = AccountBridgeGrpc + .newBlockingStub(managedChannel); + return MetadataUtils.attachHeaders(bridgeServiceBlockingStub, getHeaders()); + } + + /** + * Attaches the access token to the rpc header + * + * @return + * @throws InternalError + */ + private Metadata getHeaders() throws InternalError { + Metadata headers = new Metadata(); + Metadata.Key key = Metadata.Key.of("accessToken", Metadata.ASCII_STRING_MARSHALLER); + headers.put(key, "Bearer " + TokenInterceptorConstants.ACCESS_TOKEN_CTX.get()); + return headers; + } +} diff --git a/src/main/java/biz/nynja/account/services/erlang/interceptor/TokenInterceptor.java b/src/main/java/biz/nynja/account/services/erlang/interceptor/TokenInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..67de571509ab08d80f99da93b900722d325cdaed --- /dev/null +++ b/src/main/java/biz/nynja/account/services/erlang/interceptor/TokenInterceptor.java @@ -0,0 +1,19 @@ +/** + * Copyright (C) 2018 Nynja Inc. All rights reserved. + */ +package biz.nynja.account.services.erlang.interceptor; + +import io.grpc.*; +import org.lognet.springboot.grpc.GRpcGlobalInterceptor; + +@GRpcGlobalInterceptor +public class TokenInterceptor implements ServerInterceptor { + + @Override + public ServerCall.Listener interceptCall(ServerCall serverCall, Metadata metadata, + ServerCallHandler serverCallHandler) { + String accessToken = metadata.get(TokenInterceptorConstants.ACCESS_TOKEN_METADATA); + Context ctx = Context.current().withValue(TokenInterceptorConstants.ACCESS_TOKEN_CTX, accessToken); + return Contexts.interceptCall(ctx, serverCall, metadata, serverCallHandler); + } +} diff --git a/src/main/java/biz/nynja/account/services/erlang/interceptor/TokenInterceptorConstants.java b/src/main/java/biz/nynja/account/services/erlang/interceptor/TokenInterceptorConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..5df55f124e6950e9266d867261ea6aefe2cf3f02 --- /dev/null +++ b/src/main/java/biz/nynja/account/services/erlang/interceptor/TokenInterceptorConstants.java @@ -0,0 +1,11 @@ +package biz.nynja.account.services.erlang.interceptor; + +import io.grpc.Context; +import io.grpc.Metadata; + +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; + +public class TokenInterceptorConstants { + public static final Metadata.Key ACCESS_TOKEN_METADATA = Metadata.Key.of("accessToken", ASCII_STRING_MARSHALLER); + public static final Context.Key ACCESS_TOKEN_CTX = Context.key("accessToken"); +} diff --git a/src/main/java/biz/nynja/account/validation/Validators.java b/src/main/java/biz/nynja/account/validation/Validators.java index 930b836ad497429ef6461dc7f9655adf9fae24ca..179d49afcb6aaa776db4806750477d478f4d12b4 100644 --- a/src/main/java/biz/nynja/account/validation/Validators.java +++ b/src/main/java/biz/nynja/account/validation/Validators.java @@ -346,6 +346,41 @@ public class Validators { } } + public Validation validateUpdateSearchableOptionRequest(String profileId, String authenticationType, + String authenticationIdentifier) { + Validation validation = new Validation(); + if (authenticationType == null || authenticationType.isEmpty()) { + validation.addError( + new ValidationError("Missing authentication provider type", Cause.MISSING_AUTH_PROVIDER_TYPE)); + return validation; + } + + if (!authenticationType.equals(AuthenticationType.EMAIL.name()) + && !authenticationType.equals(AuthenticationType.PHONE.name())) { + validation.addError( + new ValidationError("Invalid authentication provider type", Cause.INVALID_AUTH_PROVIDER_TYPE)); + return validation; + } + + if (authenticationIdentifier == null || authenticationIdentifier.isEmpty()) { + validation.addError( + new ValidationError("Missing authentication provider identifier", Cause.MISSING_AUTH_PROVIDER_ID)); + return validation; + } + + if (profileId == null || profileId.isEmpty()) { + validation.addError(new ValidationError("Missing profile id", Cause.MISSING_PROFILE_ID)); + return validation; + } + + if (!isValidUuid(profileId)) { + validation.addError(new ValidationError("Invalid profile id", Cause.INVALID_PROFILE_ID)); + return validation; + } + + return validation; + } + private boolean isValidUuid(String id) { return id.matches("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 5de0eb4131e96a65d07733a20701b83b0977a70c..4956ef2ba7debe2b9b755cf8708bf9b2330a8c44 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -37,10 +37,13 @@ pending-account: account-data: max-contact-info-of-type: 10 +profile-data: + max-authenticationproviders-per-profile: 20 + erlang-bridge: - enable: false; - ip: - port: + enabled: false + host: localhost + port: 6580 #Metrics related configurations management: diff --git a/src/main/resources/application-production.yml b/src/main/resources/application-production.yml index e42ba70b961bb1c5d9c885988f45962254660012..178b0ec470491a24fd437ddb0f84d9023d3238bd 100644 --- a/src/main/resources/application-production.yml +++ b/src/main/resources/application-production.yml @@ -31,8 +31,11 @@ pending-account: account-data: max-contact-info-of-type: ${MAX_CONTACT_INFO_OF_TYPE:10} +profile-data: +max-authenticationproviders-per-profile: 20 + erlang-bridge: - enable: false; + enabled: ${ERLANG_ENABLED:false} ip: ${ERLANG_IP} port: ${ERLANG_PORT} diff --git a/src/test/java/biz/nynja/account/services/AccountServiceTests.java b/src/test/java/biz/nynja/account/services/AccountServiceTests.java index 116a08376a0eafb75287fbb477a30a63abc0a91a..0f0386d6f8bc2cf33c0566b5e814e28418fafc22 100644 --- a/src/test/java/biz/nynja/account/services/AccountServiceTests.java +++ b/src/test/java/biz/nynja/account/services/AccountServiceTests.java @@ -67,9 +67,11 @@ import biz.nynja.account.grpc.ProfileByProfileIdRequest; import biz.nynja.account.grpc.ProfileResponse; import biz.nynja.account.grpc.Role; import biz.nynja.account.grpc.SearchResponse; +import biz.nynja.account.grpc.SearchableOption; import biz.nynja.account.grpc.StatusResponse; import biz.nynja.account.grpc.UpdateAccountRequest; import biz.nynja.account.grpc.UpdateAuthenticationProviderRequest; +import biz.nynja.account.grpc.UpdateSearchableOptionRequest; import biz.nynja.account.models.Account; import biz.nynja.account.models.AccountByAuthenticationProvider; import biz.nynja.account.models.AccountByProfileId; @@ -2019,4 +2021,118 @@ public class AccountServiceTests extends GrpcServerTestBase { assertNotNull("Reply should not be null", reply); assertEquals(reply.getError().getCause(), Cause.INVALID_EMAIL); } + + @Test + public void testUpdateSearchableOptionPhoneOK() { + final UpdateSearchableOptionRequest request = UpdateSearchableOptionRequest.newBuilder() + .setProfileId(Util.PROFILE_ID.toString()).setAuthenticationIdentifier(Util.PHONE_PROVIDER) + .setAuthenticationType(AuthenticationType.PHONE.name()) + .setSearchOption(SearchableOption.SEARCH_DISABLED).build(); + + given(accountRepositoryAdditional.updateSearchableOption(Mockito.any(UUID.class), Mockito.any(String.class), + Mockito.any(String.class), Mockito.any(SearchableOption.class))).willReturn(true); + + given(profileRepository.findByProfileId(Util.PROFILE_ID)).willReturn(profile2AuthProviders); + + given(profileByAutheticationProviderRepository + .findByAuthenticationProviderAndAuthenticationProviderType(Util.PHONE_PROVIDER, "PHONE")) + .willReturn(profileByAuthenticationProvider); + + final StatusResponse reply = accountServiceBlockingStub.updateSearchableOption(request); + assertNotNull("Reply should not be null", reply); + assertEquals("SUCCESS", reply.getStatus()); + } + + @Test + public void testUpdateSearchableOptionEmailOK() { + final UpdateSearchableOptionRequest request = UpdateSearchableOptionRequest.newBuilder() + .setProfileId(Util.PROFILE_ID.toString()).setAuthenticationIdentifier(Util.EMAIL) + .setAuthenticationType(AuthenticationType.EMAIL.name()) + .setSearchOption(SearchableOption.SEARCH_DISABLED).build(); + + given(accountRepositoryAdditional.updateSearchableOption(Mockito.any(UUID.class), Mockito.any(String.class), + Mockito.any(String.class), Mockito.any(SearchableOption.class))).willReturn(true); + + given(profileRepository.findByProfileId(Util.PROFILE_ID)).willReturn(profile2AuthProviders); + + given(profileByAutheticationProviderRepository + .findByAuthenticationProviderAndAuthenticationProviderType(Util.EMAIL, "EMAIL")) + .willReturn(profileByAuthenticationProvider); + + final StatusResponse reply = accountServiceBlockingStub.updateSearchableOption(request); + assertNotNull("Reply should not be null", reply); + assertEquals("SUCCESS", reply.getStatus()); + } + + @Test + public void testUpdateSearchableOptionErrorUpdatingSearchableOption() { + final UpdateSearchableOptionRequest request = UpdateSearchableOptionRequest.newBuilder() + .setProfileId(Util.PROFILE_ID.toString()).setAuthenticationIdentifier(Util.PHONE_PROVIDER) + .setAuthenticationType(AuthenticationType.PHONE.name()) + .setSearchOption(SearchableOption.SEARCH_DISABLED).build(); + + given(accountRepositoryAdditional.updateSearchableOption(Mockito.any(UUID.class), Mockito.any(String.class), + Mockito.any(String.class), Mockito.any(SearchableOption.class))).willReturn(false); + + final StatusResponse reply = accountServiceBlockingStub.updateSearchableOption(request); + assertNotNull("Reply should not be null", reply); + assertEquals(reply.getError().getCause(), Cause.ERROR_UPDATING_SEARCHABLE_OPTION); + } + + @Test + public void testUpdateSearchableOptionMissingAuthenticationProviderType() { + final UpdateSearchableOptionRequest request = UpdateSearchableOptionRequest.newBuilder() + .setProfileId(Util.PROFILE_ID.toString()).setAuthenticationIdentifier(Util.PHONE_PROVIDER) + .setSearchOption(SearchableOption.SEARCH_DISABLED).build(); + + final StatusResponse reply = accountServiceBlockingStub.updateSearchableOption(request); + assertNotNull("Reply should not be null", reply); + assertEquals(reply.getError().getCause(), Cause.MISSING_AUTH_PROVIDER_TYPE); + } + + @Test + public void testUpdateSearchableOptionMissingAuthenticationProviderIdentifier() { + final UpdateSearchableOptionRequest request = UpdateSearchableOptionRequest.newBuilder() + .setProfileId(Util.PROFILE_ID.toString()).setAuthenticationType(AuthenticationType.PHONE.name()) + .setSearchOption(SearchableOption.SEARCH_DISABLED).build(); + + final StatusResponse reply = accountServiceBlockingStub.updateSearchableOption(request); + assertNotNull("Reply should not be null", reply); + assertEquals(reply.getError().getCause(), Cause.MISSING_AUTH_PROVIDER_ID); + } + + @Test + public void testUpdateSearchableOptionMissingProfileId() { + final UpdateSearchableOptionRequest request = UpdateSearchableOptionRequest.newBuilder() + .setAuthenticationIdentifier(Util.PHONE_PROVIDER).setAuthenticationType(AuthenticationType.PHONE.name()) + .setSearchOption(SearchableOption.SEARCH_DISABLED).build(); + + final StatusResponse reply = accountServiceBlockingStub.updateSearchableOption(request); + assertNotNull("Reply should not be null", reply); + assertEquals(reply.getError().getCause(), Cause.MISSING_PROFILE_ID); + } + + @Test + public void testUpdateSearchableOptionInvalidProfileId() { + final UpdateSearchableOptionRequest request = UpdateSearchableOptionRequest.newBuilder() + .setProfileId(Util.INVALID_PROFILE_ID).setAuthenticationIdentifier(Util.PHONE_PROVIDER) + .setAuthenticationType(AuthenticationType.PHONE.name()) + .setSearchOption(SearchableOption.SEARCH_DISABLED).build(); + + final StatusResponse reply = accountServiceBlockingStub.updateSearchableOption(request); + assertNotNull("Reply should not be null", reply); + assertEquals(reply.getError().getCause(), Cause.INVALID_PROFILE_ID); + } + + @Test + public void testUpdateSearchableOptionInvalidAuthenticationProviderType() { + final UpdateSearchableOptionRequest request = UpdateSearchableOptionRequest.newBuilder() + .setProfileId(Util.PROFILE_ID.toString()).setAuthenticationIdentifier("Facebook_provider") + .setAuthenticationType(AuthenticationType.FACEBOOK.name()) + .setSearchOption(SearchableOption.SEARCH_DISABLED).build(); + + final StatusResponse reply = accountServiceBlockingStub.updateSearchableOption(request); + assertNotNull("Reply should not be null", reply); + assertEquals(reply.getError().getCause(), Cause.INVALID_AUTH_PROVIDER_TYPE); + } }