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"> + + - + 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}} +
- +