Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4.1.2: Policy validator configurable per endpoint in config (#9248) #9308

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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