From 9451f09724f89a99e5287581940e9408e0a41ce9 Mon Sep 17 00:00:00 2001 From: Manoj Garai Date: Mon, 26 Feb 2024 16:21:27 +0000 Subject: [PATCH 01/12] Add ability to disable client * Add API and service methods to change client status * Add new columns for client status * Set status as active for new client * Add suspended label next to client name * Add column status_changed_by to client_details * Make client suspension details available to client owner --- .../find/DefaultFindAccountService.java | 5 +- .../account/find/FindAccountController.java | 43 +- .../api/account/find/FindAccountService.java | 6 +- .../ClientManagementAPIController.java | 13 + .../service/ClientManagementService.java | 2 + .../DefaultClientManagementService.java | 13 + .../DefaultClientRegistrationService.java | 2 + .../api/client/service/ClientConverter.java | 484 +++++----- .../iam/api/client/service/ClientService.java | 2 + .../client/service/DefaultClientService.java | 19 +- .../common/client/RegisteredClientDTO.java | 111 ++- .../client/ClientStatusChangedEvent.java | 28 + .../main/webapp/WEB-INF/tags/iamHeader.tag | 4 + .../webapp/WEB-INF/views/iam/dashboard.jsp | 2 +- .../clients/client/client.component.html | 183 ++-- .../clients/client/client.component.js | 21 +- .../status/client.status.component.html | 34 + .../client/status/client.status.component.js | 117 +++ .../clientslist/clientslist.component.html | 4 + .../clientslist/clientslist.component.js | 21 +- .../user/myclients/myclients.component.html | 13 +- .../user/myclients/myclients.component.js | 21 +- .../dashboard-app/services/clients.service.js | 11 +- .../dashboard-app/services/find.service.js | 13 +- .../main/webapp/resources/iam/css/tooltip.css | 49 + .../find/FindAccountIntegrationTests.java | 30 + .../ClientManagementAPIIntegrationTests.java | 18 + .../ClientRegistrationAPIControllerTests.java | 1 + .../client/ClientManagementServiceTests.java | 16 + ...nd_status_changed_on_to_client_details.sql | 2 + ...nd_status_changed_on_to_client_details.sql | 3 + .../db/migration/test/V100000___test_data.sql | 42 +- pom.xml | 850 +++++++++--------- 33 files changed, 1350 insertions(+), 833 deletions(-) create mode 100644 iam-login-service/src/main/java/it/infn/mw/iam/audit/events/client/ClientStatusChangedEvent.java create mode 100644 iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/status/client.status.component.html create mode 100644 iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/client/status/client.status.component.js create mode 100644 iam-login-service/src/main/webapp/resources/iam/css/tooltip.css create mode 100644 iam-persistence/src/main/resources/db/migration/h2/V103__add_active_and_status_changed_on_to_client_details.sql create mode 100644 iam-persistence/src/main/resources/db/migration/mysql/V103__add_active_and_status_changed_on_to_client_details.sql 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..db350a3c5 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 @@ -25,7 +25,6 @@ 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; @@ -143,4 +142,8 @@ public ScimListResponse findAccountByGroupUuidWithFilter(String groupU Page results = repo.findByGroupUuidWithFilter(group.getUuid(), filter, pageable); return responseFromPage(results, converter, pageable); } + + public Optional findAccountByUuid(String uuid) { + return repo.findByUuid(uuid); + } } 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..0e27d2b1e 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 @@ -20,20 +20,27 @@ import static java.util.Objects.isNull; import static org.springframework.web.bind.annotation.RequestMethod.GET; +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; 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 com.nimbusds.jose.shaded.json.JSONObject; + +import it.infn.mw.iam.api.common.ErrorDTO; import it.infn.mw.iam.api.common.ListResponseDTO; +import it.infn.mw.iam.api.common.error.NoSuchAccountError; import it.infn.mw.iam.api.common.form.PaginatedRequestWithFilterForm; import it.infn.mw.iam.api.scim.model.ScimConstants; import it.infn.mw.iam.api.scim.model.ScimUser; +import it.infn.mw.iam.persistence.model.IamAccount; @RestController @PreAuthorize("#iam.hasScope('iam:admin.read') or #iam.hasDashboardRole('ROLE_ADMIN')") @@ -44,6 +51,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 +129,31 @@ public ListResponseDTO findNotInGroup(@PathVariable String groupUuid, } } + @GetMapping(FIND_BY_UUID_RESOURCE) + @PreAuthorize("#iam.hasScope('iam:admin.read') or #iam.hasDashboardRole('ROLE_ADMIN') or hasRole('USER')") + public JSONObject findByUuid(@PathVariable String accountUuid) { + Optional iamAccount = service.findAccountByUuid(accountUuid); + if(iamAccount.isPresent()){ + return getIamAccountJson(iamAccount.get()); + } else{ + throw NoSuchAccountError.forUuid(accountUuid); + } + } + + @ResponseStatus(value = HttpStatus.NOT_FOUND) + @ExceptionHandler(NoSuchAccountError.class) + @ResponseBody + @PreAuthorize("#iam.hasScope('iam:admin.read') or #iam.hasDashboardRole('ROLE_ADMIN') or hasRole('USER')") + public ErrorDTO accountNotFoundError(HttpServletRequest req, Exception ex) { + return ErrorDTO.fromString(ex.getMessage()); + } + + private JSONObject getIamAccountJson(IamAccount iamAccount) { + JSONObject iamAccountJson = new JSONObject(); + iamAccountJson.put("id", iamAccount.getId()); + iamAccountJson.put("accountUuid", iamAccount.getUuid()); + iamAccountJson.put("username", iamAccount.getUsername()); + iamAccountJson.put("active", iamAccount.isActive()); + return iamAccountJson; + } } 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..aca1e154b 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 @@ -15,10 +15,13 @@ */ package it.infn.mw.iam.api.account.find; +import java.util.Optional; + import org.springframework.data.domain.Pageable; import it.infn.mw.iam.api.scim.model.ScimListResponse; import it.infn.mw.iam.api.scim.model.ScimUser; +import it.infn.mw.iam.persistence.model.IamAccount; public interface FindAccountService { @@ -45,5 +48,6 @@ ScimListResponse findAccountByGroupUuidWithFilter(String groupUuid, St ScimListResponse findAccountNotInGroupWithFilter(String groupUuid, String filter, Pageable pageable); - + + Optional findAccountByUuid(String uuid); } 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..2c377ef43 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; @@ -43,6 +44,8 @@ import org.springframework.web.bind.annotation.RestController; import com.fasterxml.jackson.annotation.JsonView; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import it.infn.mw.iam.api.client.error.InvalidPaginationRequest; import it.infn.mw.iam.api.client.error.NoSuchClient; @@ -140,6 +143,16 @@ public RegisteredClientDTO updateClient(@PathVariable String clientId, return managementService.updateClient(clientId, client); } + @PatchMapping("/{clientId}/status") + @PreAuthorize("#iam.hasScope('iam:admin.write') or #iam.hasDashboardRole('ROLE_ADMIN')") + public void updateClientStatus(@PathVariable String clientId, + @RequestBody String body) { + JsonObject jsonObject = JsonParser.parseString(body).getAsJsonObject(); + boolean status = jsonObject.get("status").getAsBoolean(); + String userId = jsonObject.get("userId").getAsString(); + managementService.updateClientStatus(clientId, status, userId); + } + @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/service/DefaultClientRegistrationService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/registration/service/DefaultClientRegistrationService.java index 09bc73fb8..6ef380583 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 @@ -330,6 +330,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); @@ -410,6 +411,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); 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..79c267a0f 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 @@ -1,21 +1,21 @@ -/** - * 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. - */ +/** + * 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.common.client; -import java.time.LocalDate; +import java.time.LocalDate; import java.util.Date; import java.util.Set; @@ -28,9 +28,9 @@ import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; -import org.hibernate.validator.constraints.URL; - -import com.fasterxml.jackson.annotation.JsonFormat; +import org.hibernate.validator.constraints.URL; + +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.databind.PropertyNamingStrategies; @@ -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") @@ -161,20 +163,20 @@ public class RegisteredClientDTO { ClientViews.DynamicRegistration.class}) private TokenEndpointAuthenticationMethod tokenEndpointAuthMethod; - @Valid - @Size(max = 512, - groups = {OnDynamicClientRegistration.class, OnDynamicClientUpdate.class, - OnClientCreation.class, OnClientUpdate.class}) - @JsonSerialize(using = CollectionAsStringSerializer.class) - @JsonDeserialize(using = StringAsSetOfStringsDeserializer.class) - @JsonView({ClientViews.Limited.class, ClientViews.Full.class, ClientViews.ClientManagement.class, - ClientViews.DynamicRegistration.class}) - private Set<@NotBlank(groups = {OnDynamicClientRegistration.class, OnDynamicClientUpdate.class, - OnClientCreation.class, OnClientUpdate.class}, - message = "must not include blank strings") @Size(min = 1, max = 2048, - message = "string size must be between 1 and 2048", - groups = {OnDynamicClientRegistration.class, OnDynamicClientUpdate.class, - OnClientCreation.class, OnClientUpdate.class}) String> scope = + @Valid + @Size(max = 512, + groups = {OnDynamicClientRegistration.class, OnDynamicClientUpdate.class, + OnClientCreation.class, OnClientUpdate.class}) + @JsonSerialize(using = CollectionAsStringSerializer.class) + @JsonDeserialize(using = StringAsSetOfStringsDeserializer.class) + @JsonView({ClientViews.Limited.class, ClientViews.Full.class, ClientViews.ClientManagement.class, + ClientViews.DynamicRegistration.class}) + private Set<@NotBlank(groups = {OnDynamicClientRegistration.class, OnDynamicClientUpdate.class, + OnClientCreation.class, OnClientUpdate.class}, + message = "must not include blank strings") @Size(min = 1, max = 2048, + message = "string size must be between 1 and 2048", + groups = {OnDynamicClientRegistration.class, OnDynamicClientUpdate.class, + OnClientCreation.class, OnClientUpdate.class}) String> scope = Sets.newHashSet(); @Min(value = 0, groups = OnClientCreation.class) @@ -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.Full.class, ClientViews.ClientManagement.class, + ClientViews.DynamicRegistration.class}) + private boolean active; + + @JsonView({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/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..2084a890c 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..6cb82457f --- /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,117 @@ +/* + * 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.setClientActiveStatus(self.client.client_id, true, self.loggedUserId) + .then(self.handleSuccess) + .catch(self.handleError); + }; + + self.disableClient = function() { + return ClientsService.setClientActiveStatus(self.client.client_id, false, self.loggedUserId) + .then(self.handleSuccess) + .catch(self.handleError); + }; + + + self.openDialog = function(loggedUserId) { + + var modalOptions = null; + var updateStatusFunc = null; + self.loggedUserId = loggedUserId; + + 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..50ad86daf 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}} +
- +