diff --git a/docs/src/main/asciidoc/includes/security/providers/abac.adoc b/docs/src/main/asciidoc/includes/security/providers/abac.adoc index 5a1f0820096..1909d7dc145 100644 --- a/docs/src/main/asciidoc/includes/security/providers/abac.adoc +++ b/docs/src/main/asciidoc/includes/security/providers/abac.adoc @@ -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}" +---- \ No newline at end of file diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/JerseySecurityContext.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/JerseySecurityContext.java index 76bd0453d9e..479d3e782df 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/JerseySecurityContext.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/JerseySecurityContext.java @@ -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. @@ -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 diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityDefinition.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityDefinition.java index 4e3df064f93..53624d5f9ab 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityDefinition.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityDefinition.java @@ -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; @@ -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; } @@ -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; } @@ -130,7 +145,7 @@ void requiresAuthentication(boolean atn) { this.requiresAuthentication = atn; } - void setRequiresAuthorization(boolean atz) { + void requiresAuthorization(boolean atz) { this.requiresAuthorization = atz; } @@ -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; @@ -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 getSecurityLevels() { + void authorizer(String authorizer) { + this.authorizer = authorizer; + } + + List 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()) { diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java index 8fc0fc23e98..101a640e81f 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java @@ -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; @@ -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; } @@ -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()) { @@ -223,11 +224,11 @@ 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; } } @@ -235,26 +236,26 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont 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); } @@ -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; }); } @@ -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; @@ -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) { @@ -427,9 +414,9 @@ 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, List> methodAnnotations = new HashMap<>(); addCustomAnnotations(methodAnnotations, method); @@ -437,7 +424,7 @@ private SecurityDefinition getMethodSecurity(InvokedResource invokedResource, .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)); @@ -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, List> 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()); @@ -533,14 +518,19 @@ List 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; } diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java index a2b7578b5b8..f37a97024ff 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java @@ -23,9 +23,14 @@ import java.util.ServiceLoader; import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.LazyValue; import io.helidon.common.config.Config; import io.helidon.common.context.Contexts; +import io.helidon.common.uri.UriPath; import io.helidon.common.uri.UriQuery; +import io.helidon.config.mp.MpConfig; +import io.helidon.http.PathMatcher; +import io.helidon.http.PathMatchers; import io.helidon.microprofile.security.spi.SecurityResponseMapper; import io.helidon.security.AuthenticationResponse; import io.helidon.security.AuthorizationResponse; @@ -45,6 +50,7 @@ import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; +import org.eclipse.microprofile.config.ConfigProvider; import org.glassfish.jersey.server.ContainerRequest; /** @@ -55,6 +61,7 @@ abstract class SecurityFilterCommon { private static final List RESPONSE_MAPPERS = HelidonServiceLoader .builder(ServiceLoader.load(SecurityResponseMapper.class)).build().asList(); + private static final LazyValue> PATH_CONFIGS = LazyValue.create(SecurityFilterCommon::createPathConfigs); private final Security security; @@ -67,6 +74,30 @@ abstract class SecurityFilterCommon { this.featureConfig = featureConfig; } + private static List createPathConfigs() { + return MpConfig.toHelidonConfig(ConfigProvider.getConfig()) + .get("server.features.security.endpoints") + .asNodeList() + .orElse(List.of()) + .stream() + .map(PathConfig::create) + .toList(); + } + + /** + * Returns the real class of this object, skipping proxies. + * + * @param object The object. + * @return Its class. + */ + static Class getRealClass(Class object) { + Class result = object; + while (result.isSynthetic()) { + result = result.getSuperclass(); + } + return result; + } + protected void doFilter(ContainerRequestContext request, SecurityContext securityContext) { SecurityTracing tracing = SecurityTracing.get(); tracing.securityContext(securityContext); @@ -79,7 +110,7 @@ protected void doFilter(ContainerRequestContext request, SecurityContext securit filterContext); } - if (filterContext.isShouldFinish()) { + if (filterContext.shouldFinish()) { if (logger().isLoggable(Level.TRACE)) { logger().log(Level.TRACE, "Endpoint %s not found, no security", request.getUriInfo().getRequestUri()); } @@ -96,17 +127,17 @@ protected void doFilter(ContainerRequestContext request, SecurityContext securit } else { origRequest = requestUri.getPath() + "?" + query; } - Map> allHeaders = new HashMap<>(filterContext.getHeaders()); + Map> allHeaders = new HashMap<>(filterContext.headers()); allHeaders.put(Security.HEADER_ORIG_URI, List.of(origRequest)); SecurityEnvironment.Builder envBuilder = SecurityEnvironment.builder(security.serverTime()) .transport(requestUri.getScheme()) - .path(filterContext.getResourcePath()) - .targetUri(filterContext.getTargetUri()) - .method(filterContext.getMethod()) - .queryParams(filterContext.getQueryParams()) + .path(filterContext.resourcePath()) + .targetUri(filterContext.targetUri()) + .method(filterContext.method()) + .queryParams(filterContext.queryParams()) .headers(allHeaders) - .addAttribute("resourceType", filterContext.getResourceName()); + .addAttribute("resourceType", filterContext.resourceName()); // The following two lines are not possible in JAX-RS or Jersey - we would have to touch // underlying web server's request... @@ -120,9 +151,14 @@ protected void doFilter(ContainerRequestContext request, SecurityContext securit } SecurityEnvironment env = envBuilder.build(); + Map configMap = new HashMap<>(); + findMethodConfig(UriPath.create(requestUri.getPath())) + .asNode() + .ifPresent(conf -> conf.asNodeList().get().forEach(node -> configMap.put(node.name(), node))); EndpointConfig ec = EndpointConfig.builder() - .securityLevels(filterContext.getMethodSecurity().getSecurityLevels()) + .securityLevels(filterContext.methodSecurity().securityLevels()) + .configMap(configMap) .build(); try { @@ -132,12 +168,12 @@ protected void doFilter(ContainerRequestContext request, SecurityContext securit request.setProperty(PROP_FILTER_CONTEXT, filterContext); //context is needed even if authn/authz fails - for auditing request.setSecurityContext(new JerseySecurityContext(securityContext, - filterContext.getMethodSecurity(), - "https".equals(filterContext.getTargetUri().getScheme()))); + filterContext.methodSecurity(), + "https".equals(filterContext.targetUri().getScheme()))); processSecurity(request, filterContext, tracing, securityContext); } finally { - if (filterContext.isTraceSuccess()) { + if (filterContext.traceSuccess()) { tracing.logProceed(); tracing.finish(); } else { @@ -147,13 +183,22 @@ protected void doFilter(ContainerRequestContext request, SecurityContext securit } } + Config findMethodConfig(UriPath path) { + return PATH_CONFIGS.get() + .stream() + .filter(pathConfig -> pathConfig.pathMatcher.prefixMatch(path).accepted()) + .findFirst() + .map(PathConfig::config) + .orElseGet(Config::empty); + } + protected void authenticate(SecurityFilterContext context, SecurityContext securityContext, AtnTracing atnTracing) { try { - SecurityDefinition methodSecurity = context.getMethodSecurity(); + SecurityDefinition methodSecurity = context.methodSecurity(); if (methodSecurity.requiresAuthentication()) { if (logger().isLoggable(Level.TRACE)) { - logger().log(Level.TRACE, "Endpoint {0} requires authentication", context.getTargetUri()); + logger().log(Level.TRACE, "Endpoint {0} requires authentication", context.targetUri()); } //authenticate request SecurityClientBuilder clientBuilder = securityContext @@ -161,15 +206,15 @@ protected void authenticate(SecurityFilterContext context, SecurityContext secur .optional(methodSecurity.authenticationOptional()) .tracingSpan(atnTracing.findParent().orElse(null)); - clientBuilder.explicitProvider(methodSecurity.getAuthenticator()); + clientBuilder.explicitProvider(methodSecurity.authenticator()); processAuthentication(context, clientBuilder, methodSecurity, atnTracing); } else { if (logger().isLoggable(Level.TRACE)) { - logger().log(Level.TRACE, "Endpoint {0} does not require authentication", context.getTargetUri()); + logger().log(Level.TRACE, "Endpoint {0} does not require authentication", context.targetUri()); } } } finally { - if (context.isTraceSuccess()) { + if (context.traceSuccess()) { securityContext.user() .ifPresent(atnTracing::logUser); @@ -178,9 +223,9 @@ protected void authenticate(SecurityFilterContext context, SecurityContext secur atnTracing.finish(); } else { - Throwable ctxThrowable = context.getTraceThrowable(); + Throwable ctxThrowable = context.traceThrowable(); if (null == ctxThrowable) { - atnTracing.error(context.getTraceDescription()); + atnTracing.error(context.traceDescription()); } else { atnTracing.error(ctxThrowable); } @@ -209,17 +254,17 @@ protected void processAuthentication(SecurityFilterContext context, if (methodSecurity.authenticationOptional()) { logger().log(Level.TRACE, "Authentication failed, but was optional, so assuming anonymous"); } else { - context.setTraceSuccess(false); - context.setTraceDescription(response.description().orElse(responseStatus.toString())); - context.setTraceThrowable(response.throwable().orElse(null)); - context.setShouldFinish(true); + context.traceSuccess(false); + context.traceDescription(response.description().orElse(responseStatus.toString())); + context.traceThrowable(response.throwable().orElse(null)); + context.shouldFinish(true); int status = response.statusCode().orElse(Response.Status.UNAUTHORIZED.getStatusCode()); abortRequest(context, response, status, Map.of()); } } case SUCCESS_FINISH -> { - context.setShouldFinish(true); + context.shouldFinish(true); int status = response.statusCode().orElse(Response.Status.OK.getStatusCode()); abortRequest(context, response, status, Map.of()); } @@ -227,9 +272,9 @@ protected void processAuthentication(SecurityFilterContext context, if (methodSecurity.authenticationOptional()) { logger().log(Level.TRACE, "Authentication failed, but was optional, so assuming anonymous"); } else { - context.setTraceSuccess(false); - context.setTraceDescription(response.description().orElse(responseStatus.toString())); - context.setShouldFinish(true); + context.traceSuccess(false); + context.traceDescription(response.description().orElse(responseStatus.toString())); + context.shouldFinish(true); abortRequest(context, response, Response.Status.UNAUTHORIZED.getStatusCode(), @@ -240,23 +285,23 @@ protected void processAuthentication(SecurityFilterContext context, if (methodSecurity.authenticationOptional() && !methodSecurity.failOnFailureIfOptional()) { logger().log(Level.TRACE, "Authentication failed, but was optional, so assuming anonymous"); } else { - context.setTraceDescription(response.description().orElse(responseStatus.toString())); - context.setTraceThrowable(response.throwable().orElse(null)); - context.setTraceSuccess(false); + context.traceDescription(response.description().orElse(responseStatus.toString())); + context.traceThrowable(response.throwable().orElse(null)); + context.traceSuccess(false); abortRequest(context, response, Response.Status.UNAUTHORIZED.getStatusCode(), Map.of()); - context.setShouldFinish(true); + context.shouldFinish(true); } } //noinspection DuplicatedCode default -> { - context.setTraceSuccess(false); - context.setTraceDescription(response.description().orElse("UNKNOWN_RESPONSE: " + responseStatus)); - context.setShouldFinish(true); + context.traceSuccess(false); + context.traceDescription(response.description().orElse("UNKNOWN_RESPONSE: " + responseStatus)); + context.shouldFinish(true); SecurityException throwable = new SecurityException("Invalid SecurityStatus returned: " + responseStatus); - context.setTraceThrowable(throwable); + context.traceThrowable(throwable); throw throwable; } } @@ -267,41 +312,41 @@ protected void processAuthentication(SecurityFilterContext context, protected void authorize(SecurityFilterContext context, SecurityContext securityContext, AtzTracing atzTracing) { - if (context.getMethodSecurity().isAtzExplicit()) { + if (context.methodSecurity().atzExplicit()) { // authorization is explicitly done by user, we MUST skip it here if (logger().isLoggable(Level.TRACE)) { - logger().log(Level.TRACE, "Endpoint {0} uses explicit authorization, skipping", context.getTargetUri()); + logger().log(Level.TRACE, "Endpoint {0} uses explicit authorization, skipping", context.targetUri()); } - context.setExplicitAtz(true); + context.explicitAtz(true); return; } try { //now authorize (also authorize anonymous requests, as we may have a path-based authorization that allows public // access - if (context.getMethodSecurity().requiresAuthorization()) { + if (context.methodSecurity().requiresAuthorization()) { if (logger().isLoggable(Level.TRACE)) { - logger().log(Level.TRACE, "Endpoint {0} requires authorization", context.getTargetUri()); + logger().log(Level.TRACE, "Endpoint {0} requires authorization", context.targetUri()); } SecurityClientBuilder clientBuilder = securityContext.atzClientBuilder() .tracingSpan(atzTracing.findParent().orElse(null)) - .explicitProvider(context.getMethodSecurity().getAuthorizer()); + .explicitProvider(context.methodSecurity().authorizer()); processAuthorization(context, clientBuilder); } else { if (logger().isLoggable(Level.TRACE)) { logger().log(Level.TRACE, "Endpoint {0} does not require authorization. Method security: {1}", - context.getTargetUri(), - context.getMethodSecurity()); + context.targetUri(), + context.methodSecurity()); } } } finally { - if (context.isTraceSuccess()) { + if (context.traceSuccess()) { atzTracing.finish(); } else { - Throwable throwable = context.getTraceThrowable(); + Throwable throwable = context.traceThrowable(); if (null == throwable) { - atzTracing.error(context.getTraceDescription()); + atzTracing.error(context.traceDescription()); } else { atzTracing.error(throwable); } @@ -321,32 +366,32 @@ protected void processAuthorization(SecurityFilterContext context, //everything is fine, we can continue with processing } case FAILURE_FINISH -> { - context.setTraceSuccess(false); - context.setTraceDescription(response.description().orElse(responseStatus.toString())); - context.setTraceThrowable(response.throwable().orElse(null)); - context.setShouldFinish(true); + context.traceSuccess(false); + context.traceDescription(response.description().orElse(responseStatus.toString())); + context.traceThrowable(response.throwable().orElse(null)); + context.shouldFinish(true); int status = response.statusCode().orElse(Response.Status.FORBIDDEN.getStatusCode()); abortRequest(context, response, status, Map.of()); } case SUCCESS_FINISH -> { - context.setShouldFinish(true); + context.shouldFinish(true); int status = response.statusCode().orElse(Response.Status.OK.getStatusCode()); abortRequest(context, response, status, Map.of()); } case FAILURE -> { - context.setTraceSuccess(false); - context.setTraceDescription(response.description().orElse(responseStatus.toString())); - context.setTraceThrowable(response.throwable().orElse(null)); - context.setShouldFinish(true); + context.traceSuccess(false); + context.traceDescription(response.description().orElse(responseStatus.toString())); + context.traceThrowable(response.throwable().orElse(null)); + context.shouldFinish(true); abortRequest(context, response, response.statusCode().orElse(Response.Status.FORBIDDEN.getStatusCode()), Map.of()); } case ABSTAIN -> { - context.setTraceSuccess(false); - context.setTraceDescription(response.description().orElse(responseStatus.toString())); - context.setShouldFinish(true); + context.traceSuccess(false); + context.traceDescription(response.description().orElse(responseStatus.toString())); + context.shouldFinish(true); abortRequest(context, response, response.statusCode().orElse(Response.Status.FORBIDDEN.getStatusCode()), @@ -354,11 +399,11 @@ protected void processAuthorization(SecurityFilterContext context, } //noinspection DuplicatedCode default -> { - context.setTraceSuccess(false); - context.setTraceDescription(response.description().orElse("UNKNOWN_RESPONSE: " + responseStatus)); - context.setShouldFinish(true); + context.traceSuccess(false); + context.traceDescription(response.description().orElse("UNKNOWN_RESPONSE: " + responseStatus)); + context.shouldFinish(true); SecurityException throwable = new SecurityException("Invalid SecurityStatus returned: " + responseStatus); - context.setTraceThrowable(throwable); + context.traceThrowable(throwable); throw throwable; } } @@ -389,7 +434,7 @@ protected void abortRequest(SecurityFilterContext context, } if (featureConfig.useAbortWith()) { - context.getJerseyRequest().abortWith(responseBuilder.build()); + context.jerseyRequest().abortWith(responseBuilder.build()); } else { String description = response.description() .orElse("Security did not allow this request to proceed."); @@ -408,17 +453,17 @@ protected void updateHeaders(Map> responseHeaders, Response protected SecurityFilterContext configureContext(SecurityFilterContext context, ContainerRequestContext requestContext, UriInfo uriInfo) { - context.setMethod(requestContext.getMethod()); - context.setHeaders(requestContext.getHeaders()); - context.setTargetUri(requestContext.getUriInfo().getRequestUri()); - context.setResourcePath(context.getTargetUri().getPath()); - context.setQueryParams(UriQuery.create(uriInfo.getRequestUri())); + context.method(requestContext.getMethod()); + context.headers(requestContext.getHeaders()); + context.targetUri(requestContext.getUriInfo().getRequestUri()); + context.resourcePath(context.targetUri().getPath()); + context.queryParams(UriQuery.create(uriInfo.getRequestUri())); - context.setJerseyRequest((ContainerRequest) requestContext); + context.jerseyRequest((ContainerRequest) requestContext); // now extract headers featureConfig().getQueryParamHandlers() - .forEach(handler -> handler.extract(uriInfo, context.getHeaders())); + .forEach(handler -> handler.extract(uriInfo, context.headers())); return context; } @@ -441,4 +486,14 @@ protected abstract void processSecurity(ContainerRequestContext request, Config config(String child) { return security.configFor(child); } + + private record PathConfig(PathMatcher pathMatcher, Config config) { + + static PathConfig create(Config config) { + String path = config.get("path").asString().orElseThrow(); + PathMatcher matcher = PathMatchers.create(path); + return new PathConfig(matcher, config.get("config")); + } + + } } diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterContext.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterContext.java index 20d082c48ba..946308d8390 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterContext.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -28,6 +28,9 @@ */ public class SecurityFilterContext { private String resourceName; + private String fullResourceName; + private String resourceMethod; + private String fullResourceMethod; private String resourcePath; private String method; private Map> headers; @@ -60,113 +63,137 @@ public String toString() { + '}'; } - String getResourceName() { + String resourceName() { return resourceName; } - void setResourceName(String resourceName) { + void resourceName(String resourceName) { this.resourceName = resourceName; } - String getResourcePath() { + String fullResourceName() { + return fullResourceName; + } + + void fullResourceName(String fullResourceName) { + this.fullResourceName = fullResourceName; + } + + String resourceMethod() { + return resourceMethod; + } + + void resourceMethod(String resourceMethod) { + this.resourceMethod = resourceMethod; + } + + String fullResourceMethod() { + return fullResourceMethod; + } + + void fullResourceMethod(String fullResourceMethod) { + this.fullResourceMethod = fullResourceMethod; + } + + String resourcePath() { return resourcePath; } - void setResourcePath(String resourcePath) { + void resourcePath(String resourcePath) { this.resourcePath = resourcePath; } - String getMethod() { + String method() { return method; } - void setMethod(String method) { + void method(String method) { this.method = method; } - Map> getHeaders() { + Map> headers() { return headers; } - void setHeaders(Map> headers) { + void headers(Map> headers) { this.headers = headers; } - URI getTargetUri() { + URI targetUri() { return targetUri; } - void setTargetUri(URI targetUri) { + void targetUri(URI targetUri) { this.targetUri = targetUri; } - ContainerRequest getJerseyRequest() { + ContainerRequest jerseyRequest() { return jerseyRequest; } - void setJerseyRequest(ContainerRequest jerseyRequest) { + void jerseyRequest(ContainerRequest jerseyRequest) { this.jerseyRequest = jerseyRequest; } - boolean isShouldFinish() { + boolean shouldFinish() { return shouldFinish; } - void setShouldFinish(boolean shouldFinish) { + void shouldFinish(boolean shouldFinish) { this.shouldFinish = shouldFinish; } - SecurityDefinition getMethodSecurity() { + SecurityDefinition methodSecurity() { return methodSecurity; } - void setMethodSecurity(SecurityDefinition methodSecurity) { + void methodSecurity(SecurityDefinition methodSecurity) { this.methodSecurity = methodSecurity; } - boolean isExplicitAtz() { + boolean explicitAtz() { return explicitAtz; } - void setExplicitAtz(boolean explicitAtz) { + void explicitAtz(boolean explicitAtz) { this.explicitAtz = explicitAtz; } - boolean isTraceSuccess() { + boolean traceSuccess() { return traceSuccess; } - void setTraceSuccess(boolean traceSuccess) { + void traceSuccess(boolean traceSuccess) { this.traceSuccess = traceSuccess; } - String getTraceDescription() { + String traceDescription() { return traceDescription; } - void setTraceDescription(String traceDescription) { + void traceDescription(String traceDescription) { this.traceDescription = traceDescription; } - Throwable getTraceThrowable() { + Throwable traceThrowable() { return traceThrowable; } - void setTraceThrowable(Throwable traceThrowable) { + void traceThrowable(Throwable traceThrowable) { this.traceThrowable = traceThrowable; } - UriQuery getQueryParams() { + UriQuery queryParams() { return queryParams; } - void setQueryParams(UriQuery queryParams) { + void queryParams(UriQuery queryParams) { this.queryParams = queryParams; } void clearTrace() { - setTraceSuccess(true); - setTraceDescription(null); - setTraceThrowable(null); + traceSuccess(true); + traceDescription(null); + traceThrowable(null); } } diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityPreMatchingFilter.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityPreMatchingFilter.java index 45be298e0cc..65a3faef81a 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityPreMatchingFilter.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityPreMatchingFilter.java @@ -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. @@ -97,10 +97,10 @@ protected void processSecurity(ContainerRequestContext request, // when I reach this point, I am sure we should at least authenticate in prematching filter 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; } @@ -122,9 +122,9 @@ protected SecurityFilterContext initRequestFiltering(ContainerRequestContext req SecurityDefinition methodDef = new SecurityDefinition(false, false); methodDef.requiresAuthentication(true); - methodDef.setRequiresAuthorization(featureConfig().shouldUsePrematchingAuthorization()); - context.setMethodSecurity(methodDef); - context.setResourceName("jax-rs"); + methodDef.requiresAuthorization(featureConfig().shouldUsePrematchingAuthorization()); + context.methodSecurity(methodDef); + context.resourceName("jax-rs"); return configureContext(context, requestContext, uriInfo); } diff --git a/microprofile/security/src/main/java/module-info.java b/microprofile/security/src/main/java/module-info.java index ebf03e7c62c..aeceddd027c 100644 --- a/microprofile/security/src/main/java/module-info.java +++ b/microprofile/security/src/main/java/module-info.java @@ -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. @@ -28,6 +28,7 @@ @SuppressWarnings({ "requires-automatic", "requires-transitive-automatic" }) module io.helidon.microprofile.security { + requires io.helidon.config.mp; requires io.helidon.jersey.common; requires io.helidon.microprofile.cdi; requires io.helidon.microprofile.server; diff --git a/microprofile/security/src/test/java/io/helidon/microprofile/security/OptionalSecurityTest.java b/microprofile/security/src/test/java/io/helidon/microprofile/security/OptionalSecurityTest.java index 4bb0afe14e1..5e55a002bbc 100644 --- a/microprofile/security/src/test/java/io/helidon/microprofile/security/OptionalSecurityTest.java +++ b/microprofile/security/src/test/java/io/helidon/microprofile/security/OptionalSecurityTest.java @@ -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. @@ -100,7 +100,7 @@ void testOptional() { */ securityFilter.processAuthentication(filterContext, clientBuilder, methodSecurity, tracing.atnTracing()); - assertThat(filterContext.isShouldFinish(), is(false)); + assertThat(filterContext.shouldFinish(), is(false)); assertThat(securityContext.user(), is(Optional.empty())); } @@ -108,7 +108,7 @@ void testOptional() { void testNotOptional() { SecurityContext securityContext = security.createContext("context_id"); SecurityFilterContext filterContext = new SecurityFilterContext(); - filterContext.setJerseyRequest(mock(ContainerRequest.class)); + filterContext.jerseyRequest(mock(ContainerRequest.class)); SecurityDefinition methodSecurity = mock(SecurityDefinition.class); when(methodSecurity.authenticationOptional()).thenReturn(false); @@ -123,7 +123,7 @@ void testNotOptional() { */ securityFilter.processAuthentication(filterContext, clientBuilder, methodSecurity, tracing.atnTracing()); - assertThat(filterContext.isShouldFinish(), is(true)); + assertThat(filterContext.shouldFinish(), is(true)); assertThat(securityContext.user(), is(Optional.empty())); } diff --git a/microprofile/security/src/test/java/io/helidon/microprofile/security/SecurityFilterTest.java b/microprofile/security/src/test/java/io/helidon/microprofile/security/SecurityFilterTest.java index 4176a55a166..1c66526c140 100644 --- a/microprofile/security/src/test/java/io/helidon/microprofile/security/SecurityFilterTest.java +++ b/microprofile/security/src/test/java/io/helidon/microprofile/security/SecurityFilterTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 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. @@ -66,7 +66,7 @@ void testAtnAbortWith() { ContainerRequest request = mock(ContainerRequest.class); SecurityFilterContext filterContext = new SecurityFilterContext(); - filterContext.setJerseyRequest(request); + filterContext.jerseyRequest(request); SecurityDefinition methodSecurity = mock(SecurityDefinition.class); @@ -74,7 +74,7 @@ void testAtnAbortWith() { when(clientBuilder.submit()).thenReturn(AuthenticationResponse.failed("Unit-test")); sf.processAuthentication(filterContext, clientBuilder, methodSecurity, tracing.atnTracing()); - assertThat(filterContext.isShouldFinish(), is(true)); + assertThat(filterContext.shouldFinish(), is(true)); verify(request).abortWith(argThat(response -> response.getStatus() == 401)); } @@ -94,7 +94,7 @@ void testAtnThrowException() { ContainerRequest request = mock(ContainerRequest.class); SecurityFilterContext filterContext = new SecurityFilterContext(); - filterContext.setJerseyRequest(request); + filterContext.jerseyRequest(request); SecurityDefinition methodSecurity = mock(SecurityDefinition.class); @@ -125,7 +125,7 @@ void testAtzAbortWith() { ContainerRequest request = mock(ContainerRequest.class); SecurityFilterContext filterContext = new SecurityFilterContext(); - filterContext.setJerseyRequest(request); + filterContext.jerseyRequest(request); SecurityClientBuilder clientBuilder = mock(SecurityClientBuilder.class); when(clientBuilder.submit()).thenReturn(AuthorizationResponse.builder() @@ -134,7 +134,7 @@ void testAtzAbortWith() { .build()); sf.processAuthorization(filterContext, clientBuilder); - assertThat(filterContext.isShouldFinish(), is(true)); + assertThat(filterContext.shouldFinish(), is(true)); verify(request).abortWith(argThat(response -> response.getStatus() == 403)); } @@ -154,7 +154,7 @@ void testAtzThrowException() { ContainerRequest request = mock(ContainerRequest.class); SecurityFilterContext filterContext = new SecurityFilterContext(); - filterContext.setJerseyRequest(request); + filterContext.jerseyRequest(request); SecurityClientBuilder clientBuilder = mock(SecurityClientBuilder.class); when(clientBuilder.submit()).thenReturn(AuthorizationResponse.builder() diff --git a/tests/integration/security/abac-policy/pom.xml b/tests/integration/security/abac-policy/pom.xml new file mode 100644 index 00000000000..f321e854038 --- /dev/null +++ b/tests/integration/security/abac-policy/pom.xml @@ -0,0 +1,63 @@ + + + + + 4.0.0 + + io.helidon.tests.integration + helidon-tests-integration-security + 4.1.0-SNAPSHOT + + + helidon-tests-integration-security-abac-policy + Helidon Tests Integration ABAC Policy Validator + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.security.abac + helidon-security-abac-policy-el + + + org.glassfish + jakarta.el + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + + \ No newline at end of file diff --git a/tests/integration/security/abac-policy/src/main/java/io/helidon/tests/integration/security/abac/policy/PolicyStatementExplicitResource.java b/tests/integration/security/abac-policy/src/main/java/io/helidon/tests/integration/security/abac/policy/PolicyStatementExplicitResource.java new file mode 100644 index 00000000000..c470f7ae02c --- /dev/null +++ b/tests/integration/security/abac-policy/src/main/java/io/helidon/tests/integration/security/abac/policy/PolicyStatementExplicitResource.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.tests.integration.security.abac.policy; + +import io.helidon.security.AuthorizationResponse; +import io.helidon.security.SecurityContext; +import io.helidon.security.abac.policy.PolicyValidator; +import io.helidon.security.annotations.Authorized; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; + +/** + * A resource with explicit authorization. + */ +@Path("/explicit") +public class PolicyStatementExplicitResource { + + /** + * Policy statement and explicit authorization is set via annotations and not overridden. + * + * @param context security context to perform an explicit authorization + * @return result of the authorization + */ + @GET + @Path("annotation") + @Authorized(explicit = true) + @PolicyValidator.PolicyStatement("${env.time.year >= 2017}") + public Response annotation(@Context SecurityContext context) { + AuthorizationResponse atzResponse = context.authorize(); + + if (atzResponse.isPermitted()) { + return Response.ok().entity("passed").build(); + } else { + return Response.status(Response.Status.FORBIDDEN) + .entity(atzResponse.description().orElse("Access not granted")) + .build(); + } + } + + /** + * Policy statement and explicit authorization is set via configuration. + * + * @param context security context to perform an explicit authorization + * @return result of the authorization + */ + @GET + @Path("configuration") + @Authorized + @PolicyValidator.PolicyStatement("${env.time.year < 2017}") + public Response configuration(@Context SecurityContext context) { + AuthorizationResponse atzResponse = context.authorize(); + + if (atzResponse.isPermitted()) { + return Response.ok().entity("passed").build(); + } else { + return Response.status(Response.Status.FORBIDDEN) + .entity(atzResponse.description().orElse("Access not granted")) + .build(); + } + } + +} diff --git a/tests/integration/security/abac-policy/src/main/java/io/helidon/tests/integration/security/abac/policy/PolicyStatementResource.java b/tests/integration/security/abac-policy/src/main/java/io/helidon/tests/integration/security/abac/policy/PolicyStatementResource.java new file mode 100644 index 00000000000..1b3551b90db --- /dev/null +++ b/tests/integration/security/abac-policy/src/main/java/io/helidon/tests/integration/security/abac/policy/PolicyStatementResource.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.tests.integration.security.abac.policy; + +import io.helidon.security.abac.policy.PolicyValidator; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; + +/** + * A resource with abac policy statements. + */ +@Path("/policy") +public class PolicyStatementResource { + + /** + * Policy statement configured via annotation. + * + * @return passed value + */ + @GET + @Path("/annotation") + @PolicyValidator.PolicyStatement("${env.time.year >= 2017}") + public String annotation() { + return "passed"; + } + + /** + * Policy statement overridden by the config. + * + * @return passed value + */ + @GET + @Path("/override") + @PolicyValidator.PolicyStatement("${env.time.year <= 2017}") + public String override() { + return "passed"; + } + + /** + * Policy statement should not be overridden by the config. + * + * @return passed value + */ + @GET + @Path("/override2") + @PolicyValidator.PolicyStatement("${env.time.year <= 2017}") + public String override2() { + return "should not pass"; + } + + /** + * Policy statement overridden by the config with asterisk present in path. + * + * @return passed value + */ + @GET + @Path("/asterisk") + @PolicyValidator.PolicyStatement("${env.time.year <= 2017}") + public String asterisk() { + return "passed"; + } + + /** + * Policy statement overridden by the config with asterisk present in path. + * + * @return passed value + */ + @GET + @Path("/asterisk2") + @PolicyValidator.PolicyStatement("${env.time.year <= 2017}") + public String asterisk2() { + return "passed"; + } + + /** + * Policy statement not overridden by configuration and should not let anyone in. + * + * @return should not pass value + */ + @GET + @Path("/notOverride") + @PolicyValidator.PolicyStatement("${env.time.year <= 2017}") + public String notOverride() { + return "should not pass"; + } + +} diff --git a/tests/integration/security/abac-policy/src/main/resources/META-INF/beans.xml b/tests/integration/security/abac-policy/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..52f89a20d18 --- /dev/null +++ b/tests/integration/security/abac-policy/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/tests/integration/security/abac-policy/src/main/resources/application.yaml b/tests/integration/security/abac-policy/src/main/resources/application.yaml new file mode 100644 index 00000000000..b3be701fd6a --- /dev/null +++ b/tests/integration/security/abac-policy/src/main/resources/application.yaml @@ -0,0 +1,35 @@ +# +# Copyright (c) 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +server: + port: 0 + features: + security: + endpoints: + - path: "/policy/override" + config: + abac.policy-validator.statement: "${env.time.year >= 2017}" + - path: "/policy/asterisk*" + config: + abac.policy-validator.statement: "${env.time.year >= 2017}" + - path: "/explicit/configuration" + config: + authorize: true + authorization-explicit: true + abac.policy-validator.statement: "${env.time.year >= 2017}" + +security: + providers: + - abac: \ No newline at end of file diff --git a/tests/integration/security/abac-policy/src/main/resources/logging.properties b/tests/integration/security/abac-policy/src/main/resources/logging.properties new file mode 100644 index 00000000000..2a6f3a7be2a --- /dev/null +++ b/tests/integration/security/abac-policy/src/main/resources/logging.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + diff --git a/tests/integration/security/abac-policy/src/test/java/io/helidon/tests/integration/security/abac/policy/ExplicitPolicyTest.java b/tests/integration/security/abac-policy/src/test/java/io/helidon/tests/integration/security/abac/policy/ExplicitPolicyTest.java new file mode 100644 index 00000000000..a77ab2618ab --- /dev/null +++ b/tests/integration/security/abac-policy/src/test/java/io/helidon/tests/integration/security/abac/policy/ExplicitPolicyTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.tests.integration.security.abac.policy; + +import io.helidon.http.Status; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest +class ExplicitPolicyTest { + + @Test + void testAnnotation(WebTarget target) { + try (Response response = target.path("/explicit/annotation") + .request() + .get()) { + assertThat(response.getStatus(), is(Status.OK_200.code())); + assertThat(response.readEntity(String.class), is("passed")); + } + } + + @Test + void testConfiguration(WebTarget target) { + try (Response response = target.path("/explicit/configuration") + .request() + .get()) { + assertThat(response.getStatus(), is(Status.OK_200.code())); + assertThat(response.readEntity(String.class), is("passed")); + } + } + +} diff --git a/tests/integration/security/abac-policy/src/test/java/io/helidon/tests/integration/security/abac/policy/PolicyTest.java b/tests/integration/security/abac-policy/src/test/java/io/helidon/tests/integration/security/abac/policy/PolicyTest.java new file mode 100644 index 00000000000..bf63cc4b70e --- /dev/null +++ b/tests/integration/security/abac-policy/src/test/java/io/helidon/tests/integration/security/abac/policy/PolicyTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.tests.integration.security.abac.policy; + +import io.helidon.http.Status; +import io.helidon.microprofile.testing.junit5.AddConfig; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest +class PolicyTest { + + @Test + void testAnnotation(WebTarget target) { + try (Response response = target.path("/policy/annotation") + .request() + .get()) { + assertThat(response.getStatus(), is(Status.OK_200.code())); + assertThat(response.readEntity(String.class), is("passed")); + } + } + + @Test + void testExplicitOverride(WebTarget target) { + try (Response response = target.path("/policy/override") + .request() + .get()) { + assertThat(response.getStatus(), is(Status.OK_200.code())); + assertThat(response.readEntity(String.class), is("passed")); + } + try (Response response = target.path("/policy/override2") + .request() + .get()) { + assertThat(response.getStatus(), is(Status.FORBIDDEN_403.code())); + } + } + + @Test + void testAsteriskOverride(WebTarget target) { + try (Response response = target.path("/policy/asterisk") + .request() + .get()) { + assertThat(response.getStatus(), is(Status.OK_200.code())); + assertThat(response.readEntity(String.class), is("passed")); + } + try (Response response = target.path("/policy/asterisk2") + .request() + .get()) { + assertThat(response.getStatus(), is(Status.OK_200.code())); + assertThat(response.readEntity(String.class), is("passed")); + } + } + + @Test + void testNotOverride(WebTarget target) { + try (Response response = target.path("/policy/notOverride") + .request() + .get()) { + assertThat(response.getStatus(), is(Status.FORBIDDEN_403.code())); + } + } +} diff --git a/tests/integration/security/pom.xml b/tests/integration/security/pom.xml index 259b82483c7..a77103d2030 100644 --- a/tests/integration/security/pom.xml +++ b/tests/integration/security/pom.xml @@ -42,5 +42,6 @@ path-params security-response-mapper security-annotation + abac-policy