Service Provider Interface for CDAP's security and authorization framework enabling pluggable authorization mechanisms.
—
The CDAP Security SPI provides a structured exception hierarchy with HTTP status codes for proper error handling in web contexts and authorization scenarios.
All security-related exceptions implement HttpErrorStatusProvider to provide appropriate HTTP status codes for web applications.
Runtime exception thrown when a Principal is not authorized to perform an Action on an EntityId.
class UnauthorizedException extends RuntimeException implements HttpErrorStatusProvider {
/**
* Create exception for single action authorization failure.
*/
UnauthorizedException(Principal principal, Action action, EntityId entityId);
/**
* Create exception for multiple actions authorization failure.
*/
UnauthorizedException(Principal principal, Set<Action> actions, EntityId entityId);
/**
* Create exception with cause for multiple actions authorization failure.
*/
UnauthorizedException(Principal principal, Set<Action> actions, EntityId entityId, Throwable ex);
/**
* Create exception for entity access denial.
*/
UnauthorizedException(Principal principal, EntityId entityId);
/**
* Create exception for conditional authorization failure.
*/
UnauthorizedException(Principal principal, Set<Action> actions, EntityId entityId, boolean needHaveAll);
/**
* Create exception with custom message.
*/
UnauthorizedException(String message);
/**
* Get HTTP status code.
*
* @return HTTP_FORBIDDEN (403)
*/
int getStatusCode();
}Checked exception thrown when attempting to create a Role or entity that already exists.
class AlreadyExistsException extends Exception implements HttpErrorStatusProvider {
/**
* Create exception for role that already exists.
*/
AlreadyExistsException(Role role);
/**
* Create exception with custom message.
*/
AlreadyExistsException(String message);
/**
* Get HTTP status code.
*
* @return HTTP_CONFLICT (409)
*/
int getStatusCode();
}Checked exception thrown for invalid input scenarios.
class BadRequestException extends Exception implements HttpErrorStatusProvider {
/**
* Create exception for role-related bad request.
*/
BadRequestException(Role role);
/**
* Create exception with custom message.
*/
BadRequestException(String message);
/**
* Get HTTP status code.
*
* @return HTTP_BAD_REQUEST (400)
*/
int getStatusCode();
}Checked exception thrown when attempting to access unknown entities or roles.
class NotFoundException extends Exception implements HttpErrorStatusProvider {
/**
* Create exception for role not found.
*/
NotFoundException(Role role);
/**
* Create exception with custom message.
*/
NotFoundException(String message);
/**
* Get HTTP status code.
*
* @return HTTP_NOT_FOUND (404)
*/
int getStatusCode();
}public class MyAuthorizer extends AbstractAuthorizer {
@Override
public void enforce(EntityId entity, Principal principal, Set<Action> actions)
throws Exception {
// Check if user exists
if (!userExists(principal)) {
throw new UnauthorizedException("Principal '" + principal.getName() + "' does not exist");
}
// Check each required action
Set<Action> missingActions = new HashSet<>();
for (Action action : actions) {
if (!hasPermission(entity, principal, action)) {
missingActions.add(action);
}
}
// Throw exception if any actions are not permitted
if (!missingActions.isEmpty()) {
throw new UnauthorizedException(principal, missingActions, entity);
}
}
@Override
public void createRole(Role role) throws Exception {
if (roleExists(role)) {
throw new AlreadyExistsException(role);
}
// Create the role
createRoleInBackend(role);
}
@Override
public void dropRole(Role role) throws Exception {
if (!roleExists(role)) {
throw new NotFoundException(role);
}
// Delete the role
deleteRoleFromBackend(role);
}
}@RestController
public class AuthorizationController {
@PostMapping("/roles")
public ResponseEntity<String> createRole(@RequestBody Role role) {
try {
authorizer.createRole(role);
return ResponseEntity.ok("Role created successfully");
} catch (AlreadyExistsException e) {
return ResponseEntity.status(e.getStatusCode())
.body("Role already exists: " + e.getMessage());
} catch (BadRequestException e) {
return ResponseEntity.status(e.getStatusCode())
.body("Invalid role data: " + e.getMessage());
}
}
@DeleteMapping("/roles/{roleName}")
public ResponseEntity<String> deleteRole(@PathVariable String roleName) {
try {
Role role = new Role(roleName);
authorizer.dropRole(role);
return ResponseEntity.ok("Role deleted successfully");
} catch (NotFoundException e) {
return ResponseEntity.status(e.getStatusCode())
.body("Role not found: " + e.getMessage());
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Error deleting role: " + e.getMessage());
}
}
@PostMapping("/authorize")
public ResponseEntity<String> checkAuthorization(
@RequestBody AuthorizationRequest request) {
try {
authorizer.enforce(request.getEntity(), request.getPrincipal(),
request.getActions());
return ResponseEntity.ok("Authorized");
} catch (UnauthorizedException e) {
return ResponseEntity.status(e.getStatusCode())
.body("Access denied: " + e.getMessage());
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Authorization check failed: " + e.getMessage());
}
}
}public class DetailedAuthorizationEnforcer implements AuthorizationEnforcer {
@Override
public void enforce(EntityId entity, Principal principal, Set<Action> actions)
throws Exception {
List<String> errors = new ArrayList<>();
// Validate principal
if (principal == null || principal.getName() == null) {
throw new UnauthorizedException("Principal cannot be null or have null name");
}
// Check if principal exists
if (!principalExists(principal)) {
throw new UnauthorizedException("Principal '" + principal.getName() +
"' of type " + principal.getType() + " does not exist");
}
// Check each action individually for detailed error reporting
for (Action action : actions) {
if (!hasPermission(entity, principal, action)) {
errors.add("Missing permission for action: " + action.name());
}
}
if (!errors.isEmpty()) {
String detailedMessage = String.format(
"Principal '%s' is not authorized to perform actions %s on entity '%s'. Reasons: %s",
principal.getName(), actions, entity, String.join("; ", errors));
throw new UnauthorizedException(detailedMessage);
}
}
@Override
public Set<? extends EntityId> isVisible(Set<? extends EntityId> entityIds,
Principal principal) throws Exception {
if (principal == null) {
throw new UnauthorizedException("Cannot check visibility with null principal");
}
return entityIds.stream()
.filter(entity -> {
try {
// Check if principal has any permission on this entity
return hasAnyPermission(entity, principal);
} catch (Exception e) {
// Log error but don't fail the entire operation
logger.warn("Error checking visibility for entity {} and principal {}: {}",
entity, principal, e.getMessage());
return false;
}
})
.collect(Collectors.toSet());
}
}@ControllerAdvice
public class SecurityExceptionHandler {
@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<ErrorResponse> handleUnauthorized(UnauthorizedException e) {
ErrorResponse error = new ErrorResponse(
"UNAUTHORIZED",
e.getMessage(),
System.currentTimeMillis()
);
return ResponseEntity.status(e.getStatusCode()).body(error);
}
@ExceptionHandler(AlreadyExistsException.class)
public ResponseEntity<ErrorResponse> handleAlreadyExists(AlreadyExistsException e) {
ErrorResponse error = new ErrorResponse(
"ALREADY_EXISTS",
e.getMessage(),
System.currentTimeMillis()
);
return ResponseEntity.status(e.getStatusCode()).body(error);
}
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException e) {
ErrorResponse error = new ErrorResponse(
"NOT_FOUND",
e.getMessage(),
System.currentTimeMillis()
);
return ResponseEntity.status(e.getStatusCode()).body(error);
}
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ErrorResponse> handleBadRequest(BadRequestException e) {
ErrorResponse error = new ErrorResponse(
"BAD_REQUEST",
e.getMessage(),
System.currentTimeMillis()
);
return ResponseEntity.status(e.getStatusCode()).body(error);
}
public static class ErrorResponse {
private final String error;
private final String message;
private final long timestamp;
public ErrorResponse(String error, String message, long timestamp) {
this.error = error;
this.message = message;
this.timestamp = timestamp;
}
// Getters...
}
}public class AuditingAuthorizer extends AbstractAuthorizer {
private final AuditLogger auditLogger;
@Override
public void enforce(EntityId entity, Principal principal, Set<Action> actions)
throws Exception {
long startTime = System.currentTimeMillis();
try {
// Perform authorization check
performAuthorizationCheck(entity, principal, actions);
// Log successful authorization
auditLogger.logAuthorizationSuccess(principal, entity, actions,
System.currentTimeMillis() - startTime);
} catch (UnauthorizedException e) {
// Log authorization failure with details
auditLogger.logAuthorizationFailure(principal, entity, actions,
e.getMessage(), System.currentTimeMillis() - startTime);
// Re-throw the exception
throw e;
} catch (Exception e) {
// Log unexpected errors
auditLogger.logAuthorizationError(principal, entity, actions,
e.getClass().getSimpleName() + ": " + e.getMessage(),
System.currentTimeMillis() - startTime);
// Re-throw the exception
throw e;
}
}
}Install with Tessl CLI
npx tessl i tessl/maven-co-cask-cdap--cdap-security-spi