From a6fe02db174be4fe79be39363661029d190d302f Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Tue, 6 Oct 2020 14:34:58 +0800 Subject: [PATCH] WIP --- index.adoc | 506 +++++++++++++++++- pom.xml | 6 +- .../crowd/model/util/DirectoryBeanUtil.java | 198 ++++--- .../crowd/rest/DirectoriesResourceImpl.java | 31 +- .../crowd/rest/api/DirectoriesResource.java | 53 -- .../crowd/service/DirectoriesServiceImpl.java | 173 +++++- .../crowd/service/api/DirectoriesService.java | 16 - .../confapi/crowd/util/AttributeUtil.java | 85 +++ .../embedded/api/MockDirectoryInternal.java | 5 +- .../model/util/DirectoryBeanUtilTest.java | 35 +- .../crowd/rest/DirectoriesResourceTest.java | 3 +- .../crowd/service/DirectoriesServiceTest.java | 182 ++++++- 12 files changed, 1077 insertions(+), 216 deletions(-) delete mode 100644 src/main/java/de/aservo/confapi/crowd/rest/api/DirectoriesResource.java delete mode 100644 src/main/java/de/aservo/confapi/crowd/service/api/DirectoriesService.java create mode 100644 src/main/java/de/aservo/confapi/crowd/util/AttributeUtil.java diff --git a/index.adoc b/index.adoc index 6deb57e..546d8df 100644 --- a/index.adoc +++ b/index.adoc @@ -1396,12 +1396,288 @@ endif::internal-generation[] === Directories +[.addDirectory] +==== addDirectory + +`POST /directories` + +Add a user directory + +===== Description + + + + +// markup not found, no include::{specDir}directories/POST/spec.adoc[opts=optional] + + + +===== Parameters + + +===== Body Parameter + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| AbstractDirectoryBean +| <> +| X +| +| + +|=== + + + +====== Query Parameters + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| test-connection +| +| - +| false +| + +|=== + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| Returns the added directory. +| <> + + +| 0 +| Returns a list of error messages. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}directories/POST/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}directories/POST/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :directories/POST/POST.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}directories/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.deleteDirectories] +==== deleteDirectories + +`DELETE /directories` + +Delete all user directories + +===== Description + +NOTE: The 'force' parameter must be set to 'true' in order to execute this request. + + +// markup not found, no include::{specDir}directories/DELETE/spec.adoc[opts=optional] + + + +===== Parameters + + + + + +====== Query Parameters + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| force +| +| - +| null +| + +|=== + + +===== Return Type + + + +- + +===== Content Type + +* */* + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| Returns an empty body. +| <<>> + + +| 0 +| Returns a list of error messages. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}directories/DELETE/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}directories/DELETE/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :directories/DELETE/DELETE.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}directories/DELETE/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.deleteDirectory] +==== deleteDirectory + +`DELETE /directories/{id}` + +Delete a user directory + +===== Description + + + + +// markup not found, no include::{specDir}directories/\{id\}/DELETE/spec.adoc[opts=optional] + + + +===== Parameters + +====== Path Parameters + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| id +| +| X +| null +| + +|=== + + + + + + +===== Return Type + + + +- + +===== Content Type + +* */* + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| Returns an empty body. +| <<>> + + +| 0 +| Returns a list of error messages. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}directories/\{id\}/DELETE/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}directories/\{id\}/DELETE/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :directories/{id}/DELETE/DELETE.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}directories/\{id\}/DELETE/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + [.getDirectories] ==== getDirectories `GET /directories` -Get the list of directories +Get all user directories ===== Description @@ -1438,12 +1714,12 @@ Get the list of directories | 200 -| +| Returns all directories. | <> | 0 -| +| Returns a list of error messages. | <> |=== @@ -1475,7 +1751,7 @@ endif::internal-generation[] `GET /directories/{id}` -Get a directory based on it's ID +Get a user directory ===== Description @@ -1509,8 +1785,7 @@ Get a directory based on it's ID ===== Return Type - -<> +<> ===== Content Type @@ -1526,12 +1801,12 @@ Get a directory based on it's ID | 200 -| -| <> +| Returns the requested directory. +| <> | 0 -| +| Returns a list of error messages. | <> |=== @@ -1558,6 +1833,219 @@ ifdef::internal-generation[] endif::internal-generation[] +[.setDirectories] +==== setDirectories + +`PUT /directories` + +Set or update a list of user directories + +===== Description + +NOTE: All existing directories with the same 'name' attribute are updated. + + +// markup not found, no include::{specDir}directories/PUT/spec.adoc[opts=optional] + + + +===== Parameters + + +===== Body Parameter + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| DirectoriesBean +| <> +| X +| +| + +|=== + + + +====== Query Parameters + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| test-connection +| +| - +| false +| + +|=== + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| Returns all directories. +| <> + + +| 0 +| Returns a list of error messages. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}directories/PUT/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}directories/PUT/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :directories/PUT/PUT.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}directories/PUT/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.setDirectory] +==== setDirectory + +`PUT /directories/{id}` + +Update a user directory + +===== Description + + + + +// markup not found, no include::{specDir}directories/\{id\}/PUT/spec.adoc[opts=optional] + + + +===== Parameters + +====== Path Parameters + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| id +| +| X +| null +| + +|=== + +===== Body Parameter + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| AbstractDirectoryBean +| <> +| X +| +| + +|=== + + + +====== Query Parameters + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| test-connection +| +| - +| false +| + +|=== + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| Returns the updated directory. +| <> + + +| 0 +| Returns a list of error messages. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}directories/\{id\}/PUT/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}directories/\{id\}/PUT/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :directories/{id}/PUT/PUT.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}directories/\{id\}/PUT/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + [.Licenses] === Licenses diff --git a/pom.xml b/pom.xml index 638036f..57bd3c0 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ confapi-crowd-plugin - 0.1.0 + 0.2.0-SNAPSHOT atlassian-plugin ConfAPI for Crowd @@ -68,6 +68,10 @@ 2.1.5 0.0.34 2.0.1 + 11 + 11 + UTF-8 + UTF-8 diff --git a/src/main/java/de/aservo/confapi/crowd/model/util/DirectoryBeanUtil.java b/src/main/java/de/aservo/confapi/crowd/model/util/DirectoryBeanUtil.java index 23c7cd6..8620af6 100644 --- a/src/main/java/de/aservo/confapi/crowd/model/util/DirectoryBeanUtil.java +++ b/src/main/java/de/aservo/confapi/crowd/model/util/DirectoryBeanUtil.java @@ -2,23 +2,27 @@ import com.atlassian.crowd.embedded.api.Directory; import com.atlassian.crowd.embedded.api.DirectoryType; +import com.atlassian.crowd.embedded.api.OperationType; import com.atlassian.crowd.model.directory.ImmutableDirectory; import de.aservo.confapi.commons.model.AbstractDirectoryBean; +import de.aservo.confapi.commons.model.DirectoryCrowdBean; import de.aservo.confapi.commons.model.DirectoryGenericBean; import de.aservo.confapi.commons.model.DirectoryInternalBean; +import de.aservo.confapi.commons.model.DirectoryLdapBean; -import javax.annotation.Nullable; +import javax.annotation.Nonnull; import java.util.HashMap; -import java.util.List; +import java.util.HashSet; import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.Set; import static com.atlassian.crowd.directory.AbstractInternalDirectory.*; +import static de.aservo.confapi.crowd.util.AttributeUtil.*; +import static java.lang.Boolean.TRUE; public class DirectoryBeanUtil { - public static final String LIST_SEPARATOR = ","; + public static final String ATTRIBUTE_USE_NESTED_GROUPS = "useNestedGroups"; /** * Build directory bean. @@ -26,13 +30,9 @@ public class DirectoryBeanUtil { * @param directory the directory * @return the directory bean */ - @Nullable + @Nonnull public static AbstractDirectoryBean toDirectoryBean( - @Nullable final Directory directory) { - - if (directory == null) { - return null; - } + @Nonnull final Directory directory) { if (directory.getType().equals(DirectoryType.INTERNAL)) { return toDirectoryInternalBean(directory); @@ -41,7 +41,7 @@ public static AbstractDirectoryBean toDirectoryBean( return toDirectoryGenericBean(directory); } - private static DirectoryInternalBean toDirectoryInternalBean( + public static DirectoryInternalBean toDirectoryInternalBean( final Directory directory) { final DirectoryInternalBean directoryBean = new DirectoryInternalBean(); @@ -49,16 +49,33 @@ private static DirectoryInternalBean toDirectoryInternalBean( directoryBean.setName(directory.getName()); directoryBean.setDescription(directory.getDescription()); directoryBean.setActive(directory.isActive()); + directoryBean.setCreatedDate(directory.getCreatedDate()); + directoryBean.setUpdatedDate(directory.getUpdatedDate()); + + final Map attributes = new HashMap<>(directory.getAttributes()); + final Set allowedOperations = new HashSet<>(directory.getAllowedOperations()); - final Map directoryAttributes = directory.getAttributes(); directoryBean.setCredentialPolicy(new DirectoryInternalBean.DirectoryInternalCredentialPolicy()); - directoryBean.getCredentialPolicy().setPasswordRegex(directoryAttributes.get(ATTRIBUTE_PASSWORD_REGEX)); - directoryBean.getCredentialPolicy().setPasswordComplexityMessage(directoryAttributes.get(ATTRIBUTE_PASSWORD_COMPLEXITY_MESSAGE)); - directoryBean.getCredentialPolicy().setPasswordMaxAttempts(toLong(directoryAttributes.get(ATTRIBUTE_PASSWORD_MAX_ATTEMPTS))); - directoryBean.getCredentialPolicy().setPasswordHistoryCount(toLong(directoryAttributes.get(ATTRIBUTE_PASSWORD_HISTORY_COUNT))); - directoryBean.getCredentialPolicy().setPasswordMaxChangeTime(toLong(directoryAttributes.get(ATTRIBUTE_PASSWORD_MAX_CHANGE_TIME))); - directoryBean.getCredentialPolicy().setPasswordExpiryNotificationDays(toList(directoryAttributes.get(ATTRIBUTE_PASSWORD_EXPIRATION_NOTIFICATION_PERIODS))); - directoryBean.getCredentialPolicy().setPasswordEncryptionMethod(directoryAttributes.get(ATTRIBUTE_USER_ENCRYPTION_METHOD)); + directoryBean.getCredentialPolicy().setPasswordRegex(attributes.get(ATTRIBUTE_PASSWORD_REGEX)); + directoryBean.getCredentialPolicy().setPasswordComplexityMessage(attributes.get(ATTRIBUTE_PASSWORD_COMPLEXITY_MESSAGE)); + directoryBean.getCredentialPolicy().setPasswordMaxAttempts(toLong(attributes.get(ATTRIBUTE_PASSWORD_MAX_ATTEMPTS))); + directoryBean.getCredentialPolicy().setPasswordHistoryCount(toLong(attributes.get(ATTRIBUTE_PASSWORD_HISTORY_COUNT))); + directoryBean.getCredentialPolicy().setPasswordMaxChangeTime(toLong(attributes.get(ATTRIBUTE_PASSWORD_MAX_CHANGE_TIME))); + directoryBean.getCredentialPolicy().setPasswordExpiryNotificationDays(toIntegerList(attributes.get(ATTRIBUTE_PASSWORD_EXPIRATION_NOTIFICATION_PERIODS))); + directoryBean.getCredentialPolicy().setPasswordEncryptionMethod(attributes.get(ATTRIBUTE_USER_ENCRYPTION_METHOD)); + + directoryBean.setAdvanced(new DirectoryInternalBean.DirectoryInternalAdvanced()); + directoryBean.getAdvanced().setEnableNestedGroups(toBoolean(attributes.getOrDefault(ATTRIBUTE_USE_NESTED_GROUPS, "false"))); + + directoryBean.setPermissions(new DirectoryInternalBean.DirectoryInternalPermissions()); + directoryBean.getPermissions().setAddGroup(allowedOperations.contains(OperationType.CREATE_GROUP)); + directoryBean.getPermissions().setAddUser(allowedOperations.contains(OperationType.CREATE_USER)); + directoryBean.getPermissions().setModifyGroup(allowedOperations.contains(OperationType.UPDATE_GROUP)); + directoryBean.getPermissions().setModifyUser(allowedOperations.contains(OperationType.UPDATE_USER)); + directoryBean.getPermissions().setModifyGroupAttributes(allowedOperations.contains(OperationType.UPDATE_GROUP_ATTRIBUTE)); + directoryBean.getPermissions().setModifyUserAttributes(allowedOperations.contains(OperationType.UPDATE_USER_ATTRIBUTE)); + directoryBean.getPermissions().setRemoveGroup(allowedOperations.contains(OperationType.DELETE_GROUP)); + directoryBean.getPermissions().setRemoveUser(allowedOperations.contains(OperationType.DELETE_USER)); return directoryBean; } @@ -81,88 +98,125 @@ private static DirectoryGenericBean toDirectoryGenericBean( * @param directoryBean the directory bean * @return the directory */ - @Nullable + @Nonnull + public static Directory toDirectory( + @Nonnull final AbstractDirectoryBean directoryBean) { + + final ImmutableDirectory.Builder directoryBuilder = ImmutableDirectory.builder( + directoryBean.getName(), toDirectoryType(directoryBean), toDirectoryImplClass(directoryBean)); + + return toDirectory(directoryBean, directoryBuilder.build()); + } + + /** + * Build directory. + * + * @param directoryBean the directory bean + * @return the directory + */ + @Nonnull public static Directory toDirectory( - @Nullable final AbstractDirectoryBean directoryBean) { + @Nonnull final AbstractDirectoryBean directoryBean, + @Nonnull final Directory directory) { - if (directoryBean == null) { - return null; + final ImmutableDirectory.Builder directoryBuilder = ImmutableDirectory.builder(directory); + + if (directoryBean.getDescription() != null) { + directoryBuilder.setDescription(directoryBean.getDescription()); + } + + if (directoryBean.getActive() != null) { + directoryBuilder.setActive(directoryBean.getActive()); } - DirectoryType directoryType = DirectoryType.UNKNOWN; - final Map directoryAttributes = new HashMap<>(); + final Map attributes = new HashMap<>(directory.getAttributes()); + final Set allowedOperations = new HashSet<>(directory.getAllowedOperations()); - if (directoryBean instanceof DirectoryInternalBean) { - directoryType = DirectoryType.INTERNAL; + if (DirectoryInternalBean.class.equals(directoryBean.getClass())) { final DirectoryInternalBean directoryInternalBean = (DirectoryInternalBean) directoryBean; - final DirectoryInternalBean.DirectoryInternalCredentialPolicy credentialPolicy = directoryInternalBean.getCredentialPolicy(); + final DirectoryInternalBean.DirectoryInternalCredentialPolicy credentialPolicy = directoryInternalBean.getCredentialPolicy(); if (credentialPolicy != null) { - directoryAttributes.put(ATTRIBUTE_PASSWORD_REGEX, credentialPolicy.getPasswordRegex()); - directoryAttributes.put(ATTRIBUTE_PASSWORD_COMPLEXITY_MESSAGE, credentialPolicy.getPasswordComplexityMessage()); - directoryAttributes.put(ATTRIBUTE_PASSWORD_MAX_ATTEMPTS, fromLong(credentialPolicy.getPasswordMaxAttempts())); - directoryAttributes.put(ATTRIBUTE_PASSWORD_HISTORY_COUNT, fromLong(credentialPolicy.getPasswordHistoryCount())); - directoryAttributes.put(ATTRIBUTE_PASSWORD_MAX_CHANGE_TIME, fromLong(credentialPolicy.getPasswordMaxChangeTime())); - directoryAttributes.put(ATTRIBUTE_PASSWORD_EXPIRATION_NOTIFICATION_PERIODS, fromList(credentialPolicy.getPasswordExpiryNotificationDays())); - directoryAttributes.put(ATTRIBUTE_USER_ENCRYPTION_METHOD, credentialPolicy.getPasswordEncryptionMethod()); + setAttributeIfNotNull(attributes, ATTRIBUTE_PASSWORD_REGEX, credentialPolicy.getPasswordRegex()); + setAttributeIfNotNull(attributes, ATTRIBUTE_PASSWORD_COMPLEXITY_MESSAGE, credentialPolicy.getPasswordComplexityMessage()); + setAttributeIfNotNull(attributes, ATTRIBUTE_PASSWORD_MAX_ATTEMPTS, fromLong(credentialPolicy.getPasswordMaxAttempts())); + setAttributeIfNotNull(attributes, ATTRIBUTE_PASSWORD_HISTORY_COUNT, fromLong(credentialPolicy.getPasswordHistoryCount())); + setAttributeIfNotNull(attributes, ATTRIBUTE_PASSWORD_MAX_CHANGE_TIME, fromLong(credentialPolicy.getPasswordMaxChangeTime())); + setAttributeIfNotNull(attributes, ATTRIBUTE_PASSWORD_EXPIRATION_NOTIFICATION_PERIODS, fromIntegerList(credentialPolicy.getPasswordExpiryNotificationDays())); + setAttributeIfNotNull(attributes, ATTRIBUTE_USER_ENCRYPTION_METHOD, credentialPolicy.getPasswordEncryptionMethod()); + } + + final DirectoryInternalBean.DirectoryInternalAdvanced advanced = directoryInternalBean.getAdvanced(); + if (advanced != null) { + setAttributeIfNotNull(attributes, ATTRIBUTE_USE_NESTED_GROUPS, fromBoolean(advanced.getEnableNestedGroups())); + } + + final DirectoryInternalBean.DirectoryInternalPermissions permissions = directoryInternalBean.getPermissions(); + if (permissions != null) { + setAllowedOperationIfNotNull(allowedOperations, OperationType.CREATE_GROUP, permissions.getAddGroup()); + setAllowedOperationIfNotNull(allowedOperations, OperationType.CREATE_USER, permissions.getAddUser()); + setAllowedOperationIfNotNull(allowedOperations, OperationType.UPDATE_GROUP, permissions.getModifyGroup()); + setAllowedOperationIfNotNull(allowedOperations, OperationType.UPDATE_USER, permissions.getModifyUser()); + setAllowedOperationIfNotNull(allowedOperations, OperationType.UPDATE_GROUP_ATTRIBUTE, permissions.getModifyGroupAttributes()); + setAllowedOperationIfNotNull(allowedOperations, OperationType.UPDATE_USER_ATTRIBUTE, permissions.getModifyUserAttributes()); + setAllowedOperationIfNotNull(allowedOperations, OperationType.DELETE_GROUP, permissions.getRemoveGroup()); + setAllowedOperationIfNotNull(allowedOperations, OperationType.DELETE_USER, permissions.getRemoveUser()); } } - return ImmutableDirectory.builder(directoryBean.getName(), directoryType, "") - .setId(directoryBean.getId()) - .setDescription(directoryBean.getDescription()) - .setActive(directoryBean.getActive()) - .setAttributes(directoryAttributes) + return directoryBuilder + .setAttributes(attributes) + .setAllowedOperations(allowedOperations) .build(); } - private static String fromList( - @Nullable final List value) { + @Nonnull + private static DirectoryType toDirectoryType( + @Nonnull final AbstractDirectoryBean directoryBean) { - if (value == null) { - return null; + if (DirectoryInternalBean.class.equals(directoryBean.getClass())) { + return DirectoryType.INTERNAL; + } else if (DirectoryCrowdBean.class.equals(directoryBean.getClass())) { + return DirectoryType.CROWD; + } else if (DirectoryLdapBean.class.equals(directoryBean.getClass())) { + return DirectoryType.AZURE_AD; } - return value.stream() - .sorted() - .map(String::valueOf) - .collect(Collectors.joining(LIST_SEPARATOR)); + return DirectoryType.UNKNOWN; } - @Nullable - private static String fromLong( - @Nullable final Long value) { + private static String toDirectoryImplClass( + @Nonnull final AbstractDirectoryBean directoryBean) { - if (value == null) { - return null; + if (DirectoryInternalBean.class.equals(directoryBean.getClass())) { + return "com.atlassian.crowd.directory.InternalDirectory"; } - return String.valueOf(value); + return null; } - @Nullable - private static List toList( - @Nullable final String value) { + private static void setAttributeIfNotNull( + final Map attributes, + final String attribute, + final String value) { - if (value == null) { - return null; + if (value != null) { + attributes.put(attribute, value); } - - return Stream.of(value.split(LIST_SEPARATOR)) - .sorted() - .map(Integer::valueOf) - .collect(Collectors.toList()); } - @Nullable - private static Long toLong( - @Nullable final String value) { + private static void setAllowedOperationIfNotNull( + final Set allowedOperations, + final OperationType operationType, + final Boolean permission) { - if (value == null) { - return null; - } + if (permission != null) { + allowedOperations.remove(operationType); - return Long.valueOf(value); + if (TRUE.equals(permission)) { + allowedOperations.add(operationType); + } + } } private DirectoryBeanUtil() { diff --git a/src/main/java/de/aservo/confapi/crowd/rest/DirectoriesResourceImpl.java b/src/main/java/de/aservo/confapi/crowd/rest/DirectoriesResourceImpl.java index da5a624..aec6918 100644 --- a/src/main/java/de/aservo/confapi/crowd/rest/DirectoriesResourceImpl.java +++ b/src/main/java/de/aservo/confapi/crowd/rest/DirectoriesResourceImpl.java @@ -2,47 +2,24 @@ import com.sun.jersey.spi.container.ResourceFilters; import de.aservo.confapi.commons.constants.ConfAPI; -import de.aservo.confapi.commons.exception.NotFoundException; -import de.aservo.confapi.commons.model.AbstractDirectoryBean; +import de.aservo.confapi.commons.rest.AbstractDirectoriesResourceImpl; +import de.aservo.confapi.commons.service.api.DirectoriesService; import de.aservo.confapi.crowd.filter.SysadminOnlyResourceFilter; -import de.aservo.confapi.crowd.rest.api.DirectoriesResource; -import de.aservo.confapi.crowd.service.api.DirectoriesService; import org.springframework.stereotype.Component; import javax.inject.Inject; import javax.ws.rs.Path; -import javax.ws.rs.core.Response; @Path(ConfAPI.DIRECTORIES) @ResourceFilters(SysadminOnlyResourceFilter.class) @Component -public class DirectoriesResourceImpl implements DirectoriesResource { - - private final DirectoriesService directoriesService; +public class DirectoriesResourceImpl extends AbstractDirectoriesResourceImpl { @Inject public DirectoriesResourceImpl( final DirectoriesService directoriesService) { - this.directoriesService = directoriesService; - } - - @Override - public Response getDirectories() { - return Response.ok(directoriesService.getDirectories()).build(); - } - - @Override - public Response getDirectory( - final long id) { - - final AbstractDirectoryBean directoryBean = directoriesService.getDirectory(id); - - if (directoryBean == null) { - throw new NotFoundException(String.format("Directory with ID '%d' cannot be found", id)); - } - - return Response.ok(directoryBean).build(); + super(directoriesService); } } diff --git a/src/main/java/de/aservo/confapi/crowd/rest/api/DirectoriesResource.java b/src/main/java/de/aservo/confapi/crowd/rest/api/DirectoriesResource.java deleted file mode 100644 index 05e9b8e..0000000 --- a/src/main/java/de/aservo/confapi/crowd/rest/api/DirectoriesResource.java +++ /dev/null @@ -1,53 +0,0 @@ -package de.aservo.confapi.crowd.rest.api; - -import de.aservo.confapi.commons.constants.ConfAPI; -import de.aservo.confapi.commons.model.DirectoriesBean; -import de.aservo.confapi.commons.model.DirectoryGenericBean; -import de.aservo.confapi.commons.model.DirectoryInternalBean; -import de.aservo.confapi.commons.model.ErrorCollection; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; - -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; - -public interface DirectoriesResource { - - @GET - @Produces(MediaType.APPLICATION_JSON) - @Operation( - tags = { ConfAPI.DIRECTORIES }, - summary = "Get the list of directories", - responses = { - @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = DirectoriesBean.class))), - @ApiResponse(responseCode = "default", content = @Content(schema = @Schema(implementation = ErrorCollection.class))) - } - ) - Response getDirectories(); - - @GET - @Path("{id}") - @Produces(MediaType.APPLICATION_JSON) - @Operation( - tags = { ConfAPI.DIRECTORIES }, - summary = "Get a directory based on it's ID", - responses = { - @ApiResponse(responseCode = "200", content = @Content(schema = @Schema( - oneOf = { - DirectoryInternalBean.class, - DirectoryGenericBean.class, - } - ))), - @ApiResponse(responseCode = "default", content = @Content(schema = @Schema(implementation = ErrorCollection.class))) - } - ) - Response getDirectory( - @PathParam("id") final long id); - -} diff --git a/src/main/java/de/aservo/confapi/crowd/service/DirectoriesServiceImpl.java b/src/main/java/de/aservo/confapi/crowd/service/DirectoriesServiceImpl.java index 145bf23..a80cac1 100644 --- a/src/main/java/de/aservo/confapi/crowd/service/DirectoriesServiceImpl.java +++ b/src/main/java/de/aservo/confapi/crowd/service/DirectoriesServiceImpl.java @@ -1,7 +1,8 @@ package de.aservo.confapi.crowd.service; import com.atlassian.crowd.embedded.api.Directory; -import com.atlassian.crowd.embedded.api.DirectoryType; +import com.atlassian.crowd.exception.DirectoryCurrentlySynchronisingException; +import com.atlassian.crowd.exception.DirectoryInstantiationException; import com.atlassian.crowd.exception.DirectoryNotFoundException; import com.atlassian.crowd.manager.directory.DirectoryManager; import com.atlassian.crowd.search.EntityDescriptor; @@ -9,22 +10,34 @@ import com.atlassian.crowd.search.query.entity.EntityQuery; import com.atlassian.plugin.spring.scanner.annotation.export.ExportAsService; import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import de.aservo.confapi.commons.exception.BadRequestException; +import de.aservo.confapi.commons.exception.InternalServerErrorException; +import de.aservo.confapi.commons.exception.NotFoundException; +import de.aservo.confapi.commons.exception.ServiceUnavailableException; import de.aservo.confapi.commons.model.AbstractDirectoryBean; import de.aservo.confapi.commons.model.DirectoriesBean; +import de.aservo.confapi.commons.model.DirectoryInternalBean; +import de.aservo.confapi.commons.service.api.DirectoriesService; import de.aservo.confapi.crowd.model.util.DirectoryBeanUtil; -import de.aservo.confapi.crowd.service.api.DirectoriesService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; +import javax.annotation.Nonnull; import javax.inject.Inject; +import javax.validation.constraints.NotNull; +import java.util.Collection; +import java.util.Comparator; +import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; +import static com.atlassian.crowd.embedded.api.DirectoryType.INTERNAL; +import static de.aservo.confapi.crowd.model.util.DirectoryBeanUtil.toDirectory; + @Component @ExportAsService(DirectoriesService.class) public class DirectoriesServiceImpl implements DirectoriesService { - private static final Logger log = LoggerFactory.getLogger(DirectoriesServiceImpl.class); + private static final int RETRY_AFTER_IN_SECONDS = 5; @ComponentImport private final DirectoryManager directoryManager; @@ -38,11 +51,7 @@ public DirectoriesServiceImpl( @Override public DirectoriesBean getDirectories() { - final EntityQuery directoryEntityQuery = QueryBuilder.queryFor(Directory.class, EntityDescriptor.directory()) - .returningAtMost(EntityQuery.ALL_RESULTS); - - return new DirectoriesBean(directoryManager.searchDirectories(directoryEntityQuery).stream() - .filter(d -> d.getType().equals(DirectoryType.INTERNAL)) + return new DirectoriesBean(findAllDirectories().stream() .map(DirectoryBeanUtil::toDirectoryBean) .collect(Collectors.toList()) ); @@ -52,12 +61,148 @@ public DirectoriesBean getDirectories() { public AbstractDirectoryBean getDirectory( final long id) { + final Directory directory = findDirectory(id); + return DirectoryBeanUtil.toDirectoryBean(directory); + } + + @Override + public DirectoriesBean setDirectories( + @NotNull final DirectoriesBean directoriesBean, + final boolean testConnection) { + + final Map existingDirectoriesByName = findAllDirectories().stream() + .collect(Collectors.toMap(Directory::getName, Function.identity())); + + for (AbstractDirectoryBean directoryBean : directoriesBean.getDirectories()) { + if (existingDirectoriesByName.containsKey(directoryBean.getName())) { + setDirectory(existingDirectoriesByName.get(directoryBean.getName()).getId(), directoryBean, testConnection); + } else { + addDirectory(directoryBean, testConnection); + } + } + + return getDirectories(); + } + + @Override + public AbstractDirectoryBean setDirectory( + final long id, + @NotNull final AbstractDirectoryBean directoryBean, + final boolean testConnection) { + + if (!(directoryBean instanceof DirectoryInternalBean)) { + throw new BadRequestException(String.format( + "Setting directory type '%s' is not supported (yet)", directoryBean.getClass())); + } + + final Directory existingDirectory = findDirectory(id); + + try { + final Directory mergedDirectory = toDirectory(directoryBean, existingDirectory); + final Directory updatedDirectory = directoryManager.updateDirectory(mergedDirectory); + return DirectoryBeanUtil.toDirectoryInternalBean(updatedDirectory); + } catch (DirectoryNotFoundException e) { + // this should not happen + throw new InternalServerErrorException(String.format( + "When trying to update directory '%s', it could not be found anymore", existingDirectory.getName())); + } + } + + @Override + public AbstractDirectoryBean addDirectory( + final @NotNull AbstractDirectoryBean directoryBean, + final boolean testConnection) { + + if (!(directoryBean instanceof DirectoryInternalBean)) { + throw new BadRequestException(String.format( + "Adding directory type '%s' is not supported (yet)", directoryBean.getClass())); + } + + try { + final Directory directory = toDirectory(directoryBean); + final Directory addedDirectory = directoryManager.addDirectory(directory); + return DirectoryBeanUtil.toDirectoryBean(addedDirectory); + } catch (DirectoryInstantiationException e) { + throw new InternalServerErrorException(String.format("Could not create directory '%s'", directoryBean.getName())); + } + } + + @Override + public void deleteDirectories( + final boolean force) { + + if (!force) { + throw new BadRequestException("'force = true' must be supplied to delete all entries"); + } + + final Collection directories = findAllDirectories(); + + directories.stream() + .sorted(new DirectoryComparator()) + .limit(directories.size() - 1L) + .forEach(d -> deleteDirectory(d.getId())); + } + + @Override + public void deleteDirectory( + final long id) { + + if (findAllDirectories().size() <= 1) { + throw new BadRequestException("Cannot delete directory '%s' as this is the last remaining directory"); + } + + final Directory directory = findDirectory(id); + try { - final Directory directory = directoryManager.findDirectoryById(id); - return DirectoryBeanUtil.toDirectoryBean(directory); + directoryManager.removeDirectory(directory); } catch (DirectoryNotFoundException e) { - log.info("Directory with id {} could not been found", id); - return null; + throw new InternalServerErrorException(String.format( + "When trying to delete directory '%s', it could not be found anymore", directory.getName())); + } catch (DirectoryCurrentlySynchronisingException e) { + throw new ServiceUnavailableException(e, RETRY_AFTER_IN_SECONDS); } } + + @Nonnull + Directory findDirectory( + final long id) { + + try { + return directoryManager.findDirectoryById(id); + } catch (DirectoryNotFoundException e) { + throw new NotFoundException(e); + } + } + + @Nonnull + Collection findAllDirectories() { + final EntityQuery allDirectoriesEntityQuery = QueryBuilder + .queryFor(Directory.class, EntityDescriptor.directory()) + .returningAtMost(EntityQuery.ALL_RESULTS); + + return directoryManager.searchDirectories(allDirectoriesEntityQuery); + } + + /** + * This comparator allows sorting directories as follows: + * + * - Non-internal directories come first + * - For the same directory types, directories with more recent creation date come first + * + * This comparator is used for the "delete-all" method in order to make sure that the oldest internal directory + * will never be deleted in order not to lock out from Crowd. + */ + static class DirectoryComparator implements Comparator { + @Override + public int compare(Directory d1, Directory d2) { + if (d1.getType().equals(INTERNAL) && !d2.getType().equals(INTERNAL)) { + return 1; + } + if (!d1.getType().equals(INTERNAL) && d2.getType().equals(INTERNAL)) { + return -1; + } + return d2.getCreatedDate().compareTo(d1.getCreatedDate()); + } + } + } diff --git a/src/main/java/de/aservo/confapi/crowd/service/api/DirectoriesService.java b/src/main/java/de/aservo/confapi/crowd/service/api/DirectoriesService.java deleted file mode 100644 index e121b29..0000000 --- a/src/main/java/de/aservo/confapi/crowd/service/api/DirectoriesService.java +++ /dev/null @@ -1,16 +0,0 @@ -package de.aservo.confapi.crowd.service.api; - -import de.aservo.confapi.commons.model.AbstractDirectoryBean; -import de.aservo.confapi.commons.model.DirectoriesBean; - -import javax.annotation.Nullable; - -public interface DirectoriesService { - - DirectoriesBean getDirectories(); - - @Nullable - AbstractDirectoryBean getDirectory( - final long id); - -} diff --git a/src/main/java/de/aservo/confapi/crowd/util/AttributeUtil.java b/src/main/java/de/aservo/confapi/crowd/util/AttributeUtil.java new file mode 100644 index 0000000..cc0def9 --- /dev/null +++ b/src/main/java/de/aservo/confapi/crowd/util/AttributeUtil.java @@ -0,0 +1,85 @@ +package de.aservo.confapi.crowd.util; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class AttributeUtil { + + public static final String LIST_SEPARATOR = ","; + + @Nullable + public static String fromLong( + @Nullable final Long value) { + + if (value == null) { + return null; + } + + return String.valueOf(value); + } + + @Nullable + public static String fromBoolean( + @Nullable final Boolean value) { + + if (value == null) { + return null; + } + + return String.valueOf(value.booleanValue()); + } + + public static String fromIntegerList( + @Nullable final List value) { + + if (value == null) { + return null; + } + + return value.stream() + .sorted() + .map(String::valueOf) + .collect(Collectors.joining(LIST_SEPARATOR)); + } + + @Nullable + public static Long toLong( + @Nullable final String value) { + + if (value == null) { + return null; + } + + return Long.valueOf(value); + } + + @Nullable + public static Boolean toBoolean( + @Nullable final String value) { + + if (value == null) { + return null; + } + + return Boolean.valueOf(value); + } + + @Nullable + public static List toIntegerList( + @Nullable final String value) { + + if (value == null) { + return null; + } + + return Stream.of(value.split(LIST_SEPARATOR)) + .sorted() + .map(Integer::valueOf) + .collect(Collectors.toList()); + } + + private AttributeUtil() {} + +} diff --git a/src/test/java/com/atlassian/crowd/embedded/api/MockDirectoryInternal.java b/src/test/java/com/atlassian/crowd/embedded/api/MockDirectoryInternal.java index bb24ad1..86e5de6 100644 --- a/src/test/java/com/atlassian/crowd/embedded/api/MockDirectoryInternal.java +++ b/src/test/java/com/atlassian/crowd/embedded/api/MockDirectoryInternal.java @@ -4,6 +4,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -23,6 +24,7 @@ public class MockDirectoryInternal implements Directory { public static final List ATTRIBUTE_PASSWORD_EXPIRATION_NOTIFICATION_PERIODS_VALUE = Arrays.asList("1", "7"); private final Map attributes; + private final Set allowedOperations; public MockDirectoryInternal() { this.attributes = Stream.of(new String[][] { @@ -33,6 +35,7 @@ public MockDirectoryInternal() { { ATTRIBUTE_PASSWORD_MAX_CHANGE_TIME, String.valueOf(ATTRIBUTE_PASSWORD_MAX_CHANGE_TIME_VALUE) }, { ATTRIBUTE_PASSWORD_EXPIRATION_NOTIFICATION_PERIODS, String.join(",", ATTRIBUTE_PASSWORD_EXPIRATION_NOTIFICATION_PERIODS_VALUE) }, }).collect(Collectors.toMap(data -> data[0], data -> data[1])); + this.allowedOperations = new HashSet<>(); } @Override @@ -67,7 +70,7 @@ public Map getAttributes() { @Override public Set getAllowedOperations() { - return null; + return allowedOperations; } @Override diff --git a/src/test/java/de/aservo/confapi/crowd/model/util/DirectoryBeanUtilTest.java b/src/test/java/de/aservo/confapi/crowd/model/util/DirectoryBeanUtilTest.java index 1d2199b..92452de 100644 --- a/src/test/java/de/aservo/confapi/crowd/model/util/DirectoryBeanUtilTest.java +++ b/src/test/java/de/aservo/confapi/crowd/model/util/DirectoryBeanUtilTest.java @@ -3,6 +3,7 @@ import com.atlassian.crowd.embedded.api.Directory; import com.atlassian.crowd.embedded.api.DirectoryType; import com.atlassian.crowd.embedded.api.MockDirectoryInternal; +import com.atlassian.crowd.embedded.api.OperationType; import de.aservo.confapi.commons.model.AbstractDirectoryBean; import de.aservo.confapi.commons.model.DirectoryInternalBean; import org.junit.Test; @@ -10,6 +11,7 @@ import org.mockito.runners.MockitoJUnitRunner; import java.util.Map; +import java.util.Set; import static com.atlassian.crowd.directory.AbstractInternalDirectory.*; import static org.junit.Assert.*; @@ -55,22 +57,25 @@ public void testToDirectoryGenericBean() { assertEquals(directoryBean.getActive(), directory.isActive()); } - @Test - public void testToDirectoryBeanWithNull() { - assertNull(DirectoryBeanUtil.toDirectoryBean(null)); - } - @Test public void testToDirectory() { final DirectoryInternalBean directoryBean = DirectoryInternalBean.EXAMPLE_1; - final Directory directory = DirectoryBeanUtil.toDirectory(directoryBean); + directoryBean.setPermissions(new DirectoryInternalBean.DirectoryInternalPermissions()); + directoryBean.getPermissions().setAddGroup(true); + directoryBean.getPermissions().setAddUser(true); + directoryBean.getPermissions().setModifyGroup(true); + directoryBean.getPermissions().setModifyUser(true); + directoryBean.getPermissions().setModifyGroupAttributes(true); + directoryBean.getPermissions().setModifyUserAttributes(true); + directoryBean.getPermissions().setRemoveGroup(true); + directoryBean.getPermissions().setRemoveUser(true); + final Directory directory = DirectoryBeanUtil.toDirectory(directoryBean); assertNotNull(directory); - assertEquals(directory.getId(), directoryBean.getId()); assertEquals(directory.getName(), directoryBean.getName()); final Map attributes = directory.getAttributes(); - assertNotNull(directory.getAttributes()); + assertNotNull(attributes); assertEquals(directoryBean.getCredentialPolicy().getPasswordRegex(), attributes.get(ATTRIBUTE_PASSWORD_REGEX)); assertEquals(directoryBean.getCredentialPolicy().getPasswordComplexityMessage(), attributes.get(ATTRIBUTE_PASSWORD_COMPLEXITY_MESSAGE)); assertEquals(String.valueOf(directoryBean.getCredentialPolicy().getPasswordMaxAttempts()), attributes.get(ATTRIBUTE_PASSWORD_MAX_ATTEMPTS)); @@ -78,11 +83,17 @@ public void testToDirectory() { assertEquals(String.valueOf(directoryBean.getCredentialPolicy().getPasswordMaxChangeTime()), attributes.get(ATTRIBUTE_PASSWORD_MAX_CHANGE_TIME)); assertNotNull(attributes.get(ATTRIBUTE_PASSWORD_EXPIRATION_NOTIFICATION_PERIODS)); assertEquals(directoryBean.getCredentialPolicy().getPasswordEncryptionMethod(), attributes.get(ATTRIBUTE_USER_ENCRYPTION_METHOD)); - } - @Test - public void testToDirectoryWithNull() { - assertNull(DirectoryBeanUtil.toDirectory(null)); + final Set allowedOperations = directory.getAllowedOperations(); + assertNotNull(allowedOperations); + assertEquals(directoryBean.getPermissions().getAddGroup(), allowedOperations.contains(OperationType.CREATE_GROUP)); + assertEquals(directoryBean.getPermissions().getAddUser(), allowedOperations.contains(OperationType.CREATE_USER)); + assertEquals(directoryBean.getPermissions().getModifyGroup(), allowedOperations.contains(OperationType.UPDATE_GROUP)); + assertEquals(directoryBean.getPermissions().getModifyUser(), allowedOperations.contains(OperationType.UPDATE_USER)); + assertEquals(directoryBean.getPermissions().getModifyGroup(), allowedOperations.contains(OperationType.UPDATE_GROUP_ATTRIBUTE)); + assertEquals(directoryBean.getPermissions().getModifyUser(), allowedOperations.contains(OperationType.UPDATE_USER_ATTRIBUTE)); + assertEquals(directoryBean.getPermissions().getRemoveGroup(), allowedOperations.contains(OperationType.DELETE_GROUP)); + assertEquals(directoryBean.getPermissions().getRemoveUser(), allowedOperations.contains(OperationType.DELETE_USER)); } } diff --git a/src/test/java/de/aservo/confapi/crowd/rest/DirectoriesResourceTest.java b/src/test/java/de/aservo/confapi/crowd/rest/DirectoriesResourceTest.java index 687ec26..0621979 100644 --- a/src/test/java/de/aservo/confapi/crowd/rest/DirectoriesResourceTest.java +++ b/src/test/java/de/aservo/confapi/crowd/rest/DirectoriesResourceTest.java @@ -2,7 +2,7 @@ import de.aservo.confapi.commons.model.DirectoriesBean; import de.aservo.confapi.commons.model.DirectoryInternalBean; -import de.aservo.confapi.crowd.service.api.DirectoriesService; +import de.aservo.confapi.commons.service.api.DirectoriesService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -10,7 +10,6 @@ import org.mockito.runners.MockitoJUnitRunner; import javax.ws.rs.core.Response; - import java.util.Collections; import static org.junit.Assert.assertEquals; diff --git a/src/test/java/de/aservo/confapi/crowd/service/DirectoriesServiceTest.java b/src/test/java/de/aservo/confapi/crowd/service/DirectoriesServiceTest.java index 10fd152..a96d763 100644 --- a/src/test/java/de/aservo/confapi/crowd/service/DirectoriesServiceTest.java +++ b/src/test/java/de/aservo/confapi/crowd/service/DirectoriesServiceTest.java @@ -1,23 +1,39 @@ package de.aservo.confapi.crowd.service; +import com.atlassian.crowd.embedded.api.Directory; +import com.atlassian.crowd.embedded.api.DirectoryType; import com.atlassian.crowd.embedded.api.MockDirectoryInternal; +import com.atlassian.crowd.exception.CrowdException; +import com.atlassian.crowd.exception.DirectoryCurrentlySynchronisingException; import com.atlassian.crowd.exception.DirectoryNotFoundException; import com.atlassian.crowd.manager.directory.DirectoryManager; +import com.atlassian.crowd.model.directory.ImmutableDirectory; import com.atlassian.crowd.search.query.entity.EntityQuery; +import de.aservo.confapi.commons.exception.BadRequestException; +import de.aservo.confapi.commons.exception.InternalServerErrorException; +import de.aservo.confapi.commons.exception.NotFoundException; +import de.aservo.confapi.commons.exception.ServiceUnavailableException; +import de.aservo.confapi.commons.model.AbstractDirectoryBean; +import de.aservo.confapi.commons.model.DirectoriesBean; +import de.aservo.confapi.crowd.model.util.DirectoryBeanUtil; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.ArrayList; import java.util.Collections; +import java.util.Date; +import java.util.List; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyLong; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) public class DirectoriesServiceTest { @@ -45,15 +61,163 @@ public void testGetDirectory() throws DirectoryNotFoundException { } @Test - public void testGetDirectoryNullWhenNotFoundWithNull() throws DirectoryNotFoundException { - doReturn(null).when(directoryManager).findDirectoryById(anyLong()); - assertNull(directoriesService.getDirectory(1L)); + public void testSetDirectoriesAddNew() { + final Directory directoryInternal = getTestDirectoryInternal(); + final Directory directoryInternalNew = getTestDirectoryInternalOther(); + final DirectoriesServiceImpl spy = spy(directoriesService); + doReturn(Collections.singletonList(directoryInternal)).when(spy).findAllDirectories(); + + final AbstractDirectoryBean directoryBean = DirectoryBeanUtil.toDirectoryBean(directoryInternalNew); + final DirectoriesBean directoriesBean = new DirectoriesBean(Collections.singletonList(directoryBean)); + final boolean testConnection = false; + doReturn(null).when(spy).addDirectory(directoryBean, testConnection); + + spy.setDirectories(directoriesBean, testConnection); + verify(spy).addDirectory(directoryBean, testConnection); + } + + @Test(expected = BadRequestException.class) + public void testSetDirectoriesAddNewUnsupportedType() { + final Directory directoryInternal = getTestDirectoryInternal(); + final Directory directoryAzureAd = getTestDirectoryAzureAd(); + final DirectoriesServiceImpl spy = spy(directoriesService); + doReturn(Collections.singletonList(directoryInternal)).when(spy).findAllDirectories(); + + final AbstractDirectoryBean directoryBean = DirectoryBeanUtil.toDirectoryBean(directoryAzureAd); + final DirectoriesBean directoriesBean = new DirectoriesBean(Collections.singletonList(directoryBean)); + final boolean testConnection = false; + spy.setDirectories(directoriesBean, testConnection); + } + + @Test + public void testSetDirectoriesSetExisting() { + final Directory directory = getTestDirectoryInternal(); + final DirectoriesServiceImpl spy = spy(directoriesService); + doReturn(Collections.singletonList(directory)).when(spy).findAllDirectories(); + + final AbstractDirectoryBean directoryBean = DirectoryBeanUtil.toDirectoryBean(directory); + final DirectoriesBean directoriesBean = new DirectoriesBean(Collections.singletonList(directoryBean)); + final boolean testConnection = false; + doReturn(null).when(spy).setDirectory(directory.getId(), directoryBean, testConnection); + + spy.setDirectories(directoriesBean, testConnection); + verify(spy).setDirectory(directory.getId(), directoryBean, testConnection); + } + + @Test(expected = BadRequestException.class) + public void testSetDirectoriesSetExistingUnsupportedType() { + final Directory directoryInternal = getTestDirectoryInternal(); + final Directory directoryAzureAd = getTestDirectoryAzureAd(); + final DirectoriesServiceImpl spy = spy(directoriesService); + doReturn(List.of(directoryInternal, directoryAzureAd)).when(spy).findAllDirectories(); + + final AbstractDirectoryBean directoryBean = DirectoryBeanUtil.toDirectoryBean(directoryAzureAd); + final DirectoriesBean directoriesBean = new DirectoriesBean(Collections.singletonList(directoryBean)); + final boolean testConnection = false; + spy.setDirectories(directoriesBean, testConnection); + } + + @Test(expected = NotFoundException.class) + public void testGetDirectoryNotFoundException() throws DirectoryNotFoundException { + final Directory directory = getTestDirectoryInternal(); + doThrow(new DirectoryNotFoundException(directory.getName())).when(directoryManager).findDirectoryById(directory.getId()); + directoriesService.getDirectory(directory.getId()); + } + + @Test + public void testDeleteDirectories() throws CrowdException { + final Directory directoryInternal = getTestDirectoryInternal(); + final Directory directoryAzureAd = getTestDirectoryAzureAd(); + final Directory directoryInternalOther = getTestDirectoryInternalOther(); + final DirectoriesServiceImpl spy = spy(directoriesService); + doReturn(List.of(directoryInternal, directoryAzureAd, directoryInternalOther)).when(spy).findAllDirectories(); + + spy.deleteDirectories(true); + verify(spy, never()).deleteDirectory(directoryInternal.getId()); + verify(spy).deleteDirectory(directoryAzureAd.getId()); + verify(spy).deleteDirectory(directoryInternalOther.getId()); + } + + @Test(expected = BadRequestException.class) + public void testDeleteDirectoriesMissingForce() { + directoriesService.deleteDirectories(false); + } + + @Test + public void testDeleteDirectory() throws CrowdException { + final Directory directory = getTestDirectoryAzureAd(); + doReturn(List.of(getTestDirectoryInternal(), directory)).when(directoryManager).searchDirectories(any()); + doReturn(directory).when(directoryManager).findDirectoryById(directory.getId()); + directoriesService.deleteDirectory(directory.getId()); + verify(directoryManager).removeDirectory(directory); + } + + @Test(expected = BadRequestException.class) + public void testDeleteDirectoryLast() { + final Directory directory = getTestDirectoryInternal(); + doReturn(Collections.singletonList(directory)).when(directoryManager).searchDirectories(any()); + directoriesService.deleteDirectory(directory.getId()); + } + + // This case should actually not happen, but we need the code coverage + @Test(expected = InternalServerErrorException.class) + public void testDeleteDirectoryNotFoundAfterAlreadyFound() throws CrowdException { + final Directory directory = getTestDirectoryInternal(); + doReturn(List.of(directory, getTestDirectoryAzureAd())).when(directoryManager).searchDirectories(any()); + doReturn(directory).when(directoryManager).findDirectoryById(directory.getId()); + doThrow(new DirectoryNotFoundException("Directory")).when(directoryManager).removeDirectory(directory); + directoriesService.deleteDirectory(directory.getId()); + } + + @Test(expected = ServiceUnavailableException.class) + public void testDeleteDirectorySynchronizing() throws CrowdException { + final Directory directory = getTestDirectoryInternal(); + doReturn(List.of(directory, getTestDirectoryAzureAd())).when(directoryManager).searchDirectories(any()); + doReturn(directory).when(directoryManager).findDirectoryById(directory.getId()); + doThrow(new DirectoryCurrentlySynchronisingException(1L)).when(directoryManager).removeDirectory(directory); + directoriesService.deleteDirectory(directory.getId()); } @Test - public void testGetDirectoryNullWhenNotFoundWithException() throws DirectoryNotFoundException { - doThrow(new DirectoryNotFoundException("Directory")).when(directoryManager).findDirectoryById(anyLong()); - assertNull(directoriesService.getDirectory(1L)); + public void testDirectoryComparator() { + final List directories = new ArrayList<>(getTestDirectories()); + directories.sort(new DirectoriesServiceImpl.DirectoryComparator()); + assertEquals(2L, (long) directories.get(0).getId()); + assertEquals(3L, (long) directories.get(1).getId()); + assertEquals(1L, (long) directories.get(2).getId()); } + private Directory getTestDirectoryInternal() { + return ImmutableDirectory + .builder("Old Internal Directory", DirectoryType.INTERNAL, "internal") + .setId(1L) + .setCreatedDate(toDate(LocalDate.now().minusDays(3))). + build(); + } + + private Directory getTestDirectoryAzureAd() { + return ImmutableDirectory + .builder("Azure AD Directory", DirectoryType.AZURE_AD, "azuread") + .setId(2L) + .setCreatedDate(toDate(LocalDate.now().minusDays(1))) + .build(); + } + + private Directory getTestDirectoryInternalOther() { + return ImmutableDirectory + .builder("New Internal Directory", DirectoryType.INTERNAL, "internal") + .setId(3L) + .setCreatedDate(toDate(LocalDate.now())) + .build(); + } + + private List getTestDirectories() { + return List.of(getTestDirectoryInternal(), getTestDirectoryAzureAd(), getTestDirectoryInternalOther()); + } + + private Date toDate( + final LocalDate localDate) { + + return Date.from(localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); + } }