or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

authentication.mdcore-adapters.mdhttp-operations.mdindex.mdjaas-integration.mdkey-rotation.mdpolicy-enforcement.mdtoken-storage.mdutility-operations.md

policy-enforcement.mddocs/

0

# Policy Enforcement

1

2

Policy Enforcement Point (PEP) integration for authorization policy evaluation with Keycloak's authorization services. This module provides the foundation for fine-grained authorization policies and resource protection in applications.

3

4

## Capabilities

5

6

### HttpAuthzRequest

7

8

HTTP authorization request wrapper providing access to request information for policy evaluation.

9

10

```java { .api }

11

/**

12

* HTTP authorization request wrapper providing access to request information for policy evaluation

13

*/

14

public class HttpAuthzRequest implements AuthzRequest {

15

/**

16

* Constructor with OIDC HTTP facade

17

* @param oidcFacade OIDC HTTP facade providing request access

18

*/

19

public HttpAuthzRequest(OIDCHttpFacade oidcFacade);

20

21

/**

22

* Get the relative path of the request

23

* @return Relative path string

24

*/

25

public String getRelativePath();

26

27

/**

28

* Get the HTTP method of the request

29

* @return HTTP method (GET, POST, PUT, DELETE, etc.)

30

*/

31

public String getMethod();

32

33

/**

34

* Get the full URI of the request

35

* @return Complete request URI

36

*/

37

public String getURI();

38

39

/**

40

* Get all headers with the specified name

41

* @param name Header name

42

* @return List of header values

43

*/

44

public List<String> getHeaders(String name);

45

46

/**

47

* Get the first parameter value with the specified name

48

* @param name Parameter name

49

* @return First parameter value or null

50

*/

51

public String getFirstParam(String name);

52

53

/**

54

* Get cookie value by name

55

* @param name Cookie name

56

* @return Cookie value or null

57

*/

58

public String getCookieValue(String name);

59

60

/**

61

* Get remote client address

62

* @return Remote IP address

63

*/

64

public String getRemoteAddr();

65

66

/**

67

* Check if the request is secure (HTTPS)

68

* @return true if secure, false otherwise

69

*/

70

public boolean isSecure();

71

72

/**

73

* Get single header value

74

* @param name Header name

75

* @return Header value or null

76

*/

77

public String getHeader(String name);

78

79

/**

80

* Get request input stream

81

* @param buffered Whether to buffer the stream

82

* @return InputStream for request body

83

*/

84

public InputStream getInputStream(boolean buffered);

85

86

/**

87

* Get the authenticated token principal

88

* @return TokenPrincipal containing token information

89

*/

90

public TokenPrincipal getPrincipal();

91

}

92

```

93

94

**Usage Examples:**

95

96

```java

97

// Create authorization request from HTTP facade

98

HttpAuthzRequest authzRequest = new HttpAuthzRequest(oidcFacade);

99

100

// Extract request information for policy evaluation

101

String method = authzRequest.getMethod();

102

String path = authzRequest.getRelativePath();

103

String uri = authzRequest.getURI();

104

String clientIp = authzRequest.getRemoteAddr();

105

boolean isSecure = authzRequest.isSecure();

106

107

// Access headers and parameters

108

String contentType = authzRequest.getHeader("Content-Type");

109

List<String> acceptHeaders = authzRequest.getHeaders("Accept");

110

String actionParam = authzRequest.getFirstParam("action");

111

112

// Access authentication information

113

TokenPrincipal principal = authzRequest.getPrincipal();

114

if (principal != null) {

115

AccessToken token = principal.getToken();

116

String username = token.getPreferredUsername();

117

Set<String> roles = token.getRealmAccess().getRoles();

118

}

119

120

// Read request body if needed

121

try (InputStream inputStream = authzRequest.getInputStream(true)) {

122

// Process request body for policy evaluation

123

String requestBody = IOUtils.toString(inputStream, StandardCharsets.UTF_8);

124

}

125

```

126

127

### HttpAuthzResponse

128

129

HTTP authorization response wrapper for sending policy enforcement responses.

130

131

```java { .api }

132

/**

133

* HTTP authorization response wrapper for sending policy enforcement responses

134

*/

135

public class HttpAuthzResponse implements AuthzResponse {

136

/**

137

* Constructor with OIDC HTTP facade

138

* @param oidcFacade OIDC HTTP facade providing response access

139

*/

140

public HttpAuthzResponse(OIDCHttpFacade oidcFacade);

141

142

/**

143

* Send error response with status code only

144

* @param statusCode HTTP status code

145

*/

146

public void sendError(int statusCode);

147

148

/**

149

* Send error response with status code and reason

150

* @param code HTTP status code

151

* @param reason Error reason phrase

152

*/

153

public void sendError(int code, String reason);

154

155

/**

156

* Set response header

157

* @param name Header name

158

* @param value Header value

159

*/

160

public void setHeader(String name, String value);

161

}

162

```

163

164

**Usage Examples:**

165

166

```java

167

// Create authorization response

168

HttpAuthzResponse authzResponse = new HttpAuthzResponse(oidcFacade);

169

170

// Send forbidden response for denied requests

171

authzResponse.sendError(403, "Access denied by policy");

172

173

// Send unauthorized response for unauthenticated requests

174

authzResponse.sendError(401, "Authentication required");

175

176

// Add custom headers to response

177

authzResponse.setHeader("X-Policy-Decision", "DENY");

178

authzResponse.setHeader("X-Required-Role", "admin");

179

180

// Send specific error for resource not found

181

authzResponse.sendError(404, "Protected resource not found");

182

```

183

184

## Policy Enforcement Patterns

185

186

### Basic Policy Enforcer Integration

187

188

```java

189

// Policy enforcement filter

190

public class PolicyEnforcementFilter implements Filter {

191

private PolicyEnforcer policyEnforcer;

192

private KeycloakDeployment deployment;

193

194

@Override

195

public void init(FilterConfig filterConfig) throws ServletException {

196

// Initialize deployment and policy enforcer

197

InputStream configStream = getClass().getResourceAsStream("/keycloak.json");

198

deployment = KeycloakDeploymentBuilder.build(configStream);

199

policyEnforcer = deployment.getPolicyEnforcer();

200

}

201

202

@Override

203

public void doFilter(ServletRequest request, ServletResponse response,

204

FilterChain chain) throws IOException, ServletException {

205

HttpServletRequest httpRequest = (HttpServletRequest) request;

206

HttpServletResponse httpResponse = (HttpServletResponse) response;

207

208

// Create OIDC facade

209

OIDCHttpFacade facade = new ServletOIDCHttpFacade(httpRequest, httpResponse);

210

211

// Create authorization request and response

212

HttpAuthzRequest authzRequest = new HttpAuthzRequest(facade);

213

HttpAuthzResponse authzResponse = new HttpAuthzResponse(facade);

214

215

try {

216

// Evaluate policy

217

AuthzResult result = policyEnforcer.enforce(authzRequest, authzResponse);

218

219

if (result.isGranted()) {

220

// Access granted - continue with request

221

chain.doFilter(request, response);

222

} else {

223

// Access denied - response already sent by policy enforcer

224

logger.warn("Access denied for {} {} by user {}",

225

authzRequest.getMethod(),

226

authzRequest.getURI(),

227

authzRequest.getPrincipal() != null ?

228

authzRequest.getPrincipal().getToken().getPreferredUsername() : "anonymous"

229

);

230

}

231

} catch (Exception e) {

232

logger.error("Policy enforcement error", e);

233

authzResponse.sendError(500, "Internal server error");

234

}

235

}

236

}

237

```

238

239

### Custom Policy Evaluation

240

241

```java

242

// Custom policy evaluator

243

public class CustomPolicyEvaluator {

244

private final KeycloakDeployment deployment;

245

246

public CustomPolicyEvaluator(KeycloakDeployment deployment) {

247

this.deployment = deployment;

248

}

249

250

public PolicyDecision evaluate(HttpAuthzRequest request) {

251

TokenPrincipal principal = request.getPrincipal();

252

if (principal == null) {

253

return PolicyDecision.deny("No authentication token");

254

}

255

256

AccessToken token = principal.getToken();

257

String method = request.getMethod();

258

String path = request.getRelativePath();

259

260

// Time-based access control

261

if (isOutsideBusinessHours() && !hasRole(token, "admin")) {

262

return PolicyDecision.deny("Access outside business hours requires admin role");

263

}

264

265

// IP-based access control

266

String clientIp = request.getRemoteAddr();

267

if (!isAllowedIpAddress(clientIp) && isSensitiveResource(path)) {

268

return PolicyDecision.deny("Sensitive resource access not allowed from this IP");

269

}

270

271

// Resource-specific policies

272

if (path.startsWith("/admin")) {

273

if (!hasRole(token, "admin")) {

274

return PolicyDecision.deny("Admin access required");

275

}

276

} else if (path.startsWith("/api/sensitive")) {

277

if (!hasRole(token, "privileged-user") && !isResourceOwner(token, path)) {

278

return PolicyDecision.deny("Insufficient privileges for sensitive API");

279

}

280

}

281

282

// Method-specific policies

283

if ("DELETE".equals(method) && !hasRole(token, "data-manager")) {

284

return PolicyDecision.deny("Delete operations require data-manager role");

285

}

286

287

return PolicyDecision.permit("Access granted");

288

}

289

290

private boolean hasRole(AccessToken token, String role) {

291

return token.getRealmAccess() != null &&

292

token.getRealmAccess().getRoles().contains(role);

293

}

294

295

private boolean isOutsideBusinessHours() {

296

LocalTime now = LocalTime.now();

297

return now.isBefore(LocalTime.of(9, 0)) || now.isAfter(LocalTime.of(17, 0));

298

}

299

300

private boolean isAllowedIpAddress(String ip) {

301

// Check against whitelist of allowed IP ranges

302

return ip.startsWith("10.0.") || ip.startsWith("192.168.") || "127.0.0.1".equals(ip);

303

}

304

305

private boolean isSensitiveResource(String path) {

306

return path.contains("/financial") || path.contains("/personal") || path.contains("/confidential");

307

}

308

309

private boolean isResourceOwner(AccessToken token, String path) {

310

// Extract resource owner from path and compare with token subject

311

String userId = extractUserIdFromPath(path);

312

return token.getSubject().equals(userId);

313

}

314

315

private String extractUserIdFromPath(String path) {

316

// Extract user ID from path like /api/users/{userId}/profile

317

String[] segments = path.split("/");

318

for (int i = 0; i < segments.length - 1; i++) {

319

if ("users".equals(segments[i]) && i + 1 < segments.length) {

320

return segments[i + 1];

321

}

322

}

323

return null;

324

}

325

326

public static class PolicyDecision {

327

private final boolean granted;

328

private final String reason;

329

330

private PolicyDecision(boolean granted, String reason) {

331

this.granted = granted;

332

this.reason = reason;

333

}

334

335

public static PolicyDecision permit(String reason) {

336

return new PolicyDecision(true, reason);

337

}

338

339

public static PolicyDecision deny(String reason) {

340

return new PolicyDecision(false, reason);

341

}

342

343

public boolean isGranted() { return granted; }

344

public String getReason() { return reason; }

345

}

346

}

347

```

348

349

### Resource-Based Authorization

350

351

```java

352

// Resource-based authorization manager

353

public class ResourceAuthorizationManager {

354

private final PolicyEnforcer policyEnforcer;

355

private final Map<String, ResourceDescriptor> resourceMap;

356

357

public ResourceAuthorizationManager(PolicyEnforcer policyEnforcer) {

358

this.policyEnforcer = policyEnforcer;

359

this.resourceMap = buildResourceMap();

360

}

361

362

public boolean isAuthorized(HttpAuthzRequest request, HttpAuthzResponse response) {

363

String path = request.getRelativePath();

364

String method = request.getMethod();

365

366

ResourceDescriptor resource = findResource(path);

367

if (resource != null) {

368

return evaluateResourceAccess(request, response, resource, method);

369

}

370

371

// Default policy for unmapped resources

372

return evaluateDefaultPolicy(request, response);

373

}

374

375

private ResourceDescriptor findResource(String path) {

376

// Find best matching resource pattern

377

return resourceMap.entrySet().stream()

378

.filter(entry -> pathMatches(path, entry.getKey()))

379

.map(Map.Entry::getValue)

380

.findFirst()

381

.orElse(null);

382

}

383

384

private boolean evaluateResourceAccess(HttpAuthzRequest request, HttpAuthzResponse response,

385

ResourceDescriptor resource, String method) {

386

TokenPrincipal principal = request.getPrincipal();

387

if (principal == null) {

388

response.sendError(401, "Authentication required");

389

return false;

390

}

391

392

AccessToken token = principal.getToken();

393

394

// Check required scopes for the method

395

Set<String> requiredScopes = resource.getRequiredScopes(method);

396

if (!hasRequiredScopes(token, requiredScopes)) {

397

response.sendError(403, "Insufficient scopes for " + method + " on " + resource.getName());

398

return false;

399

}

400

401

// Check resource-specific policies

402

if (resource.hasCustomPolicy()) {

403

PolicyDecision decision = resource.evaluateCustomPolicy(request);

404

if (!decision.isGranted()) {

405

response.sendError(403, decision.getReason());

406

return false;

407

}

408

}

409

410

return true;

411

}

412

413

private boolean hasRequiredScopes(AccessToken token, Set<String> requiredScopes) {

414

Set<String> tokenScopes = extractScopes(token);

415

return tokenScopes.containsAll(requiredScopes);

416

}

417

418

private Set<String> extractScopes(AccessToken token) {

419

String scopeClaim = (String) token.getOtherClaims().get("scope");

420

if (scopeClaim != null) {

421

return Set.of(scopeClaim.split(" "));

422

}

423

return Collections.emptySet();

424

}

425

426

private Map<String, ResourceDescriptor> buildResourceMap() {

427

Map<String, ResourceDescriptor> map = new HashMap<>();

428

429

// Define resources and their access requirements

430

map.put("/api/users/**", ResourceDescriptor.builder()

431

.name("User Management")

432

.scope("GET", "read:users")

433

.scope("POST", "create:users")

434

.scope("PUT", "update:users")

435

.scope("DELETE", "delete:users")

436

.customPolicy((request) -> {

437

// Custom policy: users can only access their own data

438

String path = request.getRelativePath();

439

String tokenSubject = request.getPrincipal().getToken().getSubject();

440

return path.contains("/" + tokenSubject + "/") ||

441

hasRole(request.getPrincipal().getToken(), "admin");

442

})

443

.build());

444

445

map.put("/api/documents/**", ResourceDescriptor.builder()

446

.name("Document Management")

447

.scope("GET", "read:documents")

448

.scope("POST", "create:documents")

449

.scope("PUT", "update:documents")

450

.scope("DELETE", "delete:documents")

451

.customPolicy((request) -> {

452

// Check document ownership or shared access

453

return checkDocumentAccess(request);

454

})

455

.build());

456

457

return map;

458

}

459

460

private boolean pathMatches(String path, String pattern) {

461

// Simple pattern matching (could use more sophisticated matching)

462

if (pattern.endsWith("/**")) {

463

String prefix = pattern.substring(0, pattern.length() - 3);

464

return path.startsWith(prefix);

465

}

466

return path.equals(pattern);

467

}

468

469

// Resource descriptor class

470

public static class ResourceDescriptor {

471

private final String name;

472

private final Map<String, Set<String>> methodScopes;

473

private final Function<HttpAuthzRequest, Boolean> customPolicy;

474

475

// Builder pattern implementation...

476

477

public Set<String> getRequiredScopes(String method) {

478

return methodScopes.getOrDefault(method.toUpperCase(), Collections.emptySet());

479

}

480

481

public boolean hasCustomPolicy() {

482

return customPolicy != null;

483

}

484

485

public PolicyDecision evaluateCustomPolicy(HttpAuthzRequest request) {

486

if (customPolicy != null) {

487

boolean allowed = customPolicy.apply(request);

488

return allowed ?

489

PolicyDecision.permit("Custom policy allows access") :

490

PolicyDecision.deny("Custom policy denies access");

491

}

492

return PolicyDecision.permit("No custom policy");

493

}

494

}

495

}

496

```

497

498

### Claim-Based Authorization

499

500

```java

501

// Claim-based authorization processor

502

public class ClaimBasedAuthorizationProcessor {

503

504

public AuthorizationDecision evaluate(HttpAuthzRequest request) {

505

TokenPrincipal principal = request.getPrincipal();

506

if (principal == null) {

507

return AuthorizationDecision.deny("No authentication token");

508

}

509

510

AccessToken token = principal.getToken();

511

Map<String, Object> claims = token.getOtherClaims();

512

513

// Department-based access control

514

String department = (String) claims.get("department");

515

String resource = request.getRelativePath();

516

517

if (resource.startsWith("/hr/") && !"HR".equals(department)) {

518

return AuthorizationDecision.deny("HR resources require HR department");

519

}

520

521

if (resource.startsWith("/finance/") && !"Finance".equals(department)) {

522

return AuthorizationDecision.deny("Finance resources require Finance department");

523

}

524

525

// Location-based access control

526

String userLocation = (String) claims.get("office_location");

527

String clientIp = request.getRemoteAddr();

528

529

if (!isLocationAllowed(userLocation, clientIp, resource)) {

530

return AuthorizationDecision.deny("Resource not accessible from current location");

531

}

532

533

// Time-based access control with user-specific rules

534

Integer maxHours = (Integer) claims.get("max_access_hours");

535

if (maxHours != null && isOutsideAllowedHours(maxHours)) {

536

return AuthorizationDecision.deny("Access outside allowed hours for user");

537

}

538

539

// Data classification access control

540

String clearanceLevel = (String) claims.get("clearance_level");

541

String resourceClassification = extractResourceClassification(resource);

542

543

if (!hasSufficientClearance(clearanceLevel, resourceClassification)) {

544

return AuthorizationDecision.deny("Insufficient clearance for resource classification");

545

}

546

547

return AuthorizationDecision.permit("All claim-based policies satisfied");

548

}

549

550

private boolean isLocationAllowed(String userLocation, String clientIp, String resource) {

551

// Implement location-based access logic

552

if ("REMOTE".equals(userLocation)) {

553

// Remote users have limited access

554

return !resource.contains("/sensitive/");

555

}

556

return true; // Office users have full access

557

}

558

559

private boolean isOutsideAllowedHours(int maxHours) {

560

int currentHour = LocalTime.now().getHour();

561

return currentHour >= maxHours;

562

}

563

564

private String extractResourceClassification(String resource) {

565

if (resource.contains("/classified/")) return "SECRET";

566

if (resource.contains("/confidential/")) return "CONFIDENTIAL";

567

if (resource.contains("/internal/")) return "INTERNAL";

568

return "PUBLIC";

569

}

570

571

private boolean hasSufficientClearance(String userClearance, String resourceClassification) {

572

Map<String, Integer> clearanceLevels = Map.of(

573

"PUBLIC", 0,

574

"INTERNAL", 1,

575

"CONFIDENTIAL", 2,

576

"SECRET", 3

577

);

578

579

int userLevel = clearanceLevels.getOrDefault(userClearance, 0);

580

int requiredLevel = clearanceLevels.getOrDefault(resourceClassification, 0);

581

582

return userLevel >= requiredLevel;

583

}

584

585

public static class AuthorizationDecision {

586

private final boolean granted;

587

private final String reason;

588

589

private AuthorizationDecision(boolean granted, String reason) {

590

this.granted = granted;

591

this.reason = reason;

592

}

593

594

public static AuthorizationDecision permit(String reason) {

595

return new AuthorizationDecision(true, reason);

596

}

597

598

public static AuthorizationDecision deny(String reason) {

599

return new AuthorizationDecision(false, reason);

600

}

601

602

public boolean isGranted() { return granted; }

603

public String getReason() { return reason; }

604

}

605

}

606

```