0
# Exception Handling
1
2
The CDAP Security SPI provides a structured exception hierarchy with HTTP status codes for proper error handling in web contexts and authorization scenarios.
3
4
## Exception Hierarchy
5
6
All security-related exceptions implement `HttpErrorStatusProvider` to provide appropriate HTTP status codes for web applications.
7
8
### UnauthorizedException
9
10
Runtime exception thrown when a Principal is not authorized to perform an Action on an EntityId.
11
12
```java { .api }
13
class UnauthorizedException extends RuntimeException implements HttpErrorStatusProvider {
14
/**
15
* Create exception for single action authorization failure.
16
*/
17
UnauthorizedException(Principal principal, Action action, EntityId entityId);
18
19
/**
20
* Create exception for multiple actions authorization failure.
21
*/
22
UnauthorizedException(Principal principal, Set<Action> actions, EntityId entityId);
23
24
/**
25
* Create exception with cause for multiple actions authorization failure.
26
*/
27
UnauthorizedException(Principal principal, Set<Action> actions, EntityId entityId, Throwable ex);
28
29
/**
30
* Create exception for entity access denial.
31
*/
32
UnauthorizedException(Principal principal, EntityId entityId);
33
34
/**
35
* Create exception for conditional authorization failure.
36
*/
37
UnauthorizedException(Principal principal, Set<Action> actions, EntityId entityId, boolean needHaveAll);
38
39
/**
40
* Create exception with custom message.
41
*/
42
UnauthorizedException(String message);
43
44
/**
45
* Get HTTP status code.
46
*
47
* @return HTTP_FORBIDDEN (403)
48
*/
49
int getStatusCode();
50
}
51
```
52
53
### AlreadyExistsException
54
55
Checked exception thrown when attempting to create a Role or entity that already exists.
56
57
```java { .api }
58
class AlreadyExistsException extends Exception implements HttpErrorStatusProvider {
59
/**
60
* Create exception for role that already exists.
61
*/
62
AlreadyExistsException(Role role);
63
64
/**
65
* Create exception with custom message.
66
*/
67
AlreadyExistsException(String message);
68
69
/**
70
* Get HTTP status code.
71
*
72
* @return HTTP_CONFLICT (409)
73
*/
74
int getStatusCode();
75
}
76
```
77
78
### BadRequestException
79
80
Checked exception thrown for invalid input scenarios.
81
82
```java { .api }
83
class BadRequestException extends Exception implements HttpErrorStatusProvider {
84
/**
85
* Create exception for role-related bad request.
86
*/
87
BadRequestException(Role role);
88
89
/**
90
* Create exception with custom message.
91
*/
92
BadRequestException(String message);
93
94
/**
95
* Get HTTP status code.
96
*
97
* @return HTTP_BAD_REQUEST (400)
98
*/
99
int getStatusCode();
100
}
101
```
102
103
### NotFoundException
104
105
Checked exception thrown when attempting to access unknown entities or roles.
106
107
```java { .api }
108
class NotFoundException extends Exception implements HttpErrorStatusProvider {
109
/**
110
* Create exception for role not found.
111
*/
112
NotFoundException(Role role);
113
114
/**
115
* Create exception with custom message.
116
*/
117
NotFoundException(String message);
118
119
/**
120
* Get HTTP status code.
121
*
122
* @return HTTP_NOT_FOUND (404)
123
*/
124
int getStatusCode();
125
}
126
```
127
128
## Usage Examples
129
130
### Authorization Enforcement
131
132
```java
133
public class MyAuthorizer extends AbstractAuthorizer {
134
135
@Override
136
public void enforce(EntityId entity, Principal principal, Set<Action> actions)
137
throws Exception {
138
139
// Check if user exists
140
if (!userExists(principal)) {
141
throw new UnauthorizedException("Principal '" + principal.getName() + "' does not exist");
142
}
143
144
// Check each required action
145
Set<Action> missingActions = new HashSet<>();
146
for (Action action : actions) {
147
if (!hasPermission(entity, principal, action)) {
148
missingActions.add(action);
149
}
150
}
151
152
// Throw exception if any actions are not permitted
153
if (!missingActions.isEmpty()) {
154
throw new UnauthorizedException(principal, missingActions, entity);
155
}
156
}
157
158
@Override
159
public void createRole(Role role) throws Exception {
160
if (roleExists(role)) {
161
throw new AlreadyExistsException(role);
162
}
163
164
// Create the role
165
createRoleInBackend(role);
166
}
167
168
@Override
169
public void dropRole(Role role) throws Exception {
170
if (!roleExists(role)) {
171
throw new NotFoundException(role);
172
}
173
174
// Delete the role
175
deleteRoleFromBackend(role);
176
}
177
}
178
```
179
180
### Web Controller Error Handling
181
182
```java
183
@RestController
184
public class AuthorizationController {
185
186
@PostMapping("/roles")
187
public ResponseEntity<String> createRole(@RequestBody Role role) {
188
try {
189
authorizer.createRole(role);
190
return ResponseEntity.ok("Role created successfully");
191
192
} catch (AlreadyExistsException e) {
193
return ResponseEntity.status(e.getStatusCode())
194
.body("Role already exists: " + e.getMessage());
195
196
} catch (BadRequestException e) {
197
return ResponseEntity.status(e.getStatusCode())
198
.body("Invalid role data: " + e.getMessage());
199
}
200
}
201
202
@DeleteMapping("/roles/{roleName}")
203
public ResponseEntity<String> deleteRole(@PathVariable String roleName) {
204
try {
205
Role role = new Role(roleName);
206
authorizer.dropRole(role);
207
return ResponseEntity.ok("Role deleted successfully");
208
209
} catch (NotFoundException e) {
210
return ResponseEntity.status(e.getStatusCode())
211
.body("Role not found: " + e.getMessage());
212
213
} catch (Exception e) {
214
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
215
.body("Error deleting role: " + e.getMessage());
216
}
217
}
218
219
@PostMapping("/authorize")
220
public ResponseEntity<String> checkAuthorization(
221
@RequestBody AuthorizationRequest request) {
222
223
try {
224
authorizer.enforce(request.getEntity(), request.getPrincipal(),
225
request.getActions());
226
return ResponseEntity.ok("Authorized");
227
228
} catch (UnauthorizedException e) {
229
return ResponseEntity.status(e.getStatusCode())
230
.body("Access denied: " + e.getMessage());
231
232
} catch (Exception e) {
233
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
234
.body("Authorization check failed: " + e.getMessage());
235
}
236
}
237
}
238
```
239
240
### Exception Handling with Detailed Messages
241
242
```java
243
public class DetailedAuthorizationEnforcer implements AuthorizationEnforcer {
244
245
@Override
246
public void enforce(EntityId entity, Principal principal, Set<Action> actions)
247
throws Exception {
248
249
List<String> errors = new ArrayList<>();
250
251
// Validate principal
252
if (principal == null || principal.getName() == null) {
253
throw new UnauthorizedException("Principal cannot be null or have null name");
254
}
255
256
// Check if principal exists
257
if (!principalExists(principal)) {
258
throw new UnauthorizedException("Principal '" + principal.getName() +
259
"' of type " + principal.getType() + " does not exist");
260
}
261
262
// Check each action individually for detailed error reporting
263
for (Action action : actions) {
264
if (!hasPermission(entity, principal, action)) {
265
errors.add("Missing permission for action: " + action.name());
266
}
267
}
268
269
if (!errors.isEmpty()) {
270
String detailedMessage = String.format(
271
"Principal '%s' is not authorized to perform actions %s on entity '%s'. Reasons: %s",
272
principal.getName(), actions, entity, String.join("; ", errors));
273
274
throw new UnauthorizedException(detailedMessage);
275
}
276
}
277
278
@Override
279
public Set<? extends EntityId> isVisible(Set<? extends EntityId> entityIds,
280
Principal principal) throws Exception {
281
282
if (principal == null) {
283
throw new UnauthorizedException("Cannot check visibility with null principal");
284
}
285
286
return entityIds.stream()
287
.filter(entity -> {
288
try {
289
// Check if principal has any permission on this entity
290
return hasAnyPermission(entity, principal);
291
} catch (Exception e) {
292
// Log error but don't fail the entire operation
293
logger.warn("Error checking visibility for entity {} and principal {}: {}",
294
entity, principal, e.getMessage());
295
return false;
296
}
297
})
298
.collect(Collectors.toSet());
299
}
300
}
301
```
302
303
### Global Exception Handler
304
305
```java
306
@ControllerAdvice
307
public class SecurityExceptionHandler {
308
309
@ExceptionHandler(UnauthorizedException.class)
310
public ResponseEntity<ErrorResponse> handleUnauthorized(UnauthorizedException e) {
311
ErrorResponse error = new ErrorResponse(
312
"UNAUTHORIZED",
313
e.getMessage(),
314
System.currentTimeMillis()
315
);
316
317
return ResponseEntity.status(e.getStatusCode()).body(error);
318
}
319
320
@ExceptionHandler(AlreadyExistsException.class)
321
public ResponseEntity<ErrorResponse> handleAlreadyExists(AlreadyExistsException e) {
322
ErrorResponse error = new ErrorResponse(
323
"ALREADY_EXISTS",
324
e.getMessage(),
325
System.currentTimeMillis()
326
);
327
328
return ResponseEntity.status(e.getStatusCode()).body(error);
329
}
330
331
@ExceptionHandler(NotFoundException.class)
332
public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException e) {
333
ErrorResponse error = new ErrorResponse(
334
"NOT_FOUND",
335
e.getMessage(),
336
System.currentTimeMillis()
337
);
338
339
return ResponseEntity.status(e.getStatusCode()).body(error);
340
}
341
342
@ExceptionHandler(BadRequestException.class)
343
public ResponseEntity<ErrorResponse> handleBadRequest(BadRequestException e) {
344
ErrorResponse error = new ErrorResponse(
345
"BAD_REQUEST",
346
e.getMessage(),
347
System.currentTimeMillis()
348
);
349
350
return ResponseEntity.status(e.getStatusCode()).body(error);
351
}
352
353
public static class ErrorResponse {
354
private final String error;
355
private final String message;
356
private final long timestamp;
357
358
public ErrorResponse(String error, String message, long timestamp) {
359
this.error = error;
360
this.message = message;
361
this.timestamp = timestamp;
362
}
363
364
// Getters...
365
}
366
}
367
```
368
369
### Audit Logging with Exceptions
370
371
```java
372
public class AuditingAuthorizer extends AbstractAuthorizer {
373
private final AuditLogger auditLogger;
374
375
@Override
376
public void enforce(EntityId entity, Principal principal, Set<Action> actions)
377
throws Exception {
378
379
long startTime = System.currentTimeMillis();
380
381
try {
382
// Perform authorization check
383
performAuthorizationCheck(entity, principal, actions);
384
385
// Log successful authorization
386
auditLogger.logAuthorizationSuccess(principal, entity, actions,
387
System.currentTimeMillis() - startTime);
388
389
} catch (UnauthorizedException e) {
390
// Log authorization failure with details
391
auditLogger.logAuthorizationFailure(principal, entity, actions,
392
e.getMessage(), System.currentTimeMillis() - startTime);
393
394
// Re-throw the exception
395
throw e;
396
397
} catch (Exception e) {
398
// Log unexpected errors
399
auditLogger.logAuthorizationError(principal, entity, actions,
400
e.getClass().getSimpleName() + ": " + e.getMessage(),
401
System.currentTimeMillis() - startTime);
402
403
// Re-throw the exception
404
throw e;
405
}
406
}
407
}