diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/find/DefaultFindAccountService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/find/DefaultFindAccountService.java index 3553dbeda..e2b4fde80 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/find/DefaultFindAccountService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/find/DefaultFindAccountService.java @@ -16,6 +16,7 @@ package it.infn.mw.iam.api.account.find; import static it.infn.mw.iam.api.utils.FindUtils.responseFromPage; +import static it.infn.mw.iam.api.utils.FindUtils.responseFromOptional; import java.util.Optional; import java.util.function.Supplier; @@ -25,11 +26,9 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; - import it.infn.mw.iam.api.scim.converter.UserConverter; import it.infn.mw.iam.api.scim.exception.IllegalArgumentException; import it.infn.mw.iam.api.scim.model.ScimListResponse; -import it.infn.mw.iam.api.scim.model.ScimListResponse.ScimListResponseBuilder; import it.infn.mw.iam.api.scim.model.ScimUser; import it.infn.mw.iam.persistence.model.IamAccount; import it.infn.mw.iam.persistence.model.IamGroup; @@ -65,18 +64,13 @@ public ScimListResponse findAccountByLabel(String labelName, String la @Override public ScimListResponse findAccountByEmail(String emailAddress) { Optional account = repo.findByEmail(emailAddress); - - ScimListResponseBuilder builder = ScimListResponse.builder(); - account.ifPresent(a -> builder.singleResource(converter.dtoFromEntity(a))); - return builder.build(); + return responseFromOptional(account, converter); } @Override public ScimListResponse findAccountByUsername(String username) { Optional account = repo.findByUsername(username); - ScimListResponseBuilder builder = ScimListResponse.builder(); - account.ifPresent(a -> builder.singleResource(converter.dtoFromEntity(a))); - return builder.build(); + return responseFromOptional(account, converter); } @Override @@ -115,9 +109,7 @@ private Supplier groupNotFoundError(String groupNameOr @Override public ScimListResponse findAccountByCertificateSubject(String certSubject) { Optional account = repo.findByCertificateSubject(certSubject); - ScimListResponseBuilder builder = ScimListResponse.builder(); - account.ifPresent(a -> builder.singleResource(converter.dtoFromEntity(a))); - return builder.build(); + return responseFromOptional(account, converter); } @Override @@ -143,4 +135,9 @@ public ScimListResponse findAccountByGroupUuidWithFilter(String groupU Page results = repo.findByGroupUuidWithFilter(group.getUuid(), filter, pageable); return responseFromPage(results, converter, pageable); } + + public ScimListResponse findAccountByUuid(String uuid) { + Optional account = repo.findByUuid(uuid); + return responseFromOptional(account, converter); + } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/find/FindAccountController.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/find/FindAccountController.java index 2356b93ee..2c540b8c8 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/find/FindAccountController.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/find/FindAccountController.java @@ -25,10 +25,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import it.infn.mw.iam.api.common.ListResponseDTO; import it.infn.mw.iam.api.common.form.PaginatedRequestWithFilterForm; @@ -44,6 +41,7 @@ public class FindAccountController { public static final String FIND_BY_LABEL_RESOURCE = "/iam/account/find/bylabel"; public static final String FIND_BY_EMAIL_RESOURCE = "/iam/account/find/byemail"; public static final String FIND_BY_USERNAME_RESOURCE = "/iam/account/find/byusername"; + public static final String FIND_BY_UUID_RESOURCE = "/iam/account/find/byuuid/{accountUuid}"; public static final String FIND_BY_CERT_SUBJECT_RESOURCE = "/iam/account/find/bycertsubject"; public static final String FIND_BY_GROUP_RESOURCE = "/iam/account/find/bygroup/{groupUuid}"; public static final String FIND_NOT_IN_GROUP_RESOURCE = @@ -121,4 +119,9 @@ public ListResponseDTO findNotInGroup(@PathVariable String groupUuid, } } + @GetMapping(value = FIND_BY_UUID_RESOURCE, produces = ScimConstants.SCIM_CONTENT_TYPE) + @PreAuthorize("#iam.hasScope('iam:admin.read') or #iam.hasDashboardRole('ROLE_ADMIN') or hasRole('USER')") + public ListResponseDTO findByUuid(@PathVariable String accountUuid) { + return service.findAccountByUuid(accountUuid); + } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/find/FindAccountService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/find/FindAccountService.java index 24314cca9..67825b9d6 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/find/FindAccountService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/find/FindAccountService.java @@ -45,5 +45,6 @@ ScimListResponse findAccountByGroupUuidWithFilter(String groupUuid, St ScimListResponse findAccountNotInGroupWithFilter(String groupUuid, String filter, Pageable pageable); - + + ScimListResponse findAccountByUuid(String uuid); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/error/ClientSuspended.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/error/ClientSuspended.java new file mode 100644 index 000000000..283e41d28 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/error/ClientSuspended.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.api.client.error; + +public class ClientSuspended extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public ClientSuspended(String message) { + super(message); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/ClientManagementAPIController.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/ClientManagementAPIController.java index a761d86df..406891a30 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/ClientManagementAPIController.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/ClientManagementAPIController.java @@ -33,6 +33,7 @@ import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -44,6 +45,7 @@ import com.fasterxml.jackson.annotation.JsonView; +import it.infn.mw.iam.api.account.AccountUtils; import it.infn.mw.iam.api.client.error.InvalidPaginationRequest; import it.infn.mw.iam.api.client.error.NoSuchClient; import it.infn.mw.iam.api.client.management.service.ClientManagementService; @@ -53,6 +55,7 @@ import it.infn.mw.iam.api.common.PagingUtils; import it.infn.mw.iam.api.common.client.RegisteredClientDTO; import it.infn.mw.iam.api.scim.model.ScimUser; +import it.infn.mw.iam.persistence.model.IamAccount; @RestController @RequestMapping(ClientManagementAPIController.ENDPOINT) @@ -61,9 +64,11 @@ public class ClientManagementAPIController { public static final String ENDPOINT = "/iam/api/clients"; private final ClientManagementService managementService; + private final AccountUtils accountUtils; - public ClientManagementAPIController(ClientManagementService managementService) { + public ClientManagementAPIController(ClientManagementService managementService, AccountUtils accountUtils) { this.managementService = managementService; + this.accountUtils = accountUtils; } @PostMapping @@ -140,6 +145,20 @@ public RegisteredClientDTO updateClient(@PathVariable String clientId, return managementService.updateClient(clientId, client); } + @PatchMapping("/{clientId}/enable") + @PreAuthorize("#iam.hasScope('iam:admin.write') or #iam.hasDashboardRole('ROLE_ADMIN')") + public void enableClient(@PathVariable String clientId) { + Optional account = accountUtils.getAuthenticatedUserAccount(); + account.ifPresent(a -> managementService.updateClientStatus(clientId, true, a.getUuid())); + } + + @PatchMapping("/{clientId}/disable") + @PreAuthorize("#iam.hasScope('iam:admin.write') or #iam.hasDashboardRole('ROLE_ADMIN')") + public void disableClient(@PathVariable String clientId) { + Optional account = accountUtils.getAuthenticatedUserAccount(); + account.ifPresent(a -> managementService.updateClientStatus(clientId, false, a.getUuid())); + } + @PostMapping("/{clientId}/secret") @ResponseStatus(CREATED) @PreAuthorize("#iam.hasScope('iam:admin.write') or #iam.hasDashboardRole('ROLE_ADMIN')") diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/ClientManagementService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/ClientManagementService.java index 9e9531b88..ccfa02d64 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/ClientManagementService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/ClientManagementService.java @@ -50,6 +50,8 @@ RegisteredClientDTO updateClient(@NotBlank String clientId, void deleteClientByClientId(@NotBlank String clientId); + void updateClientStatus(String clientId, boolean status, String userId); + ListResponseDTO getClientOwners(@NotBlank String clientId, @NotNull Pageable pageable); void assignClientOwner(@NotBlank String clientId, @IamAccountId String accountId); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java index 2298908e9..25c12c870 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/management/service/DefaultClientManagementService.java @@ -52,6 +52,7 @@ import it.infn.mw.iam.audit.events.client.ClientRegistrationAccessTokenRotatedEvent; import it.infn.mw.iam.audit.events.client.ClientRemovedEvent; import it.infn.mw.iam.audit.events.client.ClientSecretUpdatedEvent; +import it.infn.mw.iam.audit.events.client.ClientStatusChangedEvent; import it.infn.mw.iam.audit.events.client.ClientUpdatedEvent; import it.infn.mw.iam.core.IamTokenService; import it.infn.mw.iam.persistence.model.IamAccount; @@ -116,6 +117,7 @@ public RegisteredClientDTO saveNewClient(RegisteredClientDTO client) throws Pars ClientDetailsEntity entity = converter.entityFromClientManagementRequest(client); entity.setDynamicallyRegistered(false); entity.setCreatedAt(Date.from(clock.instant())); + entity.setActive(true); defaultsService.setupClientDefaults(entity); entity = clientService.saveNewClient(entity); @@ -133,6 +135,16 @@ public void deleteClientByClientId(String clientId) { eventPublisher.publishEvent(new ClientRemovedEvent(this, client)); } + @Override + public void updateClientStatus(String clientId, boolean status, String userId) { + + ClientDetailsEntity client = clientService.findClientByClientId(clientId) + .orElseThrow(ClientSuppliers.clientNotFound(clientId)); + client = clientService.updateClientStatus(client, status, userId); + String message = "Client " + (status?"enabled":"disabled"); + eventPublisher.publishEvent(new ClientStatusChangedEvent(this, client, message)); + } + @Validated(OnClientUpdate.class) @Override public RegisteredClientDTO updateClient(String clientId, RegisteredClientDTO client) @@ -148,6 +160,7 @@ public RegisteredClientDTO updateClient(String clientId, RegisteredClientDTO cli newClient.setClientId(oldClient.getClientId()); newClient.setAuthorities(oldClient.getAuthorities()); newClient.setDynamicallyRegistered(oldClient.isDynamicallyRegistered()); + newClient.setActive(oldClient.isActive()); if (NONE.equals(newClient.getTokenEndpointAuthMethod())) { newClient.setClientSecret(null); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/ClientRegistrationApiController.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/ClientRegistrationApiController.java index aaece937b..5b8ef1cd6 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/ClientRegistrationApiController.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/ClientRegistrationApiController.java @@ -40,6 +40,7 @@ import com.fasterxml.jackson.annotation.JsonView; +import it.infn.mw.iam.api.client.error.ClientSuspended; import it.infn.mw.iam.api.client.error.InvalidClientRegistrationRequest; import it.infn.mw.iam.api.client.error.NoSuchClient; import it.infn.mw.iam.api.client.registration.service.ClientRegistrationService; @@ -119,6 +120,12 @@ public ErrorDTO noSuchClient(HttpServletRequest req, Exception ex) { return ErrorDTO.fromString(ex.getMessage()); } + @ResponseStatus(value = HttpStatus.FORBIDDEN) + @ExceptionHandler(ClientSuspended.class) + public ErrorDTO clientSuspended(HttpServletRequest req, Exception ex) { + return ErrorDTO.fromString(ex.getMessage()); + } + @ResponseStatus(value = HttpStatus.BAD_REQUEST) @ExceptionHandler(InvalidClientRegistrationRequest.class) public ErrorDTO invalidRequest(HttpServletRequest req, Exception ex) { diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java index 09bc73fb8..e9aa131fb 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java @@ -48,6 +48,7 @@ import it.infn.mw.iam.api.account.AccountUtils; import it.infn.mw.iam.api.client.error.InvalidClientRegistrationRequest; +import it.infn.mw.iam.api.client.error.ClientSuspended; import it.infn.mw.iam.api.client.registration.validation.OnDynamicClientRegistration; import it.infn.mw.iam.api.client.registration.validation.OnDynamicClientUpdate; import it.infn.mw.iam.api.client.service.ClientConverter; @@ -320,6 +321,15 @@ && registrationAccessTokenAuthenticationValidForClientId(client.getClientId(), a return Optional.empty(); } + private void checkUserUpdatingSuspendedClient(Authentication authentication, ClientDetailsEntity oldClient) { + if (accountUtils.isAdmin(authentication)) { + return; + } + if(!oldClient.isActive()){ + throw new ClientSuspended("Client " + oldClient.getClientId() + " is suspended!"); + } + } + @Validated(OnDynamicClientRegistration.class) @Override public RegisteredClientDTO registerClient(RegisteredClientDTO request, @@ -330,6 +340,7 @@ public RegisteredClientDTO registerClient(RegisteredClientDTO request, ClientDetailsEntity client = converter.entityFromRegistrationRequest(request); defaultsService.setupClientDefaults(client); client.setDynamicallyRegistered(true); + client.setActive(true); checkAllowedGrantTypes(request, authentication); cleanupRequestedScopes(client, authentication); @@ -395,9 +406,10 @@ public RegisteredClientDTO updateClient(String clientId, RegisteredClientDTO req ClientDetailsEntity oldClient = lookupClient(clientId, authentication).orElseThrow(clientNotFound(clientId)); + checkUserUpdatingSuspendedClient(authentication, oldClient); checkAllowedGrantTypesOnUpdate(request, authentication, oldClient); cleanupRequestedScopesOnUpdate(request, authentication, oldClient); - + ClientDetailsEntity newClient = converter.entityFromRegistrationRequest(request); newClient.setId(oldClient.getId()); newClient.setClientSecret(oldClient.getClientSecret()); @@ -410,6 +422,7 @@ public RegisteredClientDTO updateClient(String clientId, RegisteredClientDTO req newClient.setAuthorities(oldClient.getAuthorities()); newClient.setCreatedAt(oldClient.getCreatedAt()); newClient.setReuseRefreshToken(oldClient.isReuseRefreshToken()); + newClient.setActive(oldClient.isActive()); ClientDetailsEntity savedClient = clientService.updateClient(newClient); @@ -421,8 +434,7 @@ public RegisteredClientDTO updateClient(String clientId, RegisteredClientDTO req eventPublisher.publishEvent(new ClientRegistrationAccessTokenRotatedEvent(this, savedClient)); response.setRegistrationAccessToken(t); }); - - return response; + return response; } @Override diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/ClientConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/ClientConverter.java index cb251e9c4..bc5040cf6 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/ClientConverter.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/ClientConverter.java @@ -1,243 +1,247 @@ -/** - * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package it.infn.mw.iam.api.client.service; - -import static java.util.Objects.isNull; -import static java.util.stream.Collectors.toSet; - -import java.text.ParseException; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; - -import org.mitre.oauth2.model.ClientDetailsEntity; -import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; -import org.mitre.oauth2.model.PKCEAlgorithm; -import org.springframework.stereotype.Component; - -import com.google.common.base.Strings; -import com.nimbusds.jose.jwk.JWKSet; - -import it.infn.mw.iam.api.client.registration.ClientRegistrationApiController; -import it.infn.mw.iam.api.common.client.AuthorizationGrantType; -import it.infn.mw.iam.api.common.client.OAuthResponseType; -import it.infn.mw.iam.api.common.client.RegisteredClientDTO; -import it.infn.mw.iam.api.common.client.TokenEndpointAuthenticationMethod; -import it.infn.mw.iam.config.IamProperties; -import it.infn.mw.iam.config.client_registration.ClientRegistrationProperties; - -@Component -public class ClientConverter { - - private final IamProperties iamProperties; - - private final String clientRegistrationBaseUrl; - private final ClientRegistrationProperties clientRegistrationProperties; - - public ClientConverter(IamProperties properties, - ClientRegistrationProperties clientRegistrationProperties) { - this.iamProperties = properties; - this.clientRegistrationProperties = clientRegistrationProperties; - clientRegistrationBaseUrl = - String.format("%s%s", iamProperties.getBaseUrl(), ClientRegistrationApiController.ENDPOINT); - } - - private Set cloneSet(Set stringSet) { - Set result = new HashSet<>(); - if (stringSet != null) { - result.addAll(stringSet); - } - return result; - } - - - public ClientDetailsEntity entityFromClientManagementRequest(RegisteredClientDTO dto) - throws ParseException { - ClientDetailsEntity client = entityFromRegistrationRequest(dto); - - if (dto.getAccessTokenValiditySeconds() != null && dto.getAccessTokenValiditySeconds() > 0) { - client.setAccessTokenValiditySeconds(dto.getAccessTokenValiditySeconds()); - } - // Refresh Token validity seconds zero value is valid and means infinite duration - if (dto.getRefreshTokenValiditySeconds() != null && dto.getRefreshTokenValiditySeconds() >= 0) { - client.setRefreshTokenValiditySeconds(dto.getRefreshTokenValiditySeconds()); - } - if (dto.getIdTokenValiditySeconds() != null && dto.getIdTokenValiditySeconds() > 0) { - client.setIdTokenValiditySeconds(dto.getIdTokenValiditySeconds()); - } - if (dto.getDeviceCodeValiditySeconds() != null && dto.getDeviceCodeValiditySeconds() > 0) { - client.setDeviceCodeValiditySeconds(dto.getDeviceCodeValiditySeconds()); - } - - client.setAllowIntrospection(dto.isAllowIntrospection()); - client.setReuseRefreshToken(dto.isReuseRefreshToken()); - client.setClearAccessTokensOnRefresh(dto.isClearAccessTokensOnRefresh()); - - if (dto.getCodeChallengeMethod() != null) { - PKCEAlgorithm pkceAlgo = PKCEAlgorithm.parse(dto.getCodeChallengeMethod()); - client.setCodeChallengeMethod(pkceAlgo); - } - - if (dto.getTokenEndpointAuthMethod() != null) { - client - .setTokenEndpointAuthMethod(AuthMethod.getByValue(dto.getTokenEndpointAuthMethod().name())); - } - - client.setRequireAuthTime(Boolean.valueOf(dto.isRequireAuthTime())); - - return client; - } - - - - public RegisteredClientDTO registeredClientDtoFromEntity(ClientDetailsEntity entity) { - RegisteredClientDTO clientDTO = new RegisteredClientDTO(); - - clientDTO.setClientId(entity.getClientId()); - clientDTO.setClientSecret(entity.getClientSecret()); - clientDTO.setClientName(entity.getClientName()); - clientDTO.setContacts(entity.getContacts()); - clientDTO.setGrantTypes(entity.getGrantTypes() - .stream() - .map(AuthorizationGrantType::fromGrantType) - .collect(toSet())); - - clientDTO.setJwksUri(entity.getJwksUri()); - clientDTO.setRedirectUris(cloneSet(entity.getRedirectUris())); - - clientDTO.setTokenEndpointAuthMethod(TokenEndpointAuthenticationMethod - .valueOf(Optional.ofNullable(entity.getTokenEndpointAuthMethod()) - .orElse(AuthMethod.NONE) - .getValue())); - - clientDTO.setScope(cloneSet(entity.getScope())); - clientDTO.setTosUri(entity.getTosUri()); - +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.api.client.service; + +import static java.util.Objects.isNull; +import static java.util.stream.Collectors.toSet; + +import java.text.ParseException; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; +import org.mitre.oauth2.model.PKCEAlgorithm; +import org.springframework.stereotype.Component; + +import com.google.common.base.Strings; +import com.nimbusds.jose.jwk.JWKSet; + +import it.infn.mw.iam.api.client.registration.ClientRegistrationApiController; +import it.infn.mw.iam.api.common.client.AuthorizationGrantType; +import it.infn.mw.iam.api.common.client.OAuthResponseType; +import it.infn.mw.iam.api.common.client.RegisteredClientDTO; +import it.infn.mw.iam.api.common.client.TokenEndpointAuthenticationMethod; +import it.infn.mw.iam.config.IamProperties; +import it.infn.mw.iam.config.client_registration.ClientRegistrationProperties; + +@Component +public class ClientConverter { + + private final IamProperties iamProperties; + + private final String clientRegistrationBaseUrl; + private final ClientRegistrationProperties clientRegistrationProperties; + + public ClientConverter(IamProperties properties, + ClientRegistrationProperties clientRegistrationProperties) { + this.iamProperties = properties; + this.clientRegistrationProperties = clientRegistrationProperties; + clientRegistrationBaseUrl = + String.format("%s%s", iamProperties.getBaseUrl(), ClientRegistrationApiController.ENDPOINT); + } + + private Set cloneSet(Set stringSet) { + Set result = new HashSet<>(); + if (stringSet != null) { + result.addAll(stringSet); + } + return result; + } + + + public ClientDetailsEntity entityFromClientManagementRequest(RegisteredClientDTO dto) + throws ParseException { + ClientDetailsEntity client = entityFromRegistrationRequest(dto); + + if (dto.getAccessTokenValiditySeconds() != null && dto.getAccessTokenValiditySeconds() > 0) { + client.setAccessTokenValiditySeconds(dto.getAccessTokenValiditySeconds()); + } + // Refresh Token validity seconds zero value is valid and means infinite duration + if (dto.getRefreshTokenValiditySeconds() != null && dto.getRefreshTokenValiditySeconds() >= 0) { + client.setRefreshTokenValiditySeconds(dto.getRefreshTokenValiditySeconds()); + } + if (dto.getIdTokenValiditySeconds() != null && dto.getIdTokenValiditySeconds() > 0) { + client.setIdTokenValiditySeconds(dto.getIdTokenValiditySeconds()); + } + if (dto.getDeviceCodeValiditySeconds() != null && dto.getDeviceCodeValiditySeconds() > 0) { + client.setDeviceCodeValiditySeconds(dto.getDeviceCodeValiditySeconds()); + } + + client.setAllowIntrospection(dto.isAllowIntrospection()); + client.setReuseRefreshToken(dto.isReuseRefreshToken()); + client.setClearAccessTokensOnRefresh(dto.isClearAccessTokensOnRefresh()); + + if (dto.getCodeChallengeMethod() != null) { + PKCEAlgorithm pkceAlgo = PKCEAlgorithm.parse(dto.getCodeChallengeMethod()); + client.setCodeChallengeMethod(pkceAlgo); + } + + if (dto.getTokenEndpointAuthMethod() != null) { + client + .setTokenEndpointAuthMethod(AuthMethod.getByValue(dto.getTokenEndpointAuthMethod().name())); + } + + client.setRequireAuthTime(Boolean.valueOf(dto.isRequireAuthTime())); + + return client; + } + + + + public RegisteredClientDTO registeredClientDtoFromEntity(ClientDetailsEntity entity) { + RegisteredClientDTO clientDTO = new RegisteredClientDTO(); + + clientDTO.setClientId(entity.getClientId()); + clientDTO.setClientSecret(entity.getClientSecret()); + clientDTO.setClientName(entity.getClientName()); + clientDTO.setContacts(entity.getContacts()); + clientDTO.setGrantTypes(entity.getGrantTypes() + .stream() + .map(AuthorizationGrantType::fromGrantType) + .collect(toSet())); + + clientDTO.setJwksUri(entity.getJwksUri()); + clientDTO.setRedirectUris(cloneSet(entity.getRedirectUris())); + + clientDTO.setTokenEndpointAuthMethod(TokenEndpointAuthenticationMethod + .valueOf(Optional.ofNullable(entity.getTokenEndpointAuthMethod()) + .orElse(AuthMethod.NONE) + .getValue())); + + clientDTO.setScope(cloneSet(entity.getScope())); + clientDTO.setTosUri(entity.getTosUri()); + clientDTO.setCreatedAt(entity.getCreatedAt()); if (entity.getClientLastUsed() != null) { clientDTO.setLastUsed(entity.getClientLastUsed().getLastUsed()); - } - clientDTO.setAccessTokenValiditySeconds(entity.getAccessTokenValiditySeconds()); - clientDTO.setAllowIntrospection(entity.isAllowIntrospection()); - clientDTO.setClearAccessTokensOnRefresh(entity.isClearAccessTokensOnRefresh()); - clientDTO.setClientDescription(entity.getClientDescription()); - clientDTO.setClientUri(entity.getClientUri()); - clientDTO.setDeviceCodeValiditySeconds(entity.getDeviceCodeValiditySeconds()); - clientDTO.setDynamicallyRegistered(entity.isDynamicallyRegistered()); - clientDTO.setIdTokenValiditySeconds(entity.getIdTokenValiditySeconds()); - clientDTO.setJwksUri(entity.getJwksUri()); - - Optional.ofNullable(entity.getJwks()).ifPresent(k -> clientDTO.setJwk(k.toString())); - clientDTO.setPolicyUri(entity.getPolicyUri()); - clientDTO.setRefreshTokenValiditySeconds(entity.getRefreshTokenValiditySeconds()); - - Optional.ofNullable(entity.getResponseTypes()) - .ifPresent(rts -> clientDTO - .setResponseTypes(rts.stream().map(OAuthResponseType::fromResponseType).collect(toSet()))); - - clientDTO.setReuseRefreshToken(entity.isReuseRefreshToken()); - - if (entity.isDynamicallyRegistered()) { - clientDTO.setRegistrationClientUri( - String.format("%s/%s", clientRegistrationBaseUrl, entity.getClientId())); - } - - if (entity.getCodeChallengeMethod() != null) { - clientDTO.setCodeChallengeMethod(entity.getCodeChallengeMethod().getName()); - } - - if (entity.getRequireAuthTime() != null) { - clientDTO.setRequireAuthTime(entity.getRequireAuthTime()); - } else { - clientDTO.setRequireAuthTime(false); - } - - return clientDTO; - } - - public ClientDetailsEntity entityFromRegistrationRequest(RegisteredClientDTO dto) - throws ParseException { - - ClientDetailsEntity client = new ClientDetailsEntity(); - - client.setClientId(dto.getClientId()); - client.setClientDescription(dto.getClientDescription()); - client.setClientName(dto.getClientName()); - client.setClientSecret(dto.getClientSecret()); - - client.setClientUri(dto.getClientUri()); - - if (!Strings.isNullOrEmpty(dto.getJwksUri())) { - client.setJwksUri(dto.getJwksUri()); - } else if (!Strings.isNullOrEmpty(dto.getJwk())) { - client.setJwks(JWKSet.parse(dto.getJwk())); - } - - client.setPolicyUri(dto.getPolicyUri()); - - client.setRedirectUris(cloneSet(dto.getRedirectUris())); - - client.setScope(cloneSet(dto.getScope())); - - client.setGrantTypes(new HashSet<>()); - - if (!isNull(dto.getGrantTypes())) { - client.setGrantTypes( - dto.getGrantTypes() - .stream() - .map(AuthorizationGrantType::getGrantType) - .collect(toSet())); - } - - if (dto.getScope().contains("offline_access")) { - client.getGrantTypes().add(AuthorizationGrantType.REFRESH_TOKEN.getGrantType()); - } - - if (!isNull(dto.getResponseTypes())) { - client.setResponseTypes( - dto.getResponseTypes().stream().map(OAuthResponseType::getResponseType).collect(toSet())); - } - - client.setContacts(cloneSet(dto.getContacts())); - - if (!isNull(dto.getTokenEndpointAuthMethod())) { - client - .setTokenEndpointAuthMethod(AuthMethod.getByValue(dto.getTokenEndpointAuthMethod().name())); - } - - if (dto.getCodeChallengeMethod() != null) { - PKCEAlgorithm pkceAlgo = PKCEAlgorithm.parse(dto.getCodeChallengeMethod()); - client.setCodeChallengeMethod(pkceAlgo); - } - - // bypasses MitreID default setting to zero inside client's entity - client.setAccessTokenValiditySeconds(clientRegistrationProperties.getClientDefaults().getDefaultAccessTokenValiditySeconds()); - client.setRefreshTokenValiditySeconds(clientRegistrationProperties.getClientDefaults().getDefaultRefreshTokenValiditySeconds()); - client.setIdTokenValiditySeconds(clientRegistrationProperties.getClientDefaults().getDefaultIdTokenValiditySeconds()); - client.setDeviceCodeValiditySeconds(clientRegistrationProperties.getClientDefaults().getDefaultDeviceCodeValiditySeconds()); - - return client; - } - - public RegisteredClientDTO registrationResponseFromClient(ClientDetailsEntity entity) { - RegisteredClientDTO response = registeredClientDtoFromEntity(entity); - response.setRegistrationClientUri( - String.format("%s/%s", clientRegistrationBaseUrl, entity.getClientId())); - - return response; - } - -} + } + clientDTO.setAccessTokenValiditySeconds(entity.getAccessTokenValiditySeconds()); + clientDTO.setAllowIntrospection(entity.isAllowIntrospection()); + clientDTO.setClearAccessTokensOnRefresh(entity.isClearAccessTokensOnRefresh()); + clientDTO.setClientDescription(entity.getClientDescription()); + clientDTO.setClientUri(entity.getClientUri()); + clientDTO.setDeviceCodeValiditySeconds(entity.getDeviceCodeValiditySeconds()); + clientDTO.setDynamicallyRegistered(entity.isDynamicallyRegistered()); + clientDTO.setIdTokenValiditySeconds(entity.getIdTokenValiditySeconds()); + clientDTO.setJwksUri(entity.getJwksUri()); + + Optional.ofNullable(entity.getJwks()).ifPresent(k -> clientDTO.setJwk(k.toString())); + clientDTO.setPolicyUri(entity.getPolicyUri()); + clientDTO.setRefreshTokenValiditySeconds(entity.getRefreshTokenValiditySeconds()); + + Optional.ofNullable(entity.getResponseTypes()) + .ifPresent(rts -> clientDTO + .setResponseTypes(rts.stream().map(OAuthResponseType::fromResponseType).collect(toSet()))); + + clientDTO.setReuseRefreshToken(entity.isReuseRefreshToken()); + + if (entity.isDynamicallyRegistered()) { + clientDTO.setRegistrationClientUri( + String.format("%s/%s", clientRegistrationBaseUrl, entity.getClientId())); + } + + if (entity.getCodeChallengeMethod() != null) { + clientDTO.setCodeChallengeMethod(entity.getCodeChallengeMethod().getName()); + } + + if (entity.getRequireAuthTime() != null) { + clientDTO.setRequireAuthTime(entity.getRequireAuthTime()); + } else { + clientDTO.setRequireAuthTime(false); + } + + clientDTO.setActive(entity.isActive()); + clientDTO.setStatusChangedOn(entity.getStatusChangedOn()); + clientDTO.setStatusChangedBy(entity.getStatusChangedBy()); + + return clientDTO; + } + + public ClientDetailsEntity entityFromRegistrationRequest(RegisteredClientDTO dto) + throws ParseException { + + ClientDetailsEntity client = new ClientDetailsEntity(); + + client.setClientId(dto.getClientId()); + client.setClientDescription(dto.getClientDescription()); + client.setClientName(dto.getClientName()); + client.setClientSecret(dto.getClientSecret()); + + client.setClientUri(dto.getClientUri()); + + if (!Strings.isNullOrEmpty(dto.getJwksUri())) { + client.setJwksUri(dto.getJwksUri()); + } else if (!Strings.isNullOrEmpty(dto.getJwk())) { + client.setJwks(JWKSet.parse(dto.getJwk())); + } + + client.setPolicyUri(dto.getPolicyUri()); + + client.setRedirectUris(cloneSet(dto.getRedirectUris())); + + client.setScope(cloneSet(dto.getScope())); + + client.setGrantTypes(new HashSet<>()); + + if (!isNull(dto.getGrantTypes())) { + client.setGrantTypes( + dto.getGrantTypes() + .stream() + .map(AuthorizationGrantType::getGrantType) + .collect(toSet())); + } + + if (dto.getScope().contains("offline_access")) { + client.getGrantTypes().add(AuthorizationGrantType.REFRESH_TOKEN.getGrantType()); + } + + if (!isNull(dto.getResponseTypes())) { + client.setResponseTypes( + dto.getResponseTypes().stream().map(OAuthResponseType::getResponseType).collect(toSet())); + } + + client.setContacts(cloneSet(dto.getContacts())); + + if (!isNull(dto.getTokenEndpointAuthMethod())) { + client + .setTokenEndpointAuthMethod(AuthMethod.getByValue(dto.getTokenEndpointAuthMethod().name())); + } + + if (dto.getCodeChallengeMethod() != null) { + PKCEAlgorithm pkceAlgo = PKCEAlgorithm.parse(dto.getCodeChallengeMethod()); + client.setCodeChallengeMethod(pkceAlgo); + } + + // bypasses MitreID default setting to zero inside client's entity + client.setAccessTokenValiditySeconds(clientRegistrationProperties.getClientDefaults().getDefaultAccessTokenValiditySeconds()); + client.setRefreshTokenValiditySeconds(clientRegistrationProperties.getClientDefaults().getDefaultRefreshTokenValiditySeconds()); + client.setIdTokenValiditySeconds(clientRegistrationProperties.getClientDefaults().getDefaultIdTokenValiditySeconds()); + client.setDeviceCodeValiditySeconds(clientRegistrationProperties.getClientDefaults().getDefaultDeviceCodeValiditySeconds()); + + return client; + } + + public RegisteredClientDTO registrationResponseFromClient(ClientDetailsEntity entity) { + RegisteredClientDTO response = registeredClientDtoFromEntity(entity); + response.setRegistrationClientUri( + String.format("%s/%s", clientRegistrationBaseUrl, entity.getClientId())); + + return response; + } + +} \ No newline at end of file diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/ClientService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/ClientService.java index 421439303..2c7accb0d 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/ClientService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/ClientService.java @@ -45,5 +45,7 @@ Optional findClientByClientIdAndAccount(String clientId, ClientDetailsEntity updateClient(ClientDetailsEntity client); + ClientDetailsEntity updateClientStatus(ClientDetailsEntity client, boolean status, String userId); + void deleteClient(ClientDetailsEntity client); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/DefaultClientService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/DefaultClientService.java index 48de8fd4c..1f6383cb0 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/DefaultClientService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/DefaultClientService.java @@ -88,7 +88,7 @@ private Supplier newAccountClient(IamAccount owner, @Override public ClientDetailsEntity linkClientToAccount(ClientDetailsEntity client, IamAccount owner) { IamAccountClient ac = accountClientRepo.findByAccountAndClient(owner, client) - .orElseGet(newAccountClient(owner, client)); + .orElseGet(newAccountClient(owner, client)); return ac.getClient(); } @@ -107,6 +107,13 @@ public ClientDetailsEntity updateClient(ClientDetailsEntity client) { return clientRepo.save(client); } + @Override + public ClientDetailsEntity updateClientStatus(ClientDetailsEntity client, boolean status, String userId) { + client.setActive(status); + client.setStatusChangedBy(userId); + client.setStatusChangedOn(Date.from(clock.instant())); + return clientRepo.save(client); + } @Override public Optional findClientByClientId(String clientId) { @@ -122,7 +129,7 @@ public Optional findClientByClientIdAndAccount(String clien if (maybeClient.isPresent()) { return accountClientRepo.findByAccountAndClientId(account, maybeClient.get().getId()) - .map(IamAccountClient::getClient); + .map(IamAccountClient::getClient); } return Optional.empty(); @@ -144,12 +151,12 @@ private boolean isValidAccessToken(OAuth2AccessTokenEntity a) { private void deleteTokensByClient(ClientDetailsEntity client) { // delete all valid access tokens (exclude registration and resource tokens) tokenService.getAccessTokensForClient(client) - .stream() - .filter(this::isValidAccessToken) - .forEach(at -> tokenService.revokeAccessToken(at)); + .stream() + .filter(this::isValidAccessToken) + .forEach(at -> tokenService.revokeAccessToken(at)); // delete all valid refresh tokens tokenService.getRefreshTokensForClient(client) - .forEach(rt -> tokenService.revokeRefreshToken(rt)); + .forEach(rt -> tokenService.revokeRefreshToken(rt)); } @Override diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/common/client/RegisteredClientDTO.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/common/client/RegisteredClientDTO.java index 2125fa4d5..be2a5fb9f 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/common/client/RegisteredClientDTO.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/common/client/RegisteredClientDTO.java @@ -50,6 +50,7 @@ import it.infn.mw.iam.api.client.registration.validation.ValidTokenEndpointAuthMethod; import it.infn.mw.iam.api.common.ClientViews; + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) @JsonInclude(JsonInclude.Include.NON_EMPTY) @ValidGrantType(groups = {OnClientCreation.class, OnClientUpdate.class, @@ -83,7 +84,8 @@ public class RegisteredClientDTO { private String clientSecret; @Size(min = 4, max = 256, - groups = {OnDynamicClientRegistration.class, OnClientCreation.class, OnClientUpdate.class, OnDynamicClientUpdate.class}, + groups = {OnDynamicClientRegistration.class, OnClientCreation.class, OnClientUpdate.class, + OnDynamicClientUpdate.class}, message = "Invalid length: must be between 4 and 256 characters") @NotBlank(groups = {OnDynamicClientRegistration.class, OnClientCreation.class}, message = "should not be blank") @@ -250,10 +252,22 @@ public class RegisteredClientDTO { ClientViews.DynamicRegistration.class}) @Pattern(regexp = "^$|none|plain|S256", message = "must be either an empty string, none, plain or S256", - groups = {OnClientCreation.class, - OnClientUpdate.class, OnDynamicClientRegistration.class, OnDynamicClientUpdate.class}) + groups = {OnClientCreation.class, OnClientUpdate.class, OnDynamicClientRegistration.class, + OnDynamicClientUpdate.class}) private String codeChallengeMethod; + @JsonView({ClientViews.Limited.class, ClientViews.Full.class, ClientViews.ClientManagement.class, + ClientViews.DynamicRegistration.class}) + private boolean active; + + @JsonView({ClientViews.Limited.class, ClientViews.Full.class, ClientViews.ClientManagement.class, + ClientViews.DynamicRegistration.class}) + private Date statusChangedOn; + + @JsonView({ClientViews.Limited.class, ClientViews.Full.class, ClientViews.ClientManagement.class, + ClientViews.DynamicRegistration.class}) + private String statusChangedBy; + public String getClientId() { return clientId; } @@ -511,4 +525,27 @@ public void setDefaultMaxAge(Integer defaultMaxAge) { this.defaultMaxAge = defaultMaxAge; } -} + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + public Date getStatusChangedOn() { + return statusChangedOn; + } + + public void setStatusChangedOn(Date statusChangedOn) { + this.statusChangedOn = statusChangedOn; + } + + public void setStatusChangedBy(String statusChangedBy) { + this.statusChangedBy = statusChangedBy; + } + + public String getStatusChangedBy() { + return statusChangedBy; + } +} \ No newline at end of file diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/utils/FindUtils.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/utils/FindUtils.java index 5c9b78efa..e45cdd3c6 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/utils/FindUtils.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/utils/FindUtils.java @@ -17,6 +17,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -43,6 +44,9 @@ public static ScimListResponse responseFromPage(Page results, return builder.build(); } - - + public static ScimListResponse responseFromOptional(Optional account, Converter converter) { + ScimListResponseBuilder builder = ScimListResponse.builder(); + account.ifPresent(a -> builder.singleResource(converter.dtoFromEntity(a))); + return builder.build(); + } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/client/ClientStatusChangedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/client/ClientStatusChangedEvent.java new file mode 100644 index 000000000..c48243d50 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/client/ClientStatusChangedEvent.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.audit.events.client; + +import org.mitre.oauth2.model.ClientDetailsEntity; + +public class ClientStatusChangedEvent extends ClientEvent { + + private static final long serialVersionUID = 1L; + + public ClientStatusChangedEvent(Object source, ClientDetailsEntity client, String message) { + super(source, client, message); + } + +} diff --git a/iam-login-service/src/main/webapp/WEB-INF/tags/iamHeader.tag b/iam-login-service/src/main/webapp/WEB-INF/tags/iamHeader.tag index 61b29fce3..076c9dfd6 100644 --- a/iam-login-service/src/main/webapp/WEB-INF/tags/iamHeader.tag +++ b/iam-login-service/src/main/webapp/WEB-INF/tags/iamHeader.tag @@ -43,6 +43,10 @@ rel="stylesheet" href="${resourcesPrefix}/iam/css/iam.css"> + + + @@ -199,7 +200,7 @@ - + diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.resign.component.html b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.resign.component.html new file mode 100644 index 000000000..cacfc1e2e --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.resign.component.html @@ -0,0 +1,25 @@ + + \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.resign.component.js b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.resign.component.js new file mode 100644 index 000000000..e17220e0f --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.resign.component.js @@ -0,0 +1,78 @@ +/* + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +(function () { + 'use strict'; + + function ResignModalController($scope, $uibModalInstance, toaster, AupService, user) { + var self = this; + self.enabled = true; + self.user = user; + + self.cancel = function() { + $uibModalInstance.close('Cancelled'); + }; + + self.submit = function() { + self.error = undefined; + self.enabled = false; + AupService.resignAup() + .then(function(res) { + $uibModalInstance.close('AUP signature re-signed succesfully'); + self.enabled = true; + }, function(res) { + self.error = res.data.error; + self.enabled = true; + toaster.pop({ type: 'error', body: self.error}); + }); + }; + } + + function AupResignController($scope, $uibModal, toaster) { + var self = this; + self.enabled = true; + + self.isMe = function () { + return self.userCtrl.isMe(); + }; + + self.openSignAUPModal = function() { + var modalInstance = $uibModal.open({ + templateUrl: '/resources/iam/apps/dashboard-app/templates/home/resignAup.html', + controller: ResignModalController, + controllerAs: 'resignModalCtrl', + resolve: {user: function() { return self.user; }} + }); + + modalInstance.result.then(function(msg) { + toaster.pop({type: 'success', body: msg}); + }, function () { + console.log('Re-sign AUP modal dismissed at: ' + new Date()); + }); + }; + } + + + angular.module('dashboardApp').component('aupResign', { + templateUrl: '/resources/iam/apps/dashboard-app/components/aup/aup.resign.component.html', + bindings: { + user: '<' + }, + controller: [ + '$rootScope', '$uibModal', 'toaster', 'AupService', AupResignController + ] + }); +})(); \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/client.component.html b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/client.component.html index 84bd1a220..c03f21c5c 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/client.component.html +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/client.component.html @@ -1,90 +1,95 @@ - -
-
-

-    {{$ctrl.clientVal.client_name}} - Create a new client -

- -
- -
-
-
-
- - - Main - - - - - Credentials - - - - - - Scopes - - - - - Grant types - - - - - Tokens - - - - - Crypto - - - - - Other info - - - - - Owners - - - -
-
- - - - -
-
-
-
- - + +
+
+

+    {{$ctrl.clientVal.client_name}} + Create a new client +

+
Suspended + {{$ctrl.clientStatusMessage}} +
+ +
+ +
+
+
+
+ + + Main + + + + + Credentials + + + + + + Scopes + + + + + Grant types + + + + + Tokens + + + + + Crypto + + + + + Other info + + + + + Owners + + + +
+
+ + + + + +
+
+
+
+ +
\ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/client.component.js b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/client.component.js index 468092385..a3f4002bd 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/client.component.js +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/client.component.js @@ -17,7 +17,7 @@ 'use strict'; - function ClientController(ClientsService, toaster, $uibModal, $location) { + function ClientController(ClientsService, FindService, toaster, $uibModal, $location) { var self = this; self.resetVal = resetVal; @@ -25,6 +25,7 @@ self.loadClient = loadClient; self.deleteClient = deleteClient; self.cancel = cancel; + self.getClientStatusMessage = getClientStatusMessage; self.$onInit = function () { if (self.newClient) { @@ -131,6 +132,22 @@ } }); } + + function getClientStatusMessage(){ + FindService.findAccountByUuid(self.clientVal.status_changed_by).then(function(res){ + self.clientStatusMessage = "Suspended by " + res.userName + " on " + getFormatedDate(self.clientVal.status_changed_on); + }).catch(function (res) { + console.debug("Error retrieving user account!", res); + }); + } + + function getFormatedDate(dateToFormat){ + var dateISOString = new Date(dateToFormat).toISOString(); + var ymd = dateISOString.split('T')[0]; + //Remove milliseconds + var time = dateISOString.split('T')[1].slice(0, -5); + return ymd + " " + time; + } } angular @@ -147,7 +164,7 @@ newClient: '<', clientOwners: '<' }, - controller: ['ClientsService', 'toaster', '$uibModal', '$location', ClientController], + controller: ['ClientsService', 'FindService', 'toaster', '$uibModal', '$location', ClientController], controllerAs: '$ctrl' }; } diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/status/client.status.component.html b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/status/client.status.component.html new file mode 100644 index 000000000..f74d56e6d --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/status/client.status.component.html @@ -0,0 +1,34 @@ + + + + + + \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/status/client.status.component.js b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/status/client.status.component.js new file mode 100644 index 000000000..2c8512814 --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/status/client.status.component.js @@ -0,0 +1,116 @@ +/* + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +(function() { + 'use strict'; + + function ClientStatusController(toaster, ModalService, ClientsService) { + var self = this; + + self.$onInit = function() { + self.enabled = true; + }; + + self.handleError = function(error) { + console.error(error); + self.enabled = true; + }; + + self.handleSuccess = function() { + self.enabled = true; + ClientsService.retrieveClient(self.client.client_id).then(function (client) { + console.debug("Loaded client", client); + self.client = client; + self.clientVal = angular.copy(self.client); + if (client.active) { + toaster.pop({ + type: 'success', + body: + `Client '${client.client_name}' has been restored successfully.` + }); + } else { + toaster.pop({ + type: 'success', + body: `Client '${client.client_name}' is now disabled.` + }); + } + }).catch(function (res) { + console.debug("Error retrieving client!", res); + toaster.pop({ + type: 'error', + body: 'Error retrieving client!' + }); + }); + }; + + self.enableClient = function() { + return ClientsService.enableClient(self.client.client_id) + .then(self.handleSuccess) + .catch(self.handleError); + }; + + self.disableClient = function() { + return ClientsService.disableClient(self.client.client_id) + .then(self.handleSuccess) + .catch(self.handleError); + }; + + + self.openDialog = function() { + + var modalOptions = null; + var updateStatusFunc = null; + + if (self.client.active) { + modalOptions = { + closeButtonText: 'Cancel', + actionButtonText: 'Disable client', + headerText: 'Disable ' + self.client.client_name, + bodyText: + `Are you sure you want to disable client '${self.client.client_name}'?` + }; + updateStatusFunc = self.disableClient; + + } else { + modalOptions = { + closeButtonText: 'Cancel', + actionButtonText: 'Restore client', + headerText: 'Restore ' + self.client.client_name, + bodyText: + `Are you sure you want to restore client '${self.client.client_name}'?` + }; + updateStatusFunc = self.enableClient; + } + + self.enable = false; + ModalService.showModal({}, modalOptions) + .then(function() { updateStatusFunc(); }) + .catch(function() { + console.debug("Error updating client status!", res); + }); + }; + } + + angular.module('dashboardApp').component('clientStatus', { + require: {clientCtrl: '^client'}, + bindings: {client: '='}, + templateUrl: + '/resources/iam/apps/dashboard-app/components/clients/client/status/client.status.component.html', + controller: [ + 'toaster', 'ModalService', 'ClientsService', ClientStatusController + ] + }); + +})(); \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/clientslist/clientslist.component.html b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/clientslist/clientslist.component.html index 83cdb6218..fb7426469 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/clientslist/clientslist.component.html +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/clientslist/clientslist.component.html @@ -92,6 +92,10 @@
{{c.client_id}}
+
Suspended + {{$ctrl.clientStatusMessage}} +
diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/clientslist/clientslist.component.js b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/clientslist/clientslist.component.js index bf893ccb2..a22ceb89c 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/clientslist/clientslist.component.js +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/clientslist/clientslist.component.js @@ -16,7 +16,7 @@ (function () { 'use strict'; - function ClientsListController($filter, $uibModal, ClientsService, toaster) { + function ClientsListController($filter, $uibModal, ClientsService, FindService, toaster) { var self = this; self.searchFilter = ''; @@ -27,6 +27,7 @@ self.onChangePage = onChangePage; self.deleteClient = deleteClient; self.clientTrackLastUsed = getClientTrackLastUsed(); + self.getClientStatusMessage = getClientStatusMessage; self.$onInit = function () { console.debug('ClientsListController.self', self); @@ -118,6 +119,22 @@ } }); } + + function getClientStatusMessage(client){ + FindService.findAccountByUuid(client.status_changed_by).then(function(res){ + self.clientStatusMessage = "Suspended by " + res.userName + " on " + getFormatedDate(client.status_changed_on); + }).catch(function (res) { + console.debug("Error retrieving user account!", res); + }); + } + + function getFormatedDate(dateToFormat){ + var dateISOString = new Date(dateToFormat).toISOString(); + var ymd = dateISOString.split('T')[0]; + //Remove milliseconds + var time = dateISOString.split('T')[1].slice(0, -5); + return ymd + " " + time; + } } angular @@ -130,7 +147,7 @@ bindings: { clients: "<" }, - controller: ['$filter', '$uibModal', 'ClientsService', 'toaster', ClientsListController], + controller: ['$filter', '$uibModal', 'ClientsService', 'FindService', 'toaster', ClientsListController], controllerAs: '$ctrl' }; } diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/user/myclients/myclients.component.html b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/user/myclients/myclients.component.html index 7bb427ae7..e2c89b4cf 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/user/myclients/myclients.component.html +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/user/myclients/myclients.component.html @@ -48,19 +48,28 @@

ng-repeat="client in $ctrl.clients.Resources">
- {{client.client_name}} + {{client.client_name}}
{{client.client_id}}
+
Suspended + {{$ctrl.clientStatusMessage}} +
- +
diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/services/aup.service.js b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/services/aup.service.js index e5c2d163c..08f8d6096 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/services/aup.service.js +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/services/aup.service.js @@ -30,7 +30,8 @@ touchAup: touchAup, deleteAup: deleteAup, getAupSignature: getAupSignature, - getAupSignatureForUser: getAupSignatureForUser + getAupSignatureForUser: getAupSignatureForUser, + resignAup: resignAup }; return service; @@ -80,5 +81,15 @@ return $q.reject(res); }); } + + function resignAup() { + return $http.post('/iam/aup/signature/').catch(function(res) { + if (res.status == 404) { + console.info("Account not found"); + return null; + } + return $q.reject(res); + }); + } } })(); \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/services/clients.service.js b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/services/clients.service.js index 2511c7a91..f9eec42d3 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/services/clients.service.js +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/services/clients.service.js @@ -44,7 +44,9 @@ assignClientOwner: assignClientOwner, removeClientOwner: removeClientOwner, newClient: newClient, - getClientList: getClientList + getClientList: getClientList, + enableClient: enableClient, + disableClient: disableClient }; return service; @@ -175,5 +177,21 @@ }; return newClient; } + + function enableClient(clientId){ + return $http.patch(endpoint(clientId) + "/enable").then(function (res) { + return res.data; + }).catch(function (res) { + return $q.reject(res); + }); + } + + function disableClient(clientId){ + return $http.patch(endpoint(clientId) + "/disable").then(function (res) { + return res.data; + }).catch(function (res) { + return $q.reject(res); + }); + } } })(); \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/services/find.service.js b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/services/find.service.js index 68d38f177..017095c8b 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/services/find.service.js +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/services/find.service.js @@ -26,7 +26,8 @@ function FindService($q, $http) { var service = { - findUnsubscribedGroupsForAccount: findUnsubscribedGroupsForAccount + findUnsubscribedGroupsForAccount: findUnsubscribedGroupsForAccount, + findAccountByUuid: findAccountByUuid }; return service; @@ -46,5 +47,15 @@ return $q.reject(error); }); } + + function findAccountByUuid(accountUuid) { + var url = "/iam/account/find/byuuid/" + accountUuid; + return $http.get(url).then(function (result) { + return result.data.Resources[0]; + }).catch(function (error) { + console.error("Error loading account details: ", error); + return $q.reject(error); + }); + } } }()); \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/templates/home/resignAup.html b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/templates/home/resignAup.html new file mode 100644 index 000000000..e3aa4835e --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/templates/home/resignAup.html @@ -0,0 +1,41 @@ + + + diff --git a/iam-login-service/src/main/webapp/resources/iam/css/tooltip.css b/iam-login-service/src/main/webapp/resources/iam/css/tooltip.css new file mode 100644 index 000000000..761d27879 --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/css/tooltip.css @@ -0,0 +1,49 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tool-tip { + position: relative; + display: inline-block; + border-bottom: 1px dotted black; + } + + .tool-tip .tooltiptext { + visibility: hidden; + background-color: black; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px; + position: absolute; + z-index: 1; + top: -2px; + left: 110%; + } + + .tool-tip .tooltiptext::after { + content: ""; + position: absolute; + top: 50%; + right: 100%; + margin-top: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent black transparent transparent; + } + + .tool-tip:hover .tooltiptext { + visibility: visible; + opacity: 1; + } \ No newline at end of file diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/account/find/FindAccountIntegrationTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/account/find/FindAccountIntegrationTests.java index f2c2982c1..1fd5c30fa 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/account/find/FindAccountIntegrationTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/account/find/FindAccountIntegrationTests.java @@ -19,6 +19,7 @@ import static it.infn.mw.iam.api.account.find.FindAccountController.FIND_BY_GROUP_RESOURCE; import static it.infn.mw.iam.api.account.find.FindAccountController.FIND_BY_LABEL_RESOURCE; import static it.infn.mw.iam.api.account.find.FindAccountController.FIND_BY_USERNAME_RESOURCE; +import static it.infn.mw.iam.api.account.find.FindAccountController.FIND_BY_UUID_RESOURCE; import static it.infn.mw.iam.api.account.find.FindAccountController.FIND_NOT_IN_GROUP_RESOURCE; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.containsString; @@ -99,6 +100,7 @@ public void findingRequiresAuthenticatedUser() throws Exception { mvc.perform(get(FIND_BY_USERNAME_RESOURCE).param("username", "test")).andExpect(UNAUTHORIZED); mvc.perform(get(FIND_BY_GROUP_RESOURCE, TEST_001_GROUP_UUID)).andExpect(UNAUTHORIZED); mvc.perform(get(FIND_NOT_IN_GROUP_RESOURCE, TEST_001_GROUP_UUID)).andExpect(UNAUTHORIZED); + mvc.perform(get(FIND_BY_UUID_RESOURCE, TEST_USER_UUID)).andExpect(UNAUTHORIZED); } @@ -290,4 +292,26 @@ public void findNotInGroupWorks() throws Exception { .andExpect(jsonPath("$.Resources[1].id", is("bffc67b7-47fe-410c-a6a0-cf00173a8fbb"))); } + @Test + @WithMockUser(username = "test", roles = "USER") + public void findByUUIDWorks() throws Exception { + + IamAccount testAccount = accountRepo.findByUuid(TEST_USER_UUID) + .orElseThrow(assertionError(EXPECTED_ACCOUNT_NOT_FOUND)); + + mvc.perform(get(FIND_BY_UUID_RESOURCE, testAccount.getUuid())) + .andExpect(OK) + .andExpect(jsonPath("$.Resources[0].id", is(testAccount.getUuid()))) + .andExpect(jsonPath("$.Resources[0].userName", is(testAccount.getUsername()))) + .andExpect(jsonPath("$.Resources[0].active", is(testAccount.isActive()))); + } + + @Test + @WithMockUser(username = "test", roles = "USER") + public void totalResultDoesNotExistForUnknownUUID() throws Exception { + mvc.perform(get(FIND_BY_UUID_RESOURCE, "unknown_uuid")) + .andExpect(OK) + .andExpect(jsonPath("$.totalResults").doesNotExist()) + .andExpect(jsonPath("$.Resources", emptyIterable())); + } } diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/aup/AupSignatureIntegrationTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/aup/AupSignatureIntegrationTests.java index d094be313..bb6285b63 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/aup/AupSignatureIntegrationTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/aup/AupSignatureIntegrationTests.java @@ -276,4 +276,5 @@ public void updateAupWithRightScope() throws Exception { .andExpect(jsonPath("$.error", equalTo("Account not found for id: 1234"))); } + } diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/ClientManagementAPIIntegrationTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/ClientManagementAPIIntegrationTests.java index dac6229bf..2b40da102 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/ClientManagementAPIIntegrationTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/client/ClientManagementAPIIntegrationTests.java @@ -24,6 +24,7 @@ import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -292,4 +293,36 @@ public void negativeRefreshTokenLifetimesSetToInfinite() throws Exception { .andExpect(BAD_REQUEST) .andExpect(jsonPath("$.error", containsString("must be greater than or equal to 0"))); } + + @Test + @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) + public void setClientEnableWorks() throws Exception { + + mvc.perform(get(ClientManagementAPIController.ENDPOINT + "/client")) + .andExpect(OK) + .andExpect(jsonPath("$.active").value(true)); + + mvc.perform(patch(ClientManagementAPIController.ENDPOINT + "/{clientId}/enable", "client") + ).andExpect(OK); + + mvc.perform(get(ClientManagementAPIController.ENDPOINT + "/client")) + .andExpect(OK) + .andExpect(jsonPath("$.active").value(true)); + } + + @Test + @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) + public void setClientDisableWorks() throws Exception { + + mvc.perform(get(ClientManagementAPIController.ENDPOINT + "/client")) + .andExpect(OK) + .andExpect(jsonPath("$.active").value(true)); + + mvc.perform(patch(ClientManagementAPIController.ENDPOINT + "/{clientId}/disable", "client") + ).andExpect(OK); + + mvc.perform(get(ClientManagementAPIController.ENDPOINT + "/client")) + .andExpect(OK) + .andExpect(jsonPath("$.active").value(false)); + } } diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/client/registration/ClientRegistrationAPIControllerTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/client/registration/ClientRegistrationAPIControllerTests.java index eb8ed59cd..69bcdcef5 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/client/registration/ClientRegistrationAPIControllerTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/client/registration/ClientRegistrationAPIControllerTests.java @@ -105,6 +105,7 @@ public void registerClientWithNullValuesAndCheckDefaultValues() .andExpect(jsonPath("$.grant_types", hasItem("urn:ietf:params:oauth:grant-type:device_code"))) .andExpect(jsonPath("$.token_endpoint_auth_method", is("client_secret_basic"))) .andExpect(jsonPath("$.dynamically_registered", is(true))) + .andExpect(jsonPath("$.active", is(true))) .andExpect(jsonPath("$.registration_access_token").exists()) .andExpect(jsonPath("$.allow_introspection").doesNotExist()) .andExpect(jsonPath("$.access_token_validity_seconds").doesNotExist()) diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/service/client/ClientManagementServiceTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/service/client/ClientManagementServiceTests.java index 44b8cb335..1711e80da 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/service/client/ClientManagementServiceTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/service/client/ClientManagementServiceTests.java @@ -28,11 +28,15 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; import java.text.ParseException; +import java.time.Clock; +import java.util.Date; import javax.validation.ConstraintViolationException; @@ -85,6 +89,9 @@ public class ClientManagementServiceTests { @Autowired private IamAccountRepository accountRepo; + @Autowired + private Clock clock; + private Authentication userAuth; private OAuth2Authentication ratAuth; @@ -401,4 +408,13 @@ public void testCodeChallengeValidation() { } } + @Test + public void testClientStatusChange() { + managementService.updateClientStatus("client", false, "userUUID"); + RegisteredClientDTO client = managementService.retrieveClientByClientId("client").get(); + + assertFalse(client.isActive()); + assertTrue(client.getStatusChangedOn().equals(Date.from(clock.instant()))); + assertEquals("userUUID", client.getStatusChangedBy()); + } } diff --git a/iam-persistence/src/main/resources/db/migration/h2/V103__add_active_and_status_changed_on_to_client_details.sql b/iam-persistence/src/main/resources/db/migration/h2/V103__add_active_and_status_changed_on_to_client_details.sql new file mode 100644 index 000000000..85bc0c60f --- /dev/null +++ b/iam-persistence/src/main/resources/db/migration/h2/V103__add_active_and_status_changed_on_to_client_details.sql @@ -0,0 +1,2 @@ + +ALTER TABLE client_details ADD COLUMN (active BOOLEAN, status_changed_on TIMESTAMP, status_changed_by VARCHAR(36)); \ No newline at end of file diff --git a/iam-persistence/src/main/resources/db/migration/mysql/V103__add_active_and_status_changed_on_to_client_details.sql b/iam-persistence/src/main/resources/db/migration/mysql/V103__add_active_and_status_changed_on_to_client_details.sql new file mode 100644 index 000000000..9aa3f3596 --- /dev/null +++ b/iam-persistence/src/main/resources/db/migration/mysql/V103__add_active_and_status_changed_on_to_client_details.sql @@ -0,0 +1,3 @@ +ALTER TABLE client_details ADD COLUMN (active BOOLEAN, + status_changed_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + status_changed_by VARCHAR(36)); \ No newline at end of file diff --git a/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql b/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql index 0a2303505..8db60917a 100644 --- a/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql +++ b/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql @@ -1,32 +1,32 @@ INSERT INTO client_details (id, client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection, - token_endpoint_auth_method, require_auth_time, device_code_validity_seconds, created_at) VALUES - (1, 'client', 'secret', 'Test Client', false, null, 3600, 600, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP()), - (2, 'tasks-app', 'secret', 'Tasks App', false, null, 0, 0, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP()), - (3, 'post-client', 'secret', 'Post client', false, null, 3600,600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP()), - (4, 'client-cred', 'secret', 'Client credentials', false, null, 3600, 600, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP()), - (5, 'password-grant', 'secret', 'Password grant client', false, null, 3600, 600, true, 'SECRET_BASIC',true, null, CURRENT_TIMESTAMP()), - (6, 'scim-client-ro', 'secret', 'SCIM client (read-only)', false, null, 3600, 600, true, 'SECRET_POST',false, 600, CURRENT_TIMESTAMP()), - (7, 'scim-client-rw', 'secret', 'SCIM client (read-write)', false, null, 3600, 600, true, 'SECRET_POST',false, 600, CURRENT_TIMESTAMP()), - (8, 'token-exchange-actor', 'secret', 'Token Exchange grant client actor', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP()), - (9, 'token-exchange-subject', 'secret', 'Token Exchange grant client subject', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP()), - (10, 'registration-client', 'secret', 'Registration service test client', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP()), - (11, 'token-lookup-client', 'secret', 'Token lookup client', false, null, 3600, 600, true, 'SECRET_BASIC', false, null, CURRENT_TIMESTAMP()), - (12, 'device-code-client', 'secret', 'Device code client', false, null, 3600, 600, true, 'SECRET_BASIC', false, 600, CURRENT_TIMESTAMP()), - (13, 'implicit-flow-client', null, 'Implicit Flow client', false, null, 3600, 600, false, null, false, 600, CURRENT_TIMESTAMP()), - (14, 'public-dc-client', null, 'Public Device Code client', false, null, 3600, 600, false, null, false, 600, CURRENT_TIMESTAMP()), - (17, 'admin-client-ro', 'secret', 'Admin client (read-only)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP()), - (18, 'admin-client-rw', 'secret', 'Admin client (read-write)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP()), - (19, 'public-client', null, 'Public client', false, 3600, 3600, 600, true, 'NONE', false, null, CURRENT_TIMESTAMP()); + token_endpoint_auth_method, require_auth_time, device_code_validity_seconds, created_at, active) VALUES + (1, 'client', 'secret', 'Test Client', false, null, 3600, 600, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), + (2, 'tasks-app', 'secret', 'Tasks App', false, null, 0, 0, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), + (3, 'post-client', 'secret', 'Post client', false, null, 3600,600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (4, 'client-cred', 'secret', 'Client credentials', false, null, 3600, 600, true, 'SECRET_BASIC',false, null, CURRENT_TIMESTAMP(), true), + (5, 'password-grant', 'secret', 'Password grant client', false, null, 3600, 600, true, 'SECRET_BASIC',true, null, CURRENT_TIMESTAMP(), true), + (6, 'scim-client-ro', 'secret', 'SCIM client (read-only)', false, null, 3600, 600, true, 'SECRET_POST',false, 600, CURRENT_TIMESTAMP(), true), + (7, 'scim-client-rw', 'secret', 'SCIM client (read-write)', false, null, 3600, 600, true, 'SECRET_POST',false, 600, CURRENT_TIMESTAMP(), true), + (8, 'token-exchange-actor', 'secret', 'Token Exchange grant client actor', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (9, 'token-exchange-subject', 'secret', 'Token Exchange grant client subject', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (10, 'registration-client', 'secret', 'Registration service test client', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (11, 'token-lookup-client', 'secret', 'Token lookup client', false, null, 3600, 600, true, 'SECRET_BASIC', false, null, CURRENT_TIMESTAMP(), true), + (12, 'device-code-client', 'secret', 'Device code client', false, null, 3600, 600, true, 'SECRET_BASIC', false, 600, CURRENT_TIMESTAMP(), true), + (13, 'implicit-flow-client', null, 'Implicit Flow client', false, null, 3600, 600, false, null, false, 600, CURRENT_TIMESTAMP(), true), + (14, 'public-dc-client', null, 'Public Device Code client', false, null, 3600, 600, false, null, false, 600, CURRENT_TIMESTAMP(), true), + (17, 'admin-client-ro', 'secret', 'Admin client (read-only)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (18, 'admin-client-rw', 'secret', 'Admin client (read-write)', false, null, 3600, 600, true, 'SECRET_POST',false, null, CURRENT_TIMESTAMP(), true), + (19, 'public-client', null, 'Public client', false, 3600, 3600, 600, true, 'NONE', false, null, CURRENT_TIMESTAMP(), true); INSERT INTO client_details (id, client_id, client_secret, client_name, dynamically_registered, refresh_token_validity_seconds, access_token_validity_seconds, id_token_validity_seconds, allow_introspection, - token_endpoint_auth_method, require_auth_time, token_endpoint_auth_signing_alg, jwks) VALUES + token_endpoint_auth_method, require_auth_time, token_endpoint_auth_signing_alg, jwks, active) VALUES (15, 'jwt-auth-client_secret_jwt', 'c8e9eed0-e6e4-4a66-b16e-6f37096356a7', 'JWT Bearer Auth Client (client_secret_jwt)', - false, null, 3600, 600, true, 'SECRET_JWT', false, 'HS256', null), + false, null, 3600, 600, true, 'SECRET_JWT', false, 'HS256', null, true), (16, 'jwt-auth-private_key_jwt', 'secret', 'JWT Bearer Auth Client (private_key_jwt)', false, null, 3600, 600, true,'PRIVATE_KEY', false, 'RS256', - '{"keys":[{"kty":"RSA","e":"AQAB","kid":"rsa1","n":"1y1CP181zqPNPlV1JDM7Xv0QnGswhSTHe8_XPZHxDTJkykpk_1BmgA3ovP62QRE2ORgsv5oSBI_Z_RaOc4Zx2FonjEJF2oBHtBjsAiF-pxGkM5ZPjFNgFTGp1yUUBjFDcEeIGCwPEyYSt93sQIP_0DRbViMUnpyn3xgM_a1dO5brEWR2n1Uqff1yA5NXfLS03qpl2dpH4HFY5-Zs4bvtJykpAOhoHuIQbz-hmxb9MZ3uTAwsx2HiyEJtz-suyTBHO3BM2o8UcCeyfa34ShPB8i86-sf78fOk2KeRIW1Bju3ANmdV3sxL0j29cesxKCZ06u2ZiGR3Srbft8EdLPzf-w"}]}'); + '{"keys":[{"kty":"RSA","e":"AQAB","kid":"rsa1","n":"1y1CP181zqPNPlV1JDM7Xv0QnGswhSTHe8_XPZHxDTJkykpk_1BmgA3ovP62QRE2ORgsv5oSBI_Z_RaOc4Zx2FonjEJF2oBHtBjsAiF-pxGkM5ZPjFNgFTGp1yUUBjFDcEeIGCwPEyYSt93sQIP_0DRbViMUnpyn3xgM_a1dO5brEWR2n1Uqff1yA5NXfLS03qpl2dpH4HFY5-Zs4bvtJykpAOhoHuIQbz-hmxb9MZ3uTAwsx2HiyEJtz-suyTBHO3BM2o8UcCeyfa34ShPB8i86-sf78fOk2KeRIW1Bju3ANmdV3sxL0j29cesxKCZ06u2ZiGR3Srbft8EdLPzf-w"}]}', true); INSERT INTO client_scope (owner_id, scope) VALUES (1, 'openid'), diff --git a/pom.xml b/pom.xml index 79fcb5a7d..c0b7765ff 100644 --- a/pom.xml +++ b/pom.xml @@ -1,424 +1,428 @@ - - 4.0.0 - - - org.springframework.boot - spring-boot-starter-parent - - 2.6.15 - - - - - it.infn.mw.iam-parent - iam-parent - 1.9.0 - pom - - INDIGO Identity and Access Manager (IAM) - Parent POM - - - iam-common - iam-persistence - iam-voms-aa - iam-login-service - iam-test-client - - - - - cnaf-releases - CNAF releases - https://repo.cloud.cnaf.infn.it/repository/cnaf-releases/ - - - - cnaf-snapshots - CNAF snapshots - https://repo.cloud.cnaf.infn.it/repository/cnaf-snapshots/ - - - - - ${project.version}-${git.commit.id.abbrev} - - UTF-8 - UTF-8 - - 17 - - 1.16.2 - - 1.3.6.cnaf-20240207 - 2.5.2.RELEASE - - 3.3.2 - 1.0.10.RELEASE - - - 2.6.15 - - 1.6.1 - 2.5.6 - 1.0.20 - 0.19.8 - 1.3.11 - 4.7.0 - 3.6.0 - 3.4.1 - 1.13.2 - - 4.4.0 - 2.9.0 - - 7.15.0 - - 3.0 - 0.8.7 - 3.1.4 - - - 1.0 - 2.3.2 - 2.3.2 - - 1.58 - - @ - - -Xmx2500m - iam-persistence/**/*,iam-test-client/**/*,iam-test-protected-resource/**/*,iam-common/** - - - - - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - - org.testcontainers - junit-jupiter - ${testcontainers.version} - test - - - - org.testcontainers - mysql - ${testcontainers.version} - runtime - - - - org.testcontainers - mariadb - ${testcontainers.version} - runtime - - - - org.springframework.security.oauth - spring-security-oauth2 - ${spring-security-oauth2.version} - - - - org.webjars - angularjs - ${angularjs.version} - - - - org.webjars.npm - angular-ui-bootstrap - ${angular-ui-bootstrap.version} - - - - org.webjars - angular-ui-router - ${angular-ui-router.version} - - - - org.webjars - angular-sanitize - ${angular-sanitize.version} - - - - org.webjars - angular-ui-select - ${angular-ui-select.version} - - - - org.webjars - jquery - ${jquery.version} - - - - org.webjars - jquery-ui - ${jquery-ui.version} - - - - org.webjars - bootstrap - ${bootstrap.version} - - - - org.webjars - font-awesome - ${font-awesome.version} - - - - org.italiangrid - voms-api-java - ${voms.version} - - - - org.italiangrid - voms-clients - ${voms.version} - - - - - org.mitre - openid-connect-common - ${mitreid.version} - - - - org.mitre - openid-connect-server - ${mitreid.version} - - - - org.mitre - openid-connect-client - ${mitreid.version} - - - - - - org.bouncycastle - bcpkix-jdk15on - ${bouncycastle.version} - - - - org.bouncycastle - bcprov-jdk15on - ${bouncycastle.version} - - - - com.jayway.jsonpath - json-path - ${json-path.version} - - - - io.rest-assured - rest-assured - ${rest-assured.version} - - - - org.springframework.security.extensions - spring-security-saml2-core - ${spring-security-saml.version} - - - - org.flywaydb - flyway-core - ${flyway.version} - - - - - javax.annotation - jsr250-api - ${jsr250-api.version} - - - - jakarta.xml.bind - jakarta.xml.bind-api - ${jakarta.xml.bind-api.version} - - - - org.glassfish.jaxb - jaxb-runtime - ${jaxb-runtime.version} - - - - - - - - - infn-cnaf - https://repo.cloud.cnaf.infn.it/repository/maven-public/ - - true - - - - - - - - - com.mycila - license-maven-plugin - ${license-maven-plugin.version} - - - com.google.cloud.tools - jib-maven-plugin - ${jib-maven-plugin.version} - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - - - enforce-maven - - enforce - - - - - 17 - - - 3.6.0 - - - - - - - - org.apache.maven.plugins - maven-source-plugin - - - attach-sources - - jar-no-fork - - - - - - - org.apache.maven.plugins - maven-eclipse-plugin - 2.9 - - false - true - true - - - - - - org.apache.maven.plugins - maven-war-plugin - - false - - - true - true - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - alphabetical - false - - **/*Tests.java - - - **/Abstract*.java - - - file:/dev/./urandom - true - - ${jvm.test.args} - - - - - pl.project13.maven - git-commit-id-plugin - - - - revision - - - - - false - false - yyyy-MM-dd'T'HH:mm:ssZ - true - ${project.build.outputDirectory}/git.properties - - - - - org.jacoco - jacoco-maven-plugin - ${jacoco-plugin.version} - - - - prepare-agent - - - - report - prepare-package - - report - - - - - - - + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + + 2.6.15 + + + + + it.infn.mw.iam-parent + iam-parent + 1.9.0 + pom + + INDIGO Identity and Access Manager (IAM) - Parent POM + + + iam-common + iam-persistence + iam-voms-aa + iam-login-service + iam-test-client + + + + + cnaf-releases + CNAF releases + https://repo.cloud.cnaf.infn.it/repository/cnaf-releases/ + + + + cnaf-snapshots + CNAF snapshots + https://repo.cloud.cnaf.infn.it/repository/cnaf-snapshots/ + + + + + ${project.version}-${git.commit.id.abbrev} + + UTF-8 + UTF-8 + + 17 + + 1.16.2 + + 1.3.6.cnaf-20240414 + 2.5.2.RELEASE + + 3.3.2 + 1.0.10.RELEASE + + + 2.6.15 + + 1.6.1 + 2.5.6 + 1.0.20 + 0.19.8 + 1.3.11 + 4.7.0 + 3.6.0 + 3.4.1 + 1.13.2 + + 4.4.0 + 2.9.0 + + 7.15.0 + + 3.0 + 0.8.7 + 3.1.4 + + + 1.0 + 2.3.2 + 2.3.2 + + 1.58 + + @ + + -Xmx2500m + + iam-persistence/**/*,iam-test-client/**/*,iam-test-protected-resource/**/*,iam-common/** + + + + + + + org.testcontainers + testcontainers + ${testcontainers.version} + test + + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + + + org.testcontainers + mysql + ${testcontainers.version} + runtime + + + + org.testcontainers + mariadb + ${testcontainers.version} + runtime + + + + org.springframework.security.oauth + spring-security-oauth2 + ${spring-security-oauth2.version} + + + + org.webjars + angularjs + ${angularjs.version} + + + + org.webjars.npm + angular-ui-bootstrap + ${angular-ui-bootstrap.version} + + + + org.webjars + angular-ui-router + ${angular-ui-router.version} + + + + org.webjars + angular-sanitize + ${angular-sanitize.version} + + + + org.webjars + angular-ui-select + ${angular-ui-select.version} + + + + org.webjars + jquery + ${jquery.version} + + + + org.webjars + jquery-ui + ${jquery-ui.version} + + + + org.webjars + bootstrap + ${bootstrap.version} + + + + org.webjars + font-awesome + ${font-awesome.version} + + + + org.italiangrid + voms-api-java + ${voms.version} + + + + org.italiangrid + voms-clients + ${voms.version} + + + + + org.mitre + openid-connect-common + ${mitreid.version} + + + + org.mitre + openid-connect-server + ${mitreid.version} + + + + org.mitre + openid-connect-client + ${mitreid.version} + + + + + + org.bouncycastle + bcpkix-jdk15on + ${bouncycastle.version} + + + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + + + + com.jayway.jsonpath + json-path + ${json-path.version} + + + + io.rest-assured + rest-assured + ${rest-assured.version} + + + + org.springframework.security.extensions + spring-security-saml2-core + ${spring-security-saml.version} + + + + org.flywaydb + flyway-core + ${flyway.version} + + + + + javax.annotation + jsr250-api + ${jsr250-api.version} + + + + jakarta.xml.bind + jakarta.xml.bind-api + ${jakarta.xml.bind-api.version} + + + + org.glassfish.jaxb + jaxb-runtime + ${jaxb-runtime.version} + + + + + + + + + infn-cnaf + https://repo.cloud.cnaf.infn.it/repository/maven-public/ + + true + + + + + + + + + com.mycila + license-maven-plugin + ${license-maven-plugin.version} + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-maven + + enforce + + + + + 17 + + + 3.6.0 + + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.9 + + false + true + true + + + + + + org.apache.maven.plugins + maven-war-plugin + + false + + + true + true + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + alphabetical + false + + **/*Tests.java + + + **/Abstract*.java + + + file:/dev/./urandom + true + + ${jvm.test.args} + + + + + pl.project13.maven + git-commit-id-plugin + + + + revision + + + + + false + false + yyyy-MM-dd'T'HH:mm:ssZ + true + + ${project.build.outputDirectory}/git.properties + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-plugin.version} + + + + prepare-agent + + + + report + prepare-package + + report + + + + + + + \ No newline at end of file