Merge branch 'develop' of https://github.com/mifosio/identity into develop
diff --git a/component-test/src/main/java/TestApplications.java b/component-test/src/main/java/TestApplications.java
index 1ce8d10..94062c3 100644
--- a/component-test/src/main/java/TestApplications.java
+++ b/component-test/src/main/java/TestApplications.java
@@ -14,32 +14,43 @@
* limitations under the License.
*/
+import io.mifos.anubis.api.v1.TokenConstants;
import io.mifos.anubis.api.v1.domain.AllowedOperation;
import io.mifos.anubis.api.v1.domain.Signature;
+import io.mifos.anubis.token.TenantRefreshTokenSerializer;
+import io.mifos.anubis.token.TokenSerializationResult;
import io.mifos.core.api.context.AutoUserContext;
+import io.mifos.core.api.util.FeignTargetWithCookieJar;
import io.mifos.core.api.util.NotFoundException;
import io.mifos.core.lang.security.RsaKeyPairFactory;
import io.mifos.identity.api.v1.PermittableGroupIds;
+import io.mifos.identity.api.v1.client.IdentityManager;
+import io.mifos.identity.api.v1.domain.Authentication;
import io.mifos.identity.api.v1.domain.CallEndpointSet;
import io.mifos.identity.api.v1.domain.Permission;
+import io.mifos.identity.api.v1.domain.User;
import io.mifos.identity.api.v1.events.*;
import org.apache.commons.lang.RandomStringUtils;
import org.junit.Assert;
import org.junit.Test;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.TimeUnit;
/**
* @author Myrle Krantz
*/
public class TestApplications extends AbstractComponentTest {
+ private static final String CALL_ENDPOINT_SET_IDENTIFIER = "doughboy";
+
@Test
public void testSetApplicationSignature() throws InterruptedException {
try (final AutoUserContext ignored
= tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
- final ApplicationSignatureEvent appPlusSig = setApplicationSignature();
+ final ApplicationSignatureTestData appPlusSig = setApplicationSignature();
final List<String> foundApplications = getTestSubject().getApplications();
Assert.assertTrue(foundApplications.contains(appPlusSig.getApplicationIdentifier()));
@@ -54,7 +65,7 @@
public void testCreateAndDeleteApplicationPermission() throws InterruptedException {
try (final AutoUserContext ignored
= tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
- final ApplicationSignatureEvent appPlusSig = setApplicationSignature();
+ final ApplicationSignatureTestData appPlusSig = setApplicationSignature();
final Permission identityManagementPermission = new Permission();
identityManagementPermission.setPermittableEndpointGroupIdentifier(PermittableGroupIds.IDENTITY_MANAGEMENT);
@@ -101,7 +112,7 @@
public void testDeleteApplication() throws InterruptedException {
try (final AutoUserContext ignored
= tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
- final ApplicationSignatureEvent appPlusSig = setApplicationSignature();
+ final ApplicationSignatureTestData appPlusSig = setApplicationSignature();
getTestSubject().deleteApplication(appPlusSig.getApplicationIdentifier());
@@ -124,7 +135,7 @@
@Test
public void testApplicationPermissionUserApprovalProvisioning() throws InterruptedException {
- final ApplicationSignatureEvent appPlusSig;
+ final ApplicationSignatureTestData appPlusSig;
final Permission identityManagementPermission;
try (final AutoUserContext ignored
= tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
@@ -208,7 +219,7 @@
public void manageApplicationEndpointSet() throws InterruptedException {
try (final AutoUserContext ignored
= tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
- final ApplicationSignatureEvent appPlusSig = setApplicationSignature();
+ final ApplicationSignatureTestData appPlusSig = setApplicationSignature();
final String endpointSetIdentifier = testEnvironment.generateUniqueIdentifer("epset");
final CallEndpointSet endpointSet = new CallEndpointSet();
@@ -256,7 +267,7 @@
@Test
public void applicationIssuedRefreshTokenHappyCase() throws InterruptedException {
- final ApplicationSignatureEvent appPlusSig;
+ final ApplicationSignatureTestData appPlusSig;
final Permission rolePermission = buildRolePermission();
final Permission userPermission = buildUserPermission();
try (final AutoUserContext ignored
@@ -271,6 +282,14 @@
Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_POST_APPLICATION_PERMISSION,
new ApplicationPermissionEvent(appPlusSig.getApplicationIdentifier(),
userPermission.getPermittableEndpointGroupIdentifier())));
+ getTestSubject().createApplicationCallEndpointSet(
+ appPlusSig.getApplicationIdentifier(),
+ new CallEndpointSet(CALL_ENDPOINT_SET_IDENTIFIER,
+ Arrays.asList(rolePermission.getPermittableEndpointGroupIdentifier(),
+ userPermission.getPermittableEndpointGroupIdentifier())));
+ Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_POST_APPLICATION_CALLENDPOINTSET,
+ new ApplicationCallEndpointSetEvent(appPlusSig.getApplicationIdentifier(),
+ CALL_ENDPOINT_SET_IDENTIFIER)));
}
final String userid;
@@ -289,8 +308,33 @@
userPermission.getPermittableEndpointGroupIdentifier(),
userid,
true);
+ getTestSubject().setApplicationPermissionEnabledForUser(
+ appPlusSig.getApplicationIdentifier(),
+ rolePermission.getPermittableEndpointGroupIdentifier(),
+ userid,
+ true);
}
- //TODO: get me a refresh token here. use it to get an access token. Then access like mad.
+
+ final TokenSerializationResult tokenSerializationResult =
+ new TenantRefreshTokenSerializer().build(new TenantRefreshTokenSerializer.Specification()
+ .setUser(userid)
+ .setEndpointSet(CALL_ENDPOINT_SET_IDENTIFIER)
+ .setSecondsToLive(30)
+ .setKeyTimestamp(appPlusSig.getKeyTimestamp())
+ .setPrivateKey(appPlusSig.getKeyPair().privateKey())
+ .setSourceApplication(appPlusSig.getApplicationIdentifier()));
+
+
+ final FeignTargetWithCookieJar<IdentityManager> identityManagerWithCookieJar
+ = apiFactory.createWithCookieJar(IdentityManager.class, testEnvironment.serverURI());
+
+ identityManagerWithCookieJar.putCookie("/token", TokenConstants.REFRESH_TOKEN_COOKIE_NAME, tokenSerializationResult.getToken());
+ final Authentication applicationAuthentication = identityManagerWithCookieJar.getFeignTarget().refresh();
+
+ try (final AutoUserContext ignored = new AutoUserContext(userid, applicationAuthentication.getAccessToken())) {
+ final List<User> users = getTestSubject().getUsers();
+ Assert.assertFalse(users.isEmpty());
+ }
}
private String createTestApplicationName()
@@ -298,15 +342,36 @@
return "test" + RandomStringUtils.randomNumeric(3) + "-v1";
}
- private ApplicationSignatureEvent setApplicationSignature() throws InterruptedException {
+ static class ApplicationSignatureTestData {
+ private final String applicationIdentifier;
+ private final RsaKeyPairFactory.KeyPairHolder keyPair;
+
+ ApplicationSignatureTestData(final String applicationIdentifier, final RsaKeyPairFactory.KeyPairHolder keyPair) {
+ this.applicationIdentifier = applicationIdentifier;
+ this.keyPair = keyPair;
+ }
+
+ String getApplicationIdentifier() {
+ return applicationIdentifier;
+ }
+
+ RsaKeyPairFactory.KeyPairHolder getKeyPair() {
+ return keyPair;
+ }
+
+ String getKeyTimestamp() {
+ return keyPair.getTimestamp();
+ }
+ }
+
+ private ApplicationSignatureTestData setApplicationSignature() throws InterruptedException {
final String testApplicationName = createTestApplicationName();
final RsaKeyPairFactory.KeyPairHolder keyPair = RsaKeyPairFactory.createKeyPair();
final Signature signature = new Signature(keyPair.getPublicKeyMod(), keyPair.getPublicKeyExp());
getTestSubject().setApplicationSignature(testApplicationName, keyPair.getTimestamp(), signature);
- final ApplicationSignatureEvent event = new ApplicationSignatureEvent(testApplicationName, keyPair.getTimestamp());
- Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_PUT_APPLICATION_SIGNATURE, event));
- return event;
+ Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_PUT_APPLICATION_SIGNATURE, new ApplicationSignatureEvent(testApplicationName, keyPair.getTimestamp())));
+ return new ApplicationSignatureTestData(testApplicationName, keyPair);
}
}
diff --git a/component-test/src/main/java/TestPasswords.java b/component-test/src/main/java/TestPasswords.java
index 1a9045b..63b2dc1 100644
--- a/component-test/src/main/java/TestPasswords.java
+++ b/component-test/src/main/java/TestPasswords.java
@@ -26,7 +26,6 @@
import org.junit.Test;
import java.time.Duration;
-import java.util.concurrent.TimeUnit;
/**
* @author Myrle Krantz
@@ -122,8 +121,6 @@
Assert.assertTrue(found);
}
- Thread.sleep(100);
-
final TimeStampChecker passwordExpirationChecker = TimeStampChecker.inTheFutureWithWiggleRoom(Duration.ofDays(93), Duration.ofHours(24));
final Authentication userAuthenticationAfterPasswordChange = getTestSubject().login(username, TestEnvironment.encodePassword(newPassword));
final String passwordExpiration = userAuthenticationAfterPasswordChange.getPasswordExpiration();
@@ -185,7 +182,6 @@
final String roleIdentifier = createRole(buildUserPermission(), buildSelfPermission(), buildRolePermission());
final String username = createUserWithNonexpiredPassword(AHMES_PASSWORD, roleIdentifier);
- TimeUnit.SECONDS.sleep(1);
try (final AutoUserContext ignored2 = loginUser(username, AHMES_PASSWORD)) {
getTestSubject().changeUserPassword(ADMIN_IDENTIFIER, new Password(TestEnvironment.encodePassword(AHMES_FRIENDS_PASSWORD)));
Assert.fail("Should not be able to change antony's password from any account other than antony's.");
diff --git a/service/src/main/java/io/mifos/identity/internal/command/handler/AuthenticationCommandHandler.java b/service/src/main/java/io/mifos/identity/internal/command/handler/AuthenticationCommandHandler.java
index e5bcdca..d4bbe44 100644
--- a/service/src/main/java/io/mifos/identity/internal/command/handler/AuthenticationCommandHandler.java
+++ b/service/src/main/java/io/mifos/identity/internal/command/handler/AuthenticationCommandHandler.java
@@ -31,6 +31,7 @@
import io.mifos.core.lang.TenantContextHolder;
import io.mifos.core.lang.config.TenantHeaderFilter;
import io.mifos.core.lang.security.RsaPrivateKeyBuilder;
+import io.mifos.core.lang.security.RsaPublicKeyBuilder;
import io.mifos.identity.api.v1.events.EventConstants;
import io.mifos.identity.internal.command.AuthenticationCommandResponse;
import io.mifos.identity.internal.command.PasswordAuthenticationCommand;
@@ -47,12 +48,15 @@
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;
+import javax.annotation.Nullable;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
+import java.time.ZoneId;
import java.util.*;
+import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -72,6 +76,10 @@
private final TenantAccessTokenSerializer tenantAccessTokenSerializer;
private final TenantRefreshTokenSerializer tenantRefreshTokenSerializer;
private final TenantRsaKeyProvider tenantRsaKeyProvider;
+ private final ApplicationSignatures applicationSignatures;
+ private final ApplicationPermissions applicationPermissions;
+ private final ApplicationPermissionUsers applicationPermissionUsers;
+ private final ApplicationCallEndpointSets applicationCallEndpointSets;
private final JmsTemplate jmsTemplate;
private final Gson gson;
private final Logger logger;
@@ -98,6 +106,10 @@
final TenantRefreshTokenSerializer tenantRefreshTokenSerializer,
@SuppressWarnings("SpringJavaAutowiringInspection")
final TenantRsaKeyProvider tenantRsaKeyProvider,
+ final ApplicationSignatures applicationSignatures,
+ final ApplicationPermissions applicationPermissions,
+ final ApplicationPermissionUsers applicationPermissionUsers,
+ final ApplicationCallEndpointSets applicationCallEndpointSets,
final JmsTemplate jmsTemplate,
final ApplicationName applicationName,
@Qualifier(IdentityConstants.JSON_SERIALIZER_NAME) final Gson gson,
@@ -111,6 +123,10 @@
this.tenantAccessTokenSerializer = tenantAccessTokenSerializer;
this.tenantRefreshTokenSerializer = tenantRefreshTokenSerializer;
this.tenantRsaKeyProvider = tenantRsaKeyProvider;
+ this.applicationSignatures = applicationSignatures;
+ this.applicationPermissions = applicationPermissions;
+ this.applicationPermissionUsers = applicationPermissionUsers;
+ this.applicationCallEndpointSets = applicationCallEndpointSets;
this.jmsTemplate = jmsTemplate;
this.gson = gson;
this.logger = logger;
@@ -147,21 +163,20 @@
throw AmitAuthenticationException.userPasswordCombinationNotFound();
}
- final Optional<LocalDateTime> passwordExpiration = getExpiration(user);
-
- final TokenSerializationResult accessToken = getAccessToken(
- user.getIdentifier(),
- getTokenPermissions(user, passwordExpiration, privateTenantInfo.getTimeToChangePasswordAfterExpirationInDays()),
- privateSignature);
-
final TokenSerializationResult refreshToken = getRefreshToken(user, privateSignature);
+ final AuthenticationCommandResponse ret = getAuthenticationResponse(
+ applicationName.toString(),
+ Optional.empty(),
+ privateTenantInfo,
+ privateSignature,
+ user,
+ refreshToken.getToken(),
+ refreshToken.getExpiration());
+
fireAuthenticationEvent(user.getIdentifier());
- return new AuthenticationCommandResponse(
- accessToken.getToken(), DateConverter.toIsoString(accessToken.getExpiration()),
- refreshToken.getToken(), DateConverter.toIsoString(refreshToken.getExpiration()),
- passwordExpiration.map(DateConverter::toIsoString).orElse(null));
+ return ret;
}
private PrivateSignatureEntity checkedGetPrivateSignature() {
@@ -185,9 +200,16 @@
private class TenantIdentityRsaKeyProvider implements TenantApplicationRsaKeyProvider {
@Override
public PublicKey getApplicationPublicKey(final String tokenApplicationName, final String timestamp) throws InvalidKeyTimestampException {
- if (!applicationName.toString().equals(tokenApplicationName))
- throw new IllegalArgumentException("Currently only supporting refresh tokens issued by identity");
- return tenantRsaKeyProvider.getPublicKey(timestamp);
+ if (applicationName.toString().equals(tokenApplicationName))
+ return tenantRsaKeyProvider.getPublicKey(timestamp);
+
+ final ApplicationSignatureEntity signature = applicationSignatures.get(tokenApplicationName, timestamp)
+ .orElseThrow(() -> new InvalidKeyTimestampException(timestamp));
+
+ return new RsaPublicKeyBuilder()
+ .setPublicKeyMod(signature.getPublicKeyMod())
+ .setPublicKeyExp(signature.getPublicKeyExp())
+ .build();
}
}
@@ -202,18 +224,64 @@
final PrivateSignatureEntity privateSignature = checkedGetPrivateSignature();
final UserEntity user = getUser(deserializedRefreshToken.getUserIdentifier());
+ final String sourceApplicationName = deserializedRefreshToken.getSourceApplication();
+ return getAuthenticationResponse(
+ sourceApplicationName,
+ Optional.ofNullable(deserializedRefreshToken.getEndpointSet()),
+ privateTenantInfo,
+ privateSignature,
+ user,
+ command.getRefreshToken(),
+ LocalDateTime.ofInstant(deserializedRefreshToken.getExpiration().toInstant(), ZoneId.of("UTC")));
+ }
+
+ private AuthenticationCommandResponse getAuthenticationResponse(
+ final String sourceApplicationName,
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ final Optional<String> callEndpointSet,
+ final PrivateTenantInfoEntity privateTenantInfo,
+ final PrivateSignatureEntity privateSignature,
+ final UserEntity user,
+ final String refreshToken,
+ final LocalDateTime refreshTokenExpiration) {
final Optional<LocalDateTime> passwordExpiration = getExpiration(user);
- final TokenSerializationResult accessToken = getAccessToken(
+ final int gracePeriod = privateTenantInfo.getTimeToChangePasswordAfterExpirationInDays();
+ if (pastGracePeriod(passwordExpiration, gracePeriod))
+ throw AmitAuthenticationException.passwordExpired();
+
+ final Set<TokenPermission> tokenPermissions;
+
+ if (sourceApplicationName.equals(applicationName.toString())) { //ie, this is a token for the identity manager.
+ if (pastExpiration(passwordExpiration)) {
+ tokenPermissions = identityEndpointsAllowedEvenWithExpiredPassword();
+ logger.info("Password expired {}", passwordExpiration.map(LocalDateTime::toString).orElse("empty"));
+ }
+ else {
+ tokenPermissions = getUserTokenPermissions(user);
+ }
+ }
+ else {
+ tokenPermissions = getApplicationTokenPermissions(user, sourceApplicationName, callEndpointSet);
+ }
+
+ logger.info("Access token for tenant{}, user {}, application {}, and callEndpointSet {} being returned containing the permissions '{}'.",
+ TenantContextHolder.identifier().orElse("null"),
user.getIdentifier(),
- getTokenPermissions(user, passwordExpiration, privateTenantInfo.getTimeToChangePasswordAfterExpirationInDays()),
+ sourceApplicationName,
+ callEndpointSet.orElse("null"),
+ tokenPermissions.toString());
+
+ final TokenSerializationResult accessToken = getAuthenticationResponse(
+ user.getIdentifier(),
+ tokenPermissions,
privateSignature);
return new AuthenticationCommandResponse(
- accessToken.getToken(), DateConverter.toIsoString(accessToken.getExpiration()),
- command.getRefreshToken(), DateConverter.toIsoString(deserializedRefreshToken.getExpiration()),
- passwordExpiration.map(DateConverter::toIsoString).orElse(null));
+ accessToken.getToken(), DateConverter.toIsoString(accessToken.getExpiration()),
+ refreshToken, DateConverter.toIsoString(refreshTokenExpiration),
+ passwordExpiration.map(DateConverter::toIsoString).orElse(null));
}
private Optional<LocalDateTime> getExpiration(final UserEntity user)
@@ -254,8 +322,8 @@
);
}
- private TokenSerializationResult getAccessToken(
- final String identifier,
+ private TokenSerializationResult getAuthenticationResponse(
+ final String userIdentifier,
final Set<TokenPermission> tokenPermissions,
final PrivateSignatureEntity privateSignatureEntity) {
@@ -270,48 +338,150 @@
.setPrivateKey(privateKey)
.setTokenContent(new TokenContent(new ArrayList<>(tokenPermissions)))
.setSecondsToLive(accessTtl)
- .setUser(identifier);
+ .setUser(userIdentifier);
return tenantAccessTokenSerializer.build(x);
}
- private Set<TokenPermission> getTokenPermissions(
- final UserEntity user,
- @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
- final Optional<LocalDateTime> passwordExpiration,
- final long gracePeriod) throws AmitAuthenticationException {
+ private Set<TokenPermission> getUserTokenPermissions(
+ final UserEntity user) {
+
final Optional<RoleEntity> userRole = roles.get(user.getRole());
- final Set<TokenPermission> tokenPermissions;
+ final Set<TokenPermission> tokenPermissions = userRole
+ .map(r -> r.getPermissions().stream().flatMap(this::mapPermissions).collect(Collectors.toSet()))
+ .orElse(new HashSet<>());
- if (pastGracePeriod(passwordExpiration, gracePeriod))
- throw AmitAuthenticationException.passwordExpired();
+ tokenPermissions.addAll(identityEndpointsForEveryUser());
- if (pastExpiration(passwordExpiration)) {
- tokenPermissions = new HashSet<>();
- }
- else {
- tokenPermissions = userRole.map(r -> r.getPermissions().stream().flatMap(this::mapPermissions).collect(Collectors.toSet()))
- .orElse(new HashSet<>());
- }
+ return tokenPermissions;
+ }
- tokenPermissions.add(
- new TokenPermission(
+ private Set<TokenPermission> getApplicationTokenPermissions(
+ final UserEntity user,
+ final String sourceApplicationName,
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional<String> callEndpointSet) {
+ //If the call endpoint set was given, but does not correspond to a stored call endpoint set, throw an exception.
+ //If it wasn't given then return all of the permissions for the application.
+ final Optional<ApplicationCallEndpointSetEntity> applicationCallEndpointSet
+ = callEndpointSet.map(x -> applicationCallEndpointSets.get(sourceApplicationName, x)
+ .orElseThrow(AmitAuthenticationException::invalidToken));
+
+ final RoleEntity userRole = roles.get(user.getRole())
+ .orElseThrow(AmitAuthenticationException::userPasswordCombinationNotFound);
+
+ return applicationCallEndpointSet.map(x -> this.getApplicationCallEndpointSetTokenPermissions(user.getIdentifier(), userRole, x, sourceApplicationName))
+ .orElseGet(() -> this.getApplicationUserTokenPermissions(user.getIdentifier(), userRole, sourceApplicationName));
+ }
+
+ private Set<TokenPermission> getApplicationCallEndpointSetTokenPermissions(
+ final String userIdentifier,
+ final RoleEntity userRole,
+ final ApplicationCallEndpointSetEntity applicationCallEndpointSet,
+ final String sourceApplicationName) {
+ final List<PermissionType> permissionsForUser = userRole.getPermissions();
+ final Set<PermissionType> permissionsRequestedByApplication = applicationCallEndpointSet.getCallEndpointGroupIdentifiers().stream()
+ .map(x -> applicationPermissions.getPermissionForApplication(sourceApplicationName, x))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toSet());
+
+ final Stream<PermissionType> applicationRequestedPermissionsTheUserHas
+ = intersectPermissionList(permissionsForUser, permissionsRequestedByApplication.stream());
+
+ final Set<PermissionType> permissionsPossible = applicationRequestedPermissionsTheUserHas
+ .filter(x ->
+ applicationPermissionUsers.enabled(sourceApplicationName, x.getPermittableGroupIdentifier(), userIdentifier))
+ .collect(Collectors.toSet());
+
+ if (!permissionsPossible.containsAll(permissionsRequestedByApplication))
+ throw AmitAuthenticationException.applicationMissingPermissions(userIdentifier, sourceApplicationName);
+
+ return permissionsPossible.stream()
+ .flatMap(this::mapPermissions)
+ .collect(Collectors.toSet());
+ }
+
+ private Set<TokenPermission> getApplicationUserTokenPermissions(
+ final String userIdentifier,
+ final RoleEntity userRole,
+ final String sourceApplicationName) {
+ final List<PermissionType> permissionsForUser = userRole.getPermissions();
+ final List<PermissionType> permissionsRequestedByApplication = applicationPermissions.getAllPermissionsForApplication(sourceApplicationName);
+
+ final Stream<PermissionType> applicationRequestedPermissionsTheUserHas
+ = intersectPermissionList(permissionsForUser, permissionsRequestedByApplication.stream());
+
+ return applicationRequestedPermissionsTheUserHas
+ .filter(x ->
+ applicationPermissionUsers.enabled(sourceApplicationName, x.getPermittableGroupIdentifier(), userIdentifier))
+ .flatMap(this::mapPermissions)
+ .collect(Collectors.toSet());
+ }
+
+ private Stream<PermissionType> intersectPermissionList(
+ final List<PermissionType> permissionsForUser,
+ final Stream<PermissionType> permissionsRequestedByApplication) {
+ final Map<String, Set<AllowedOperationType>> keyedUserPermissions = transformToSearchablePermissions(permissionsForUser);
+
+ return permissionsRequestedByApplication
+ .map(x -> new PermissionType(
+ x.getPermittableGroupIdentifier(),
+ intersectSets(keyedUserPermissions.get(x.getPermittableGroupIdentifier()), x.getAllowedOperations())))
+ .filter(x -> !x.getAllowedOperations().isEmpty());
+ }
+
+ static <T> Set<T> intersectSets(
+ final @Nullable Set<T> allowedOperations1,
+ final @Nullable Set<T> allowedOperations2) {
+ if (allowedOperations1 == null || allowedOperations2 == null)
+ return Collections.emptySet();
+
+ final Set<T> ret = new HashSet<>(allowedOperations1);
+ ret.retainAll(allowedOperations2);
+ return ret;
+ }
+
+ static Map<String, Set<AllowedOperationType>> transformToSearchablePermissions(final List<PermissionType> permissionsForUser) {
+ final Collector<Set<AllowedOperationType>, Set<AllowedOperationType>, Set<AllowedOperationType>> setToSetCollector
+ = Collector.of(
+ HashSet::new,
+ Set::addAll,
+ (x, y) -> {
+ final Set<AllowedOperationType> ret = new HashSet<>();
+ ret.addAll(x);
+ ret.addAll(y);
+ return ret;
+ });
+
+ return permissionsForUser.stream().collect(
+ Collectors.groupingBy(PermissionType::getPermittableGroupIdentifier,
+ Collectors.mapping(PermissionType::getAllowedOperations, setToSetCollector)));
+ }
+
+ private Set<TokenPermission> identityEndpointsForEveryUser() {
+ final Set<TokenPermission> ret = identityEndpointsAllowedEvenWithExpiredPassword();
+
+ ret.add(new TokenPermission(
applicationName + "/applications/*/permissions/*/users/{useridentifier}/enabled",
AllowedOperation.ALL));
- tokenPermissions.add(
- new TokenPermission(
- applicationName + "/users/{useridentifier}/password",
- Collections.singleton(AllowedOperation.CHANGE)));
- tokenPermissions.add(
- new TokenPermission(
+ ret.add(new TokenPermission(
applicationName + "/users/{useridentifier}/permissions",
Collections.singleton(AllowedOperation.READ)));
- tokenPermissions.add(
- new TokenPermission(
+
+ return ret;
+ }
+
+ private Set<TokenPermission> identityEndpointsAllowedEvenWithExpiredPassword() {
+ final Set<TokenPermission> ret = new HashSet<>();
+
+ ret.add(new TokenPermission(
+ applicationName + "/users/{useridentifier}/password",
+ Collections.singleton(AllowedOperation.CHANGE)));
+ ret.add(new TokenPermission(
applicationName + "/token/_current",
Collections.singleton(AllowedOperation.DELETE)));
- return tokenPermissions;
+ return ret;
}
static boolean pastExpiration(
diff --git a/service/src/main/java/io/mifos/identity/internal/command/handler/UserCommandHandler.java b/service/src/main/java/io/mifos/identity/internal/command/handler/UserCommandHandler.java
index 3d99b65..0a54236 100644
--- a/service/src/main/java/io/mifos/identity/internal/command/handler/UserCommandHandler.java
+++ b/service/src/main/java/io/mifos/identity/internal/command/handler/UserCommandHandler.java
@@ -25,7 +25,10 @@
import io.mifos.identity.internal.command.CreateUserCommand;
import io.mifos.identity.internal.repository.UserEntity;
import io.mifos.identity.internal.repository.Users;
+import io.mifos.identity.internal.util.IdentityConstants;
+import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
@@ -40,14 +43,17 @@
private final Users usersRepository;
private final UserEntityCreator userEntityCreator;
+ private final Logger logger;
@Autowired
UserCommandHandler(
- final Users usersRepository,
- final UserEntityCreator userEntityCreator)
+ final Users usersRepository,
+ final UserEntityCreator userEntityCreator,
+ @Qualifier(IdentityConstants.LOGGER_NAME) final Logger logger)
{
this.usersRepository = usersRepository;
this.userEntityCreator = userEntityCreator;
+ this.logger = logger;
}
@CommandHandler
@@ -74,6 +80,7 @@
user.getIdentifier(), user.getRole(), command.getPassword(),
!SecurityContextHolder.getContext().getAuthentication().getPrincipal().equals(command.getIdentifier()));
usersRepository.add(userWithNewPassword);
+ logger.info("Changed password for user {}, expiration date is now {}", user.getIdentifier(), userWithNewPassword.getPasswordExpiresOn());
return user.getIdentifier();
}
diff --git a/service/src/test/java/io/mifos/identity/internal/command/handler/AuthenticationCommandHandlerTest.java b/service/src/test/java/io/mifos/identity/internal/command/handler/AuthenticationCommandHandlerTest.java
index 759cbb6..503a6c1 100644
--- a/service/src/test/java/io/mifos/identity/internal/command/handler/AuthenticationCommandHandlerTest.java
+++ b/service/src/test/java/io/mifos/identity/internal/command/handler/AuthenticationCommandHandlerTest.java
@@ -16,7 +16,10 @@
package io.mifos.identity.internal.command.handler;
import com.google.gson.Gson;
+import io.mifos.anubis.provider.TenantRsaKeyProvider;
import io.mifos.anubis.token.TenantAccessTokenSerializer;
+import io.mifos.anubis.token.TenantRefreshTokenSerializer;
+import io.mifos.anubis.token.TokenDeserializationResult;
import io.mifos.anubis.token.TokenSerializationResult;
import io.mifos.core.lang.ApplicationName;
import io.mifos.core.lang.DateConverter;
@@ -41,10 +44,9 @@
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.Optional;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.when;
@@ -82,11 +84,17 @@
final ApplicationName applicationName = Mockito.mock(ApplicationName.class);
final Gson gson = new Gson();
final Logger logger = Mockito.mock(Logger.class);
+ final TenantRsaKeyProvider tenantRsaKeyProvider = Mockito.mock(TenantRsaKeyProvider.class);
+ final ApplicationSignatures applicationSignatures = Mockito.mock(ApplicationSignatures.class);
+ final ApplicationPermissions applicationPermissions = Mockito.mock(ApplicationPermissions.class);
+ final ApplicationPermissionUsers applicationPermissionUsers = Mockito.mock(ApplicationPermissionUsers.class);
+ final ApplicationCallEndpointSets applicationCallEndpointSets = Mockito.mock(ApplicationCallEndpointSets.class);
commandHandler = new AuthenticationCommandHandler(
users, roles, permittableGroups, signatures, tenants,
hashGenerator,
- tenantAccessTokenSerializer, tenantRefreshTokenSerializer,
+ tenantAccessTokenSerializer, tenantRefreshTokenSerializer, tenantRsaKeyProvider,
+ applicationSignatures, applicationPermissions, applicationPermissionUsers, applicationCallEndpointSets,
jmsTemplate, applicationName,
gson, logger);
@@ -123,8 +131,8 @@
final TokenSerializationResult refreshTokenSerializationResult = new TokenSerializationResult("blah", LocalDateTime.now(ZoneId.of("UTC")).plusSeconds(REFRESH_TOKEN_TIME_TO_LIVE));
when(tenantRefreshTokenSerializer.build(anyObject())).thenReturn(refreshTokenSerializationResult);
- final TenantRefreshTokenSerializer.Deserialized deserialized = new TenantRefreshTokenSerializer.Deserialized(USER_NAME, Date.from(Instant.now().plusSeconds(REFRESH_TOKEN_TIME_TO_LIVE)), TEST_APPLICATION_NAME);
- when(tenantRefreshTokenSerializer.deserialize(anyObject())).thenReturn(deserialized);
+ final TokenDeserializationResult deserialized = new TokenDeserializationResult(USER_NAME, Date.from(Instant.now().plusSeconds(REFRESH_TOKEN_TIME_TO_LIVE)), TEST_APPLICATION_NAME, null);
+ when(tenantRefreshTokenSerializer.deserialize(anyObject(), anyObject())).thenReturn(deserialized);
when(hashGenerator.isEqual(any(), any(), any(), any(), anyInt(), anyInt())).thenReturn(true);
}
@@ -174,28 +182,28 @@
@Test
public void correctDeterminationOfPasswordExpiration()
{
- final LocalDate passwordExpirationFromToday = LocalDate.now(ZoneId.of("UTC"));
- Assert.assertTrue(AuthenticationCommandHandler.pastExpiration(passwordExpirationFromToday));
+ final LocalDateTime passwordExpirationFromToday = LocalDateTime.now(ZoneId.of("UTC"));
+ Assert.assertTrue(AuthenticationCommandHandler.pastExpiration(Optional.of(passwordExpirationFromToday)));
- final LocalDate passwordExpirationFromYesterday = passwordExpirationFromToday.minusDays(1);
- Assert.assertTrue(AuthenticationCommandHandler.pastExpiration(passwordExpirationFromYesterday));
+ final LocalDateTime passwordExpirationFromYesterday = passwordExpirationFromToday.minusDays(1);
+ Assert.assertTrue(AuthenticationCommandHandler.pastExpiration(Optional.of(passwordExpirationFromYesterday)));
- final LocalDate passwordExpirationFromTommorrow = passwordExpirationFromToday.plusDays(1);
- Assert.assertFalse(AuthenticationCommandHandler.pastExpiration(passwordExpirationFromTommorrow));
+ final LocalDateTime passwordExpirationFromTommorrow = passwordExpirationFromToday.plusDays(1);
+ Assert.assertFalse(AuthenticationCommandHandler.pastExpiration(Optional.of(passwordExpirationFromTommorrow)));
}
@Test
public void correctDeterminationOfPasswordGracePeriod()
{
- final LocalDate passwordExpirationFromToday = LocalDate.now(ZoneId.of("UTC"));
- Assert.assertFalse(AuthenticationCommandHandler.pastGracePeriod(passwordExpirationFromToday, GRACE_PERIOD));
+ final LocalDateTime passwordExpirationFromToday = LocalDateTime.now(ZoneId.of("UTC"));
+ Assert.assertFalse(AuthenticationCommandHandler.pastGracePeriod(Optional.of(passwordExpirationFromToday), GRACE_PERIOD));
- final LocalDate nowJustWithinPasswordExpirationAndGracePeriod = passwordExpirationFromToday.minusDays(GRACE_PERIOD - 1);
- Assert.assertFalse(AuthenticationCommandHandler.pastGracePeriod(nowJustWithinPasswordExpirationAndGracePeriod, GRACE_PERIOD));
+ final LocalDateTime nowJustWithinPasswordExpirationAndGracePeriod = passwordExpirationFromToday.minusDays(GRACE_PERIOD - 1);
+ Assert.assertFalse(AuthenticationCommandHandler.pastGracePeriod(Optional.of(nowJustWithinPasswordExpirationAndGracePeriod), GRACE_PERIOD));
- final LocalDate nowJustOutsideOfPasswordExpirationAndGracePeriod = passwordExpirationFromToday.minusDays(GRACE_PERIOD);
- Assert.assertTrue(AuthenticationCommandHandler.pastGracePeriod(nowJustOutsideOfPasswordExpirationAndGracePeriod, GRACE_PERIOD));
+ final LocalDateTime nowJustOutsideOfPasswordExpirationAndGracePeriod = passwordExpirationFromToday.minusDays(GRACE_PERIOD);
+ Assert.assertTrue(AuthenticationCommandHandler.pastGracePeriod(Optional.of(nowJustOutsideOfPasswordExpirationAndGracePeriod), GRACE_PERIOD));
}
@Test
@@ -212,4 +220,46 @@
Assert.assertEquals(dateString, localDateTimeString);
Assert.assertTrue(localDateTimeString.startsWith(localDateString.substring(0, localDateString.length()-1))); //(removing Z)
}
+
+ @Test
+ public void intersectSets() {
+ final Set<AllowedOperationType> intersectionWithNull
+ = AuthenticationCommandHandler.intersectSets(new HashSet<>(), null);
+ Assert.assertTrue(intersectionWithNull.isEmpty());
+
+ final Set<AllowedOperationType> intersectionWithEqualSet
+ = AuthenticationCommandHandler.intersectSets(Collections.singleton(AllowedOperationType.CHANGE),
+ Collections.singleton(AllowedOperationType.CHANGE));
+ Assert.assertEquals(Collections.singleton(AllowedOperationType.CHANGE), intersectionWithEqualSet);
+
+ final Set<AllowedOperationType> intersectionWithAll
+ = AuthenticationCommandHandler.intersectSets(AllowedOperationType.ALL,
+ AllowedOperationType.ALL);
+ Assert.assertEquals(AllowedOperationType.ALL, intersectionWithAll);
+
+ final Set<AllowedOperationType> intersectionWithNonOverlappingSet
+ = AuthenticationCommandHandler.intersectSets(Collections.singleton(AllowedOperationType.DELETE),
+ Collections.singleton(AllowedOperationType.CHANGE));
+ Assert.assertTrue(intersectionWithNonOverlappingSet.isEmpty());
+
+ final Set<AllowedOperationType> intersectionWithSubSet
+ = AuthenticationCommandHandler.intersectSets(AllowedOperationType.ALL,
+ Collections.singleton(AllowedOperationType.CHANGE));
+ Assert.assertEquals(Collections.singleton(AllowedOperationType.CHANGE), intersectionWithSubSet);
+
+ final Set<AllowedOperationType> intersectionWithPartiallyOverlapping = AuthenticationCommandHandler.intersectSets(
+ Stream.of(AllowedOperationType.CHANGE, AllowedOperationType.DELETE).collect(Collectors.toSet()),
+ Stream.of(AllowedOperationType.CHANGE, AllowedOperationType.READ).collect(Collectors.toSet()));
+ Assert.assertEquals(Collections.singleton(AllowedOperationType.CHANGE), intersectionWithPartiallyOverlapping);
+ }
+
+ @Test
+ public void transformToSearchablePermissions()
+ {
+ Map<String, Set<AllowedOperationType>> x = AuthenticationCommandHandler.transformToSearchablePermissions(Arrays.asList(
+ new PermissionType("x", new HashSet<>(AllowedOperationType.ALL)),
+ new PermissionType("y", new HashSet<>(Collections.singletonList(AllowedOperationType.CHANGE)))));
+
+ Assert.assertEquals(AllowedOperationType.ALL, x.get("x"));
+ }
}