Skip to content
Snippets Groups Projects
Verified Commit 73c70764 authored by Dominik Frantisek Bucik's avatar Dominik Frantisek Bucik
Browse files

feat: :guitar: Dynreg - delete client

parent c07a689e
No related branches found
No related tags found
1 merge request!379feat: 🎸 Dynreg - delete client
Pipeline #401184 passed
Showing
with 135 additions and 7 deletions
......@@ -44,6 +44,7 @@ import lombok.ToString;
import org.eclipse.persistence.annotations.CascadeOnDelete;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.util.StringUtils;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
......@@ -71,6 +72,7 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static cz.muni.ics.oauth2.model.ClientDetailsEntity.DELETE_BY_CLIENT_ID;
import static cz.muni.ics.oauth2.model.ClientDetailsEntity.PARAM_CLIENT_ID;
import static cz.muni.ics.oauth2.model.ClientDetailsEntity.PARAM_PARENT_CLIENT_ID;
import static cz.muni.ics.oauth2.model.ClientDetailsEntity.PARAM_RESOURCE_ID;
......@@ -107,6 +109,11 @@ import static cz.muni.ics.oauth2.model.ClientDetailsEntity.QUERY_BY_CLIENT_ID;
name = QUERY_ALL_BY_PARENT_CLIENT_ID,
query = "SELECT c FROM ClientDetailsEntity c " +
"WHERE c.parentClientId = :" + PARAM_PARENT_CLIENT_ID),
@NamedQuery(
name = DELETE_BY_CLIENT_ID,
query = "DELETE FROM ClientDetailsEntity c " +
"WHERE c.clientId = :" + PARAM_CLIENT_ID
)
})
public class ClientDetailsEntity implements ClientDetails {
......@@ -114,6 +121,7 @@ public class ClientDetailsEntity implements ClientDetails {
public static final String QUERY_ALL = "ClientDetailsEntity.findAll";
public static final String QUERY_RESOURCE_IDS_BY_CLIENT_ID = "ClientDetailsEntity.getResourceIdsForClientID";
public static final String QUERY_ALL_BY_PARENT_CLIENT_ID = "ClientDetailsEntity.getByParentClientId";
public static final String DELETE_BY_CLIENT_ID = "ClientDetailsEntity.deleteByClientId";
public static final String PARAM_CLIENT_ID = "clientId";
public static final String PARAM_RESOURCE_ID = "resourceId";
......@@ -451,4 +459,13 @@ public class ClientDetailsEntity implements ClientDetails {
this.deviceCodeValiditySeconds = deviceCodeValiditySeconds;
}
public void makeClientDynamicallyRegistered(Long parentClientId) {
if (parentClientId == null) {
throw new IllegalArgumentException("When making client dynamically registered, parentClientId must be provided");
}
this.parentClientId = parentClientId;
this.dynamicallyRegistered = true;
}
}
......@@ -37,4 +37,6 @@ public interface OAuth2ClientRepository {
Collection<String> getResourceIdsForClientID(String resourceId);
void deleteClientByClientId(String cid);
}
......@@ -20,11 +20,13 @@ package cz.muni.ics.oauth2.repository.impl;
import cz.muni.ics.oauth2.model.ClientDetailsEntity;
import cz.muni.ics.oauth2.repository.OAuth2ClientRepository;
import cz.muni.ics.util.jpa.JpaUtil;
import org.eclipse.persistence.exceptions.DatabaseException;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import java.util.Collection;
......@@ -105,6 +107,13 @@ public class JpaOAuth2ClientRepository implements OAuth2ClientRepository {
return query.getResultList();
}
@Override
public void deleteClientByClientId(String clientId) {
Query query = manager.createNamedQuery(ClientDetailsEntity.DELETE_BY_CLIENT_ID);
query.setParameter(ClientDetailsEntity.PARAM_CLIENT_ID, clientId);
query.executeUpdate();
}
private Collection<ClientDetailsEntity> getClientsByParentClientId(Long parentId) {
TypedQuery<ClientDetailsEntity> query = manager.createNamedQuery(
ClientDetailsEntity.QUERY_ALL_BY_PARENT_CLIENT_ID, ClientDetailsEntity.class
......
......@@ -48,4 +48,6 @@ public interface ClientDetailsEntityService extends ClientDetailsService {
boolean checkResourceIdsAreAllowedForClient(String clientId, Set<String> resourceIds);
Set<String> getMismatchedResourceIds(String clientId, Set<String> resourceIds);
void removeClient(ClientDetailsEntity client);
}
......@@ -7,4 +7,6 @@ public interface DynamicClientRegistrationService {
ClientDetailsEntity saveClient(String tokenClientId, DynamicallyRegisteredRequestBody requestedRegistration);
void removeClient(String tokenClientId, String dynregClientId);
}
......@@ -56,10 +56,12 @@ import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Service
@Slf4j
......@@ -460,6 +462,34 @@ public class DefaultOAuth2ClientDetailsEntityService implements ClientDetailsEnt
return copy;
}
@Override
public void removeClient(ClientDetailsEntity dynregClient) {
String clientId = dynregClient.getClientId();
Set<String> clientIdsToRemove = new HashSet<>();
clientIdsToRemove.add(clientId);
boolean found = true;
Collection<ClientDetailsEntity> clients = clientRepository.getAllClients();
while (found) {
Set<String> childClients = clients.stream()
.filter(client -> clientIdsToRemove.contains(client.getParentClientId()))
.map(ClientDetailsEntity::getClientId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
for (String cid: clientIdsToRemove) {
clientRepository.deleteClientByClientId(cid);
}
clientIdsToRemove.clear();
clientIdsToRemove.addAll(childClients);
if (childClients.isEmpty()) {
found = false;
}
}
}
/**
* Utility class to load a sector identifier's set of authorized redirect URIs.
*
......
......@@ -70,6 +70,28 @@ public class DynamicClientRegistrationServiceImpl implements DynamicClientRegist
return clientDetailsEntityService.saveNewClient(client);
}
@Override
public void removeClient(String tokenClientId, String dynregClientId) {
ClientDetailsEntity tokenClient = clientDetailsEntityService.loadClientByClientId(tokenClientId);
if (tokenClient == null) {
throw new InvalidClientException("Client for ID " + tokenClientId + " not found");
}
ClientDetailsEntity dynregClient = clientDetailsEntityService.loadClientByClientId(dynregClientId);
if (dynregClient == null) {
throw new InvalidClientException("Client for ID " + dynregClientId + " not found");
} else if (!dynregClient.isDynamicallyRegistered()) {
throw new InvalidClientException("Client "
+ dynregClientId + " has not been registered dynamically, thus cannot be removed");
}
if (!tokenClient.equals(dynregClient)) {
throw new InvalidClientException("Client "
+ dynregClientId + " has not been registered by the caller client, thus cannot be removed");
}
clientDetailsEntityService.removeClient(dynregClient);
}
private void validateWithParentClient(ClientDetailsEntity tokenClient, DynamicallyRegisteredRequestBody requestedRegistration) {
Set<String> requestedScope = requestedRegistration.getScope();
Set<String> authScope = tokenClient.getScope();
......
......@@ -10,10 +10,14 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.InvalidRequestException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
......@@ -28,6 +32,18 @@ public class DynamicRegistrationEndpoint {
public static final String URL = "register";
public static final String PARAM_CLIENT_ID = "dynregClientId";
public static final String PATH_PARAM_CLIENT_ID = '{' + PARAM_CLIENT_ID + '}';
public static final String GRANT_TYPE = "grant_type";
public static final String CLIENT_ID = "client_id";
public static final String CLIENT_CREDENTIALS = "client_credentials";
public static final String SCOPE_DYNREG = "client_dynamic_registration";
private final DynamicClientRegistrationService dynamicClientRegistrationService;
@Autowired
......@@ -47,15 +63,10 @@ public class DynamicRegistrationEndpoint {
) {
OAuth2Authentication authentication = (OAuth2Authentication) auth;
Set<String> scope = authentication.getOAuth2Request().getScope();
if (scope == null || !scope.contains("client_dynamic_registration")) {
if (scope == null || !scope.contains(SCOPE_DYNREG)) {
throw new InvalidRequestException("The provided token does not contain the required scope");
}
Map<String, String> tokenRequestParameters = ((OAuth2Authentication) auth).getOAuth2Request().getRequestParameters();
if (tokenRequestParameters == null) {
throw new InvalidRequestException("Could not extract token request parameters from the used token");
} else if (!"client_credentials".equals(tokenRequestParameters.getOrDefault("grant_type", ""))) {
throw new InvalidRequestException("Token has to be issued using client_credentials grant type");
}
validateTokenRequestParams(authentication);
String tokenClientId = authentication.getOAuth2Request().getClientId();
ClientDetailsEntity registeredClient = dynamicClientRegistrationService.saveClient(tokenClientId, client);
......@@ -73,4 +84,37 @@ public class DynamicRegistrationEndpoint {
return DynamicRegistrationEndpointView.VIEWNAME;
}
private void validateTokenRequestParams(OAuth2Authentication auth) {
Map<String, String> tokenRequestParameters = auth.getOAuth2Request().getRequestParameters();
if (tokenRequestParameters == null) {
throw new InvalidRequestException("Could not extract token request parameters from the used token");
} else if (!CLIENT_CREDENTIALS.equals(tokenRequestParameters.getOrDefault(GRANT_TYPE, ""))) {
throw new InvalidRequestException("Token has to be issued using client_credentials grant type");
} else if (!StringUtils.hasText(tokenRequestParameters.get(CLIENT_ID))) {
throw new InvalidRequestException("Token has no associated client identifier");
}
}
@DeleteMapping(
value = "/" + URL + "/" + PATH_PARAM_CLIENT_ID
)
public ResponseEntity<Void> deregisterClient(
@PathVariable(PARAM_CLIENT_ID) String dynregClientId,
Authentication auth
) {
OAuth2Authentication authentication = (OAuth2Authentication) auth;
Set<String> scope = authentication.getOAuth2Request().getScope();
if (scope == null || !scope.contains(SCOPE_DYNREG)) {
throw new InvalidRequestException("The provided token does not contain the required scope");
} else if (!StringUtils.hasText(dynregClientId)) {
throw new InvalidRequestException("Path parameter identifying the client to be deleted not specified");
}
validateTokenRequestParams(authentication);
String tokenClientId = authentication.getOAuth2Request().getClientId();
dynamicClientRegistrationService.removeClient(tokenClientId, dynregClientId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment