Skip to content

Commit

Permalink
Policy validator configurable per endpoint in config (#9248)
Browse files Browse the repository at this point in the history
Policy validator configurable per endpoint in config

Signed-off-by: David Kral <[email protected]>
  • Loading branch information
Verdent authored Sep 19, 2024
1 parent 4f40082 commit c91a232
Show file tree
Hide file tree
Showing 19 changed files with 790 additions and 182 deletions.
12 changes: 12 additions & 0 deletions docs/src/main/asciidoc/includes/security/providers/abac.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,15 @@ security:
----
include::{sourcedir}/includes/security/providers/AbacSnippets.java[tag=snippet_4, indent=0]
----
[source,yaml]
.Configuration example for `JAX-RS` over the configuration
----
server:
features:
security:
endpoints:
- path: "/somePath"
config:
abac.policy-validator.statement: "${env.time.year >= 2017}"
----
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2023 Oracle and/or its affiliates.
* Copyright (c) 2018, 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -36,7 +36,7 @@ public Principal getUserPrincipal() {

@Override
public boolean isUserInRole(String role) {
return securityContext.isUserInRole(role, methodSecurity.getAuthorizer());
return securityContext.isUserInRole(role, methodSecurity.authorizer());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.List;
import java.util.Map;

import io.helidon.common.config.Config;
import io.helidon.security.AuditEvent;
import io.helidon.security.SecurityLevel;
import io.helidon.security.annotations.Audited;
Expand Down Expand Up @@ -97,7 +98,21 @@ SecurityDefinition copyMe() {
return result;
}

public void add(Authenticated atn) {
void fromConfig(Config config) {
config.get("authorize").as(Boolean.class).ifPresent(this::requiresAuthorization);
config.get("authorizer").as(String.class).ifPresent(this::authorizer);
config.get("authorization-explicit").as(Boolean.class).ifPresent(this::atzExplicit);
config.get("authenticate").as(Boolean.class).ifPresent(this::requiresAuthentication);
config.get("authenticator").as(String.class).ifPresent(this::authenticator);
config.get("authentication-optional").as(Boolean.class).ifPresent(this::authenticationOptional);
config.get("audit").as(Boolean.class).ifPresent(this::audited);
config.get("audit-event-type").as(String.class).ifPresent(this::auditEventType);
config.get("audit-message-format").as(String.class).ifPresent(this::auditMessageFormat);
config.get("audit-ok-severity").as(AuditEvent.AuditSeverity.class).ifPresent(this::auditOkSeverity);
config.get("audit-error-severity").as(AuditEvent.AuditSeverity.class).ifPresent(this::auditErrorSeverity);
}

void add(Authenticated atn) {
if (null == atn) {
return;
}
Expand All @@ -106,7 +121,7 @@ public void add(Authenticated atn) {
this.authenticator = "".equals(atn.provider()) ? null : atn.provider();
}

public void add(Authorized atz) {
void add(Authorized atz) {
if (null == atz) {
return;
}
Expand All @@ -130,7 +145,7 @@ void requiresAuthentication(boolean atn) {
this.requiresAuthentication = atn;
}

void setRequiresAuthorization(boolean atz) {
void requiresAuthorization(boolean atz) {
this.requiresAuthorization = atz;
}

Expand All @@ -154,10 +169,18 @@ boolean authenticationOptional() {
return authnOptional;
}

void authenticationOptional(boolean authnOptional) {
this.authnOptional = authnOptional;
}

boolean failOnFailureIfOptional() {
return failOnFailureIfOptional;
}

void failOnFailureIfOptional(boolean failOnFailureIfOptional) {
this.failOnFailureIfOptional = failOnFailureIfOptional;
}

boolean requiresAuthorization() {
if (null != requiresAuthorization) {
return requiresAuthorization;
Expand All @@ -171,47 +194,79 @@ boolean requiresAuthorization() {
return (count != 0) || authorizeByDefault;
}

public boolean isAtzExplicit() {
boolean atzExplicit() {
return atzExplicit;
}

String getAuthenticator() {
void atzExplicit(boolean atzExplicit) {
this.atzExplicit = atzExplicit;
}

String authenticator() {
return authenticator;
}

String getAuthorizer() {
void authenticator(String authenticator) {
this.authenticator = authenticator;
}

String authorizer() {
return authorizer;
}

public List<SecurityLevel> getSecurityLevels() {
void authorizer(String authorizer) {
this.authorizer = authorizer;
}

List<SecurityLevel> securityLevels() {
return securityLevels;
}

public boolean isAudited() {
boolean audited() {
return audited;
}

public String getAuditEventType() {
void audited(boolean audited) {
this.audited = audited;
}

String auditEventType() {
return auditEventType;
}

public String getAuditMessageFormat() {
void auditEventType(String auditEventType) {
this.auditEventType = auditEventType;
}

String auditMessageFormat() {
return auditMessageFormat;
}

public AuditEvent.AuditSeverity getAuditOkSeverity() {
void auditMessageFormat(String auditMessageFormat) {
this.auditMessageFormat = auditMessageFormat;
}

AuditEvent.AuditSeverity auditOkSeverity() {
return auditOkSeverity;
}

public AuditEvent.AuditSeverity getAuditErrorSeverity() {
void auditOkSeverity(AuditEvent.AuditSeverity auditOkSeverity) {
this.auditOkSeverity = auditOkSeverity;
}

AuditEvent.AuditSeverity auditErrorSeverity() {
return auditErrorSeverity;
}

public AnnotationAnalyzer.AnalyzerResponse analyzerResponse(AnnotationAnalyzer analyzer) {
void auditErrorSeverity(AuditEvent.AuditSeverity auditOkSeverity) {
this.auditErrorSeverity = auditOkSeverity;
}

AnnotationAnalyzer.AnalyzerResponse analyzerResponse(AnnotationAnalyzer analyzer) {
return analyzerResponses.get(analyzer);
}

public void analyzerResponse(AnnotationAnalyzer analyzer, AnnotationAnalyzer.AnalyzerResponse analyzerResponse) {
void analyzerResponse(AnnotationAnalyzer analyzer, AnnotationAnalyzer.AnalyzerResponse analyzerResponse) {
analyzerResponses.put(analyzer, analyzerResponse);

switch (analyzerResponse.authenticationResponse()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import io.helidon.common.HelidonServiceLoader;
import io.helidon.common.config.Config;
import io.helidon.common.context.Contexts;
import io.helidon.common.uri.UriPath;
import io.helidon.jersey.common.InvokedResource;
import io.helidon.security.AuditEvent;
import io.helidon.security.Security;
Expand Down Expand Up @@ -155,9 +156,9 @@ protected void processSecurity(ContainerRequestContext request,
* Authentication
*/
authenticate(filterContext, securityContext, tracing.atnTracing());
LOGGER.log(Level.TRACE, () -> "Filter after authentication. Should finish: " + filterContext.isShouldFinish());
LOGGER.log(Level.TRACE, () -> "Filter after authentication. Should finish: " + filterContext.shouldFinish());
// authentication failed
if (filterContext.isShouldFinish()) {
if (filterContext.shouldFinish()) {
return;
}

Expand Down Expand Up @@ -203,7 +204,7 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont
SecurityDefinition methodSecurity = jerseySecurityContext.methodSecurity();
SecurityContext securityContext = jerseySecurityContext.securityContext();

if (fc.isExplicitAtz() && !securityContext.isAuthorized()) {
if (fc.explicitAtz() && !securityContext.isAuthorized()) {
// now we have an option that the response code is already an error (e.g. BadRequest)
// in such a case we return the original error, as we may have never reached the method code
switch (responseContext.getStatusInfo().getFamily()) {
Expand All @@ -223,38 +224,38 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont
responseContext.setEntity("");
}
responseContext.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode());
LOGGER.log(Level.ERROR, "Authorization failure. Request for" + fc.getResourcePath()
LOGGER.log(Level.ERROR, "Authorization failure. Request for" + fc.resourcePath()
+ " has failed, as it was marked"
+ "as explicitly authorized, yet authorization was never called on security context. The "
+ "method was invoked and may have changed data. Marking as internal server error");
fc.setShouldFinish(true);
fc.shouldFinish(true);
break;
}
}

ResponseTracing responseTracing = SecurityTracing.get().responseTracing();

try {
if (methodSecurity.isAudited()) {
if (methodSecurity.audited()) {
AuditEvent.AuditSeverity auditSeverity;
if (responseContext.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL) {
auditSeverity = methodSecurity.getAuditOkSeverity();
auditSeverity = methodSecurity.auditOkSeverity();
} else {
auditSeverity = methodSecurity.getAuditErrorSeverity();
auditSeverity = methodSecurity.auditErrorSeverity();
}

SecurityAuditEvent auditEvent = SecurityAuditEvent
.audit(auditSeverity, methodSecurity.getAuditEventType(), methodSecurity.getAuditMessageFormat())
.addParam(AuditEvent.AuditParam.plain("method", fc.getMethod()))
.addParam(AuditEvent.AuditParam.plain("path", fc.getResourcePath()))
.audit(auditSeverity, methodSecurity.auditEventType(), methodSecurity.auditMessageFormat())
.addParam(AuditEvent.AuditParam.plain("method", fc.method()))
.addParam(AuditEvent.AuditParam.plain("path", fc.resourcePath()))
.addParam(AuditEvent.AuditParam.plain("status", String.valueOf(responseContext.getStatus())))
.addParam(AuditEvent.AuditParam.plain("subject",
securityContext.user()
.or(securityContext::service)
.orElse(SecurityContext.ANONYMOUS)))
.addParam(AuditEvent.AuditParam.plain("transport", "http"))
.addParam(AuditEvent.AuditParam.plain("resourceType", fc.getResourceName()))
.addParam(AuditEvent.AuditParam.plain("targetUri", fc.getTargetUri()));
.addParam(AuditEvent.AuditParam.plain("resourceType", fc.resourceName()))
.addParam(AuditEvent.AuditParam.plain("targetUri", fc.targetUri()));

securityContext.audit(auditEvent);
}
Expand All @@ -271,16 +272,16 @@ protected SecurityFilterContext initRequestFiltering(ContainerRequestContext req
return invokedResource
.definitionMethod()
.map(definitionMethod -> {
context.setMethodSecurity(getMethodSecurity(invokedResource,
definitionMethod,
(ExtendedUriInfo) requestContext.getUriInfo()));
context.setResourceName(definitionMethod.getDeclaringClass().getSimpleName());
context.methodSecurity(getMethodSecurity(invokedResource,
definitionMethod,
(ExtendedUriInfo) requestContext.getUriInfo()));
context.resourceName(definitionMethod.getDeclaringClass().getSimpleName());

return configureContext(context, requestContext, requestContext.getUriInfo());
})
.orElseGet(() -> {
// this will end in 404, just let it on
context.setShouldFinish(true);
context.shouldFinish(true);
return context;
});
}
Expand Down Expand Up @@ -325,7 +326,7 @@ private SecurityDefinition securityForClass(Class<?> theClass, SecurityDefinitio
SecurityLevel securityLevel = SecurityLevel.create(realClass.getName())
.withClassAnnotations(customAnnotsMap)
.build();
definition.getSecurityLevels().add(securityLevel);
definition.securityLevels().add(securityLevel);

for (AnnotationAnalyzer analyzer : analyzers) {
AnnotationAnalyzer.AnalyzerResponse analyzerResponse;
Expand All @@ -344,20 +345,6 @@ private SecurityDefinition securityForClass(Class<?> theClass, SecurityDefinitio
return definition;
}

/**
* Returns the real class of this object, skipping proxies.
*
* @param object The object.
* @return Its class.
*/
private static Class<?> getRealClass(Class<?> object) {
Class<?> result = object;
while (result.isSynthetic()) {
result = result.getSuperclass();
}
return result;
}

private SecurityDefinition getMethodSecurity(InvokedResource invokedResource,
Method definitionMethod,
ExtendedUriInfo uriInfo) {
Expand Down Expand Up @@ -427,17 +414,17 @@ private SecurityDefinition getMethodSecurity(InvokedResource invokedResource,
for (Method method : methodsToProcess) {
Class<?> clazz = method.getDeclaringClass();
current = securityForClass(clazz, current);
SecurityDefinition methodDef = processMethod(current.copyMe(), method);
SecurityDefinition methodDef = processMethod(current.copyMe(), uriInfo.getPath(), method);

SecurityLevel currentSecurityLevel = methodDef.getSecurityLevels().get(methodDef.getSecurityLevels().size() - 1);
SecurityLevel currentSecurityLevel = methodDef.securityLevels().get(methodDef.securityLevels().size() - 1);

Map<Class<? extends Annotation>, List<Annotation>> methodAnnotations = new HashMap<>();
addCustomAnnotations(methodAnnotations, method);
SecurityLevel newSecurityLevel = SecurityLevel.create(currentSecurityLevel)
.withMethodName(method.getName())
.withMethodAnnotations(methodAnnotations)
.build();
methodDef.getSecurityLevels().set(methodDef.getSecurityLevels().size() - 1, newSecurityLevel);
methodDef.securityLevels().set(methodDef.securityLevels().size() - 1, newSecurityLevel);
for (AnnotationAnalyzer analyzer : analyzers) {
AnnotationAnalyzer.AnalyzerResponse analyzerResponse = analyzer.analyze(method,
current.analyzerResponse(analyzer));
Expand Down Expand Up @@ -466,16 +453,14 @@ private SecurityDefinition getMethodSecurity(InvokedResource invokedResource,
}

SecurityDefinition resClassSecurity = obtainClassSecurityDefinition(appRealClass, appClassSecurity, definitionClass);
SecurityDefinition methodDef = processMethod(resClassSecurity, uriInfo.getRequestUri().getPath(), definitionMethod);


SecurityDefinition methodDef = processMethod(resClassSecurity, definitionMethod);

int index = methodDef.getSecurityLevels().size() - 1;
SecurityLevel currentSecurityLevel = methodDef.getSecurityLevels().get(index);
int index = methodDef.securityLevels().size() - 1;
SecurityLevel currentSecurityLevel = methodDef.securityLevels().get(index);
Map<Class<? extends Annotation>, List<Annotation>> methodLevelAnnotations = new HashMap<>();
addCustomAnnotations(methodLevelAnnotations, definitionMethod);

methodDef.getSecurityLevels().set(index, SecurityLevel.create(currentSecurityLevel)
methodDef.securityLevels().set(index, SecurityLevel.create(currentSecurityLevel)
.withMethodName(definitionMethod.getName())
.withMethodAnnotations(methodLevelAnnotations)
.build());
Expand Down Expand Up @@ -533,14 +518,19 @@ List<AnnotationAnalyzer> analyzers() {
return this.analyzers;
}

private static SecurityDefinition processMethod(SecurityDefinition current, Method method) {
Authenticated atn = method.getAnnotation(Authenticated.class);
Authorized atz = method.getAnnotation(Authorized.class);
Audited audited = method.getAnnotation(Audited.class);
private SecurityDefinition processMethod(SecurityDefinition current, String path, Method method) {
SecurityDefinition methodDef = current.copyMe();
methodDef.add(atn);
methodDef.add(atz);
methodDef.add(audited);
findMethodConfig(UriPath.create(path))
.asNode()
.ifPresentOrElse(methodDef::fromConfig,
() -> {
Authenticated atn = method.getAnnotation(Authenticated.class);
Authorized atz = method.getAnnotation(Authorized.class);
Audited audited = method.getAnnotation(Audited.class);
methodDef.add(atn);
methodDef.add(atz);
methodDef.add(audited);
});
return methodDef;
}

Expand Down
Loading

0 comments on commit c91a232

Please sign in to comment.