Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Administrators can disable a client #747

Merged
merged 13 commits into from
May 24, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -143,4 +142,8 @@ public ScimListResponse<ScimUser> findAccountByGroupUuidWithFilter(String groupU
Page<IamAccount> results = repo.findByGroupUuidWithFilter(group.getUuid(), filter, pageable);
return responseFromPage(results, converter, pageable);
}

public Optional<IamAccount> findAccountByUuid(String uuid) {
return repo.findByUuid(uuid);
}
enricovianello marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -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')")
Expand All @@ -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 =
Expand Down Expand Up @@ -121,4 +129,31 @@ public ListResponseDTO<ScimUser> 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) {
rmiccoli marked this conversation as resolved.
Show resolved Hide resolved
Optional<IamAccount> iamAccount = service.findAccountByUuid(accountUuid);
if(iamAccount.isPresent()){
return getIamAccountJson(iamAccount.get());
} else{
throw NoSuchAccountError.forUuid(accountUuid);
rmiccoli marked this conversation as resolved.
Show resolved Hide resolved
}
}

@ResponseStatus(value = HttpStatus.NOT_FOUND)
@ExceptionHandler(NoSuchAccountError.class)
@ResponseBody
@PreAuthorize("#iam.hasScope('iam:admin.read') or #iam.hasDashboardRole('ROLE_ADMIN') or hasRole('USER')")
rmiccoli marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -45,5 +48,6 @@ ScimListResponse<ScimUser> findAccountByGroupUuidWithFilter(String groupUuid, St

ScimListResponse<ScimUser> findAccountNotInGroupWithFilter(String groupUuid, String filter,
Pageable pageable);


Optional<IamAccount> findAccountByUuid(String uuid);
rmiccoli marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -140,6 +143,16 @@ public RegisteredClientDTO updateClient(@PathVariable String clientId,
return managementService.updateClient(clientId, client);
}

@PatchMapping("/{clientId}/status")
enricovianello marked this conversation as resolved.
Show resolved Hide resolved
@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')")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ RegisteredClientDTO updateClient(@NotBlank String clientId,

void deleteClientByClientId(@NotBlank String clientId);

void updateClientStatus(String clientId, boolean status, String userId);

ListResponseDTO<ScimUser> getClientOwners(@NotBlank String clientId, @NotNull Pageable pageable);

void assignClientOwner(@NotBlank String clientId, @IamAccountId String accountId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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)
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -119,6 +120,12 @@ public ErrorDTO noSuchClient(HttpServletRequest req, Exception ex) {
return ErrorDTO.fromString(ex.getMessage());
}

@ResponseStatus(value = HttpStatus.BAD_REQUEST)
rmiccoli marked this conversation as resolved.
Show resolved Hide resolved
@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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -330,6 +331,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);
Expand Down Expand Up @@ -397,32 +399,36 @@ public RegisteredClientDTO updateClient(String clientId, RegisteredClientDTO req

rmiccoli marked this conversation as resolved.
Show resolved Hide resolved
checkAllowedGrantTypesOnUpdate(request, authentication, oldClient);
cleanupRequestedScopesOnUpdate(request, authentication, oldClient);

ClientDetailsEntity newClient = converter.entityFromRegistrationRequest(request);
newClient.setId(oldClient.getId());
newClient.setClientSecret(oldClient.getClientSecret());
newClient.setAccessTokenValiditySeconds(oldClient.getAccessTokenValiditySeconds());
newClient.setIdTokenValiditySeconds(oldClient.getIdTokenValiditySeconds());
newClient.setRefreshTokenValiditySeconds(oldClient.getRefreshTokenValiditySeconds());
newClient.setDeviceCodeValiditySeconds(oldClient.getDeviceCodeValiditySeconds());
newClient.setDynamicallyRegistered(true);
newClient.setAllowIntrospection(oldClient.isAllowIntrospection());
newClient.setAuthorities(oldClient.getAuthorities());
newClient.setCreatedAt(oldClient.getCreatedAt());
newClient.setReuseRefreshToken(oldClient.isReuseRefreshToken());

ClientDetailsEntity savedClient = clientService.updateClient(newClient);

eventPublisher.publishEvent(new ClientUpdatedEvent(this, savedClient));

RegisteredClientDTO response = converter.registrationResponseFromClient(savedClient);

maybeUpdateRegistrationAccessToken(savedClient, authentication).ifPresent(t -> {
eventPublisher.publishEvent(new ClientRegistrationAccessTokenRotatedEvent(this, savedClient));
response.setRegistrationAccessToken(t);
});

return response;
if(oldClient.isActive()){
ClientDetailsEntity newClient = converter.entityFromRegistrationRequest(request);
newClient.setId(oldClient.getId());
newClient.setClientSecret(oldClient.getClientSecret());
newClient.setAccessTokenValiditySeconds(oldClient.getAccessTokenValiditySeconds());
newClient.setIdTokenValiditySeconds(oldClient.getIdTokenValiditySeconds());
newClient.setRefreshTokenValiditySeconds(oldClient.getRefreshTokenValiditySeconds());
newClient.setDeviceCodeValiditySeconds(oldClient.getDeviceCodeValiditySeconds());
newClient.setDynamicallyRegistered(true);
newClient.setAllowIntrospection(oldClient.isAllowIntrospection());
newClient.setAuthorities(oldClient.getAuthorities());
newClient.setCreatedAt(oldClient.getCreatedAt());
newClient.setReuseRefreshToken(oldClient.isReuseRefreshToken());
newClient.setActive(oldClient.isActive());

ClientDetailsEntity savedClient = clientService.updateClient(newClient);

eventPublisher.publishEvent(new ClientUpdatedEvent(this, savedClient));

RegisteredClientDTO response = converter.registrationResponseFromClient(savedClient);

maybeUpdateRegistrationAccessToken(savedClient, authentication).ifPresent(t -> {
eventPublisher.publishEvent(new ClientRegistrationAccessTokenRotatedEvent(this, savedClient));
response.setRegistrationAccessToken(t);
});
return response;
} else {
throw new ClientSuspended("Client " + clientId + " is suspended!");
}

}

@Override
Expand Down
Loading
Loading