All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.helidon.security.integration.grpc.GrpcSecurityHandler Maven / Gradle / Ivy

There is a newer version: 4.0.0-ALPHA6
Show newest version
/*
 * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
 *
 * 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.security.integration.grpc;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.Priority;

import io.helidon.config.Config;
import io.helidon.grpc.core.InterceptorPriorities;
import io.helidon.grpc.server.ServiceDescriptor;
import io.helidon.security.AuditEvent;
import io.helidon.security.AuthenticationResponse;
import io.helidon.security.AuthorizationResponse;
import io.helidon.security.ClassToInstanceStore;
import io.helidon.security.Security;
import io.helidon.security.SecurityClientBuilder;
import io.helidon.security.SecurityContext;
import io.helidon.security.SecurityRequest;
import io.helidon.security.SecurityRequestBuilder;
import io.helidon.security.SecurityResponse;
import io.helidon.security.integration.common.AtnTracing;
import io.helidon.security.integration.common.AtzTracing;
import io.helidon.security.integration.common.SecurityTracing;
import io.helidon.security.internal.SecurityAuditEvent;

import io.grpc.Context;
import io.grpc.Contexts;
import io.grpc.ForwardingServerCall;
import io.grpc.ForwardingServerCallListener;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.grpc.Status;
import io.opentracing.Span;
import io.opentracing.SpanContext;

import static io.helidon.security.AuditEvent.AuditParam.plain;

/**
 * Handles security for the gRPC server. This handler is registered either by hand on the gRPC routing config,
 * or automatically from configuration when integration is done through {@link GrpcSecurity#create(Config)}
 * or {@link GrpcSecurity#create(Security)}.
 * 

* This class is an implementation of a {@link ServerInterceptor} with a priority of * {@link InterceptorPriorities#CONTEXT} that will add itself to the call context with the key * {@link GrpcSecurity#GRPC_SECURITY_HANDLER}. This will then cause the {@link GrpcSecurity} * interceptor that runs later with a priority of {@link InterceptorPriorities#AUTHENTICATION} to use * this instance of the handler. */ // we need to have all fields optional and this is cleaner than checking for null @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Priority(InterceptorPriorities.CONTEXT) public class GrpcSecurityHandler implements ServerInterceptor, ServiceDescriptor.Configurer { private static final Logger LOGGER = Logger.getLogger(GrpcSecurityHandler.class.getName()); private static final String KEY_ROLES_ALLOWED = "roles-allowed"; private static final String KEY_AUTHENTICATOR = "authenticator"; private static final String KEY_AUTHORIZER = "authorizer"; private static final String KEY_AUTHENTICATE = "authenticate"; private static final String KEY_AUTHENTICATION_OPTIONAL = "authentication-optional"; private static final String KEY_AUTHORIZE = "authorize"; private static final String KEY_AUDIT = "audit"; private static final String KEY_AUDIT_EVENT_TYPE = "audit-event-type"; private static final String KEY_AUDIT_MESSAGE_FORMAT = "audit-message-format"; private static final String DEFAULT_AUDIT_EVENT_TYPE = "grpcRequest"; private static final String DEFAULT_AUDIT_MESSAGE_FORMAT = "%2$s %1$s %4$s %5$s requested by %3$s"; private static final GrpcSecurityHandler DEFAULT_INSTANCE = builder().build(); private final Optional> rolesAllowed; private final Optional> customObjects; private final Optional config; private final Optional explicitAuthenticator; private final Optional explicitAuthorizer; private final Optional authenticate; private final Optional authenticationOptional; private final Optional authorize; private final Optional audited; private final Optional auditEventType; private final Optional auditMessageFormat; private final boolean combined; private final Map configMap = new HashMap<>(); // lazily initialized (as it requires a context value to first create it) private final AtomicReference combinedHandler = new AtomicReference<>(); private GrpcSecurityHandler(Builder builder) { // must copy values to be safely immutable this.rolesAllowed = builder.rolesAllowed.flatMap(strings -> { Set newRoles = new HashSet<>(strings); return Optional.of(newRoles); }); // must copy values to be safely immutable this.customObjects = builder.customObjects.flatMap(store -> { ClassToInstanceStore ctis = new ClassToInstanceStore<>(); ctis.putAll(store); return Optional.of(ctis); }); config = builder.config; explicitAuthenticator = builder.explicitAuthenticator; explicitAuthorizer = builder.explicitAuthorizer; authenticate = builder.authenticate; authenticationOptional = builder.authenticationOptional; audited = builder.audited; auditEventType = builder.auditEventType; auditMessageFormat = builder.auditMessageFormat; authorize = builder.authorize; combined = builder.combined; config.ifPresent(conf -> { if (conf.exists() && !conf.isLeaf()) { conf.asNodeList().get().forEach(node -> configMap.put(node.name(), node)); } }); } /** * Create an instance from configuration. *

* The config expected (example in HOCON format): *

     * {
     *   #
     *   # these are used by {@link GrpcSecurity} when loaded from config, to register
     *   # with the {@link io.helidon.grpc.server.GrpcServer}
     *   #
     *   path = "/noRoles"
     *   methods = ["get"]
     *
     *   #
     *   # these are used by this class
     *   #
     *   # whether to authenticate this request - defaults to false (even if authorize is true)
     *   authenticate = true
     *   # if set to true, authentication failure will not abort request and will continue as anonymous (defaults to false)
     *   authentication optional
     *   # use a named authenticator (as supported by security - if not defined, default authenticator is used)
     *   authenticator = "basic-auth"
     *   # an array of allowed roles for this path - must have a security provider supporting roles
     *   roles-allowed = ["user"]
     *   # whether to authorize this request - defaults to true (authorization is "on" by default)
     *   authorize = true
     *   # use a named authorizer (as supported by security - if not defined, default authorizer is used, if none defined, all is
     *   #   permitted)
     *   authorizer = "roles"
     *   # whether to audit this request - defaults to false, if enabled, request is audited with event type "request"
     *   audit = true
     *   # override for event-type, defaults to {@value #DEFAULT_AUDIT_EVENT_TYPE}
     *   audit-event-type = "unit_test"
     *   # override for audit message format, defaults to {@value #DEFAULT_AUDIT_MESSAGE_FORMAT}
     *   audit-message-format = "Unit test message format"
     *   # override for audit severity for successful requests (1xx, 2xx and 3xx status codes),
     *   #   defaults to {@link AuditEvent.AuditSeverity#SUCCESS}
     *   audit-ok-severity = "AUDIT_FAILURE"
     *   # override for audit severity for unsuccessful requests (4xx and 5xx status codes),
     *   #   defaults to {@link AuditEvent.AuditSeverity#FAILURE}
     *   audit-error-severity = "INFO"
     *
     *   #
     *   # Any other configuration - this all gets passed to a security provider, so check your provider's documentation
     *   #
     *   custom-provider {
     *      custom-key = "some value"
     *   }
     * }
     * 
* * @param config Config at the point of a single handler configuration * @param defaults Default values to copy * @return an instance configured from the config (using defaults from defaults parameter for missing values) */ static GrpcSecurityHandler create(Config config, GrpcSecurityHandler defaults) { Builder builder = builder(defaults); config.get(KEY_ROLES_ALLOWED).asList(String.class) .ifPresentOrElse(builder::rolesAllowed, () -> defaults.rolesAllowed.ifPresent(builder::rolesAllowed)); if (config.exists()) { builder.config(config); } config.get(KEY_AUTHENTICATOR).asString().or(() -> defaults.explicitAuthenticator) .ifPresent(builder::authenticator); config.get(KEY_AUTHORIZER).asString().or(() -> defaults.explicitAuthorizer) .ifPresent(builder::authorizer); config.get(KEY_AUTHENTICATE).as(Boolean.class).or(() -> defaults.authenticate) .ifPresent(builder::authenticate); config.get(KEY_AUTHENTICATION_OPTIONAL).as(Boolean.class) .or(() -> defaults.authenticationOptional) .ifPresent(builder::authenticationOptional); config.get(KEY_AUDIT).as(Boolean.class).or(() -> defaults.audited) .ifPresent(builder::audit); config.get(KEY_AUTHORIZE).as(Boolean.class).or(() -> defaults.authorize) .ifPresent(builder::authorize); config.get(KEY_AUDIT_EVENT_TYPE).asString().or(() -> defaults.auditEventType) .ifPresent(builder::auditEventType); config.get(KEY_AUDIT_MESSAGE_FORMAT).asString().or(() -> defaults.auditMessageFormat) .ifPresent(builder::auditMessageFormat); // now resolve implicit behavior // roles allowed implies atn and atz if (config.get(KEY_ROLES_ALLOWED).exists()) { // we have roles allowed defined if (!config.get(KEY_AUTHENTICATE).exists()) { builder.authenticate(true); } if (!config.get(KEY_AUTHORIZE).exists()) { builder.authorize(true); } } // optional atn implies atn config.get(KEY_AUTHENTICATION_OPTIONAL).as(Boolean.class).ifPresent(aBoolean -> { if (aBoolean) { if (!config.get(KEY_AUTHENTICATE).exists()) { builder.authenticate(true); } } }); // explicit atn provider implies atn config.get(KEY_AUTHENTICATOR).asString().ifPresent(value -> { if (!config.get(KEY_AUTHENTICATE).exists()) { builder.authenticate(true); } }); // explicit atz provider implies atz config.get(KEY_AUTHORIZER).asString().ifPresent(value -> { if (!config.get(KEY_AUTHORIZE).exists()) { builder.authorize(true); } }); return builder.build(); } private static void configure(Config config, String key, Optional defaultValue, Consumer builderMethod, Class clazz) { config.get(key).as(clazz).or(() -> defaultValue).ifPresent(builderMethod); } static GrpcSecurityHandler create() { // constant is OK, object is immutable return DEFAULT_INSTANCE; } private static Builder builder() { return new Builder(); } private static Builder builder(GrpcSecurityHandler toCopy) { return new Builder().configureFrom(toCopy); } /** * Modifies a {@link io.helidon.grpc.server.ServiceDescriptor.Rules} to add this {@link GrpcSecurityHandler}. * * @param rules the {@link io.helidon.grpc.server.ServiceDescriptor.Rules} to modify */ @Override public void configure(ServiceDescriptor.Rules rules) { rules.addContextValue(GrpcSecurity.GRPC_SECURITY_HANDLER, this); } @Override public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { Context context = Context.current().withValue(GrpcSecurity.GRPC_SECURITY_HANDLER, this); return Contexts.interceptCall(context, call, headers, next); } /** * Perform security checks. * * @param call the current gRPC call to check * @param headers the call headers * @param next the next handler in the chain * @param the request type * @param the response type * * @return listener for processing incoming messages for {@code call}, never {@code null} */ ServerCall.Listener handleSecurity(ServerCall call, Metadata headers, ServerCallHandler next) { SecurityContext securityContext = GrpcSecurity.SECURITY_CONTEXT.get(); if (securityContext == null) { call.close(Status.FAILED_PRECONDITION .withDescription("Security context not present. Maybe you forgot to " + "GrpcRouting.builder().intercept(GrpcSecurity.create" + "(security))..."), new Metadata()); return new EmptyListener<>(); } if (combined) { return processSecurity(securityContext, call, headers, next); } else { // the following condition may be met for multiple threads - and we don't really care // as the result is exactly the same in all cases and doesn't have side effects if (null == combinedHandler.get()) { // we may have a default handler configured GrpcSecurityHandler defaultHandler = GrpcSecurity.GRPC_SECURITY_HANDLER.get(); if (defaultHandler == null) { defaultHandler = DEFAULT_INSTANCE; } // intentional same instance comparison, as I want to prevent endless loop //noinspection ObjectEquality if (defaultHandler == DEFAULT_INSTANCE) { combinedHandler.set(this); } else { combinedHandler.compareAndSet(null, builder(defaultHandler).configureFrom(this).combined().build()); } } return combinedHandler.get().processSecurity(securityContext, call, headers, next); } } private ServerCall.Listener processSecurity(SecurityContext securityContext, ServerCall call, Metadata headers, ServerCallHandler next) { SecurityTracing tracing = SecurityTracing.get(); tracing.securityContext(securityContext); securityContext.endpointConfig(securityContext.endpointConfig() .derive() .configMap(configMap) .customObjects(customObjects.orElse(new ClassToInstanceStore<>())) .build()); CompletionStage stage = processAuthentication(call, headers, securityContext, tracing.atnTracing()) .thenCompose(atnResult -> { if (atnResult.proceed) { // authentication was OK or disabled, we should continue return processAuthorization(securityContext, tracing.atzTracing()); } else { // authentication told us to stop processing return CompletableFuture.completedFuture(AtxResult.STOP); } }) .thenApply(atzResult -> { if (atzResult.proceed) { // authorization was OK, we can continue processing tracing.logProceed(); tracing.finish(); return true; } else { tracing.logDeny(); tracing.finish(); return false; } }); ServerCall.Listener listener; CallWrapper callWrapper = new CallWrapper<>(call); try { boolean proceed = stage.toCompletableFuture().get(); if (proceed) { listener = next.startCall(callWrapper, headers); } else { callWrapper.close(Status.PERMISSION_DENIED, new Metadata()); listener = new EmptyListener<>(); } } catch (Throwable throwable) { tracing.error(throwable); LOGGER.log(Level.SEVERE, "Unexpected exception during security processing", throwable); callWrapper.close(Status.INTERNAL, new Metadata()); listener = new EmptyListener<>(); } return new AuditingListener<>(listener, callWrapper, headers, securityContext); } private void processAudit(ServerCall call, Metadata headers, SecurityContext securityContext, Status status) { // make sure we actually should audit if (!audited.orElse(true)) { // explicitly disabled return; } AuditEvent.AuditSeverity severity = status.isOk() ? AuditEvent.AuditSeverity.SUCCESS : AuditEvent.AuditSeverity.FAILURE; SecurityAuditEvent auditEvent = SecurityAuditEvent .audit(severity, auditEventType.orElse(DEFAULT_AUDIT_EVENT_TYPE), auditMessageFormat.orElse(DEFAULT_AUDIT_MESSAGE_FORMAT)) .addParam(plain("method", call.getMethodDescriptor().getFullMethodName())) .addParam(plain("status", status.getCode())) .addParam(plain("subject", securityContext.user().orElse(SecurityContext.ANONYMOUS))) .addParam(plain("transport", "grpc")) .addParam(plain("resourceType", "grpc")); securityContext.service().ifPresent(svc -> auditEvent.addParam(plain("service", svc.toString()))); securityContext.audit(auditEvent); } private CompletionStage processAuthentication(ServerCall call, Metadata headers, SecurityContext securityContext, AtnTracing atnTracing) { if (!authenticate.orElse(false)) { return CompletableFuture.completedFuture(AtxResult.PROCEED); } CompletableFuture future = new CompletableFuture<>(); SecurityClientBuilder clientBuilder = securityContext.atnClientBuilder(); configureSecurityRequest(clientBuilder, atnTracing.findParent().orElse(null), atnTracing.findParentSpan().orElse(null)); clientBuilder.explicitProvider(explicitAuthenticator.orElse(null)).submit().thenAccept(response -> { switch (response.status()) { case SUCCESS: //everything is fine, we can continue with processing break; case FAILURE_FINISH: if (atnFinishFailure(future)) { atnSpanFinish(atnTracing, response); return; } break; case SUCCESS_FINISH: atnFinish(future); atnSpanFinish(atnTracing, response); return; case ABSTAIN: case FAILURE: if (atnAbstainFailure(future)) { atnSpanFinish(atnTracing, response); return; } break; default: Exception e = new SecurityException("Invalid SecurityStatus returned: " + response.status()); future.completeExceptionally(e); atnTracing.error(e); return; } atnSpanFinish(atnTracing, response); future.complete(new AtxResult(clientBuilder.buildRequest())); }).exceptionally(throwable -> { atnTracing.error(throwable); future.completeExceptionally(throwable); return null; }); return future; } private void atnSpanFinish(AtnTracing atnTracing, AuthenticationResponse response) { response.user().ifPresent(atnTracing::logUser); response.service().ifPresent(atnTracing::logService); atnTracing.logStatus(response.status()); atnTracing.finish(); } private boolean atnAbstainFailure(CompletableFuture future) { if (authenticationOptional.orElse(false)) { LOGGER.finest("Authentication failed, but was optional, so assuming anonymous"); return false; } future.complete(AtxResult.STOP); return true; } private boolean atnFinishFailure(CompletableFuture future) { if (authenticationOptional.orElse(false)) { LOGGER.finest("Authentication failed, but was optional, so assuming anonymous"); return false; } else { future.complete(AtxResult.STOP); return true; } } private void atnFinish(CompletableFuture future) { future.complete(AtxResult.STOP); } private void configureSecurityRequest(SecurityRequestBuilder> request, SpanContext parentSpanContext, Span parentSpan) { request.optional(authenticationOptional.orElse(false)) .tracingSpan(parentSpanContext) .tracingSpan(parentSpan); } private CompletionStage processAuthorization( SecurityContext context, AtzTracing atzTracing) { CompletableFuture future = new CompletableFuture<>(); if (!authorize.orElse(false)) { future.complete(AtxResult.PROCEED); atzTracing.logStatus(SecurityResponse.SecurityStatus.ABSTAIN); atzTracing.finish(); return future; } Set rolesSet = rolesAllowed.orElse(Set.of()); if (!rolesSet.isEmpty()) { // first validate roles - RBAC is supported out of the box by security, no need to invoke provider if (explicitAuthorizer.isPresent()) { if (rolesSet.stream().noneMatch(role -> context.isUserInRole(role, explicitAuthorizer.get()))) { future.complete(AtxResult.STOP); atzTracing.finish(); return future; } } else { if (rolesSet.stream().noneMatch(context::isUserInRole)) { future.complete(AtxResult.STOP); atzTracing.finish(); return future; } } } SecurityClientBuilder client; client = context.atzClientBuilder(); configureSecurityRequest(client, atzTracing.findParent().orElse(null), atzTracing.findParentSpan().orElse(null)); client.explicitProvider(explicitAuthorizer.orElse(null)).submit().thenAccept(response -> { atzTracing.logStatus(response.status()); switch (response.status()) { case SUCCESS: //everything is fine, we can continue with processing break; case FAILURE_FINISH: case SUCCESS_FINISH: atzTracing.finish(); future.complete(AtxResult.STOP); return; case ABSTAIN: case FAILURE: atzTracing.finish(); future.complete(AtxResult.STOP); return; default: SecurityException e = new SecurityException("Invalid SecurityStatus returned: " + response.status()); atzTracing.error(e); future.completeExceptionally(e); return; } atzTracing.finish(); // everything was OK future.complete(AtxResult.PROCEED); }).exceptionally(throwable -> { atzTracing.error(throwable); future.completeExceptionally(throwable); return null; }); return future; } /** * Use a named authenticator (as supported by security - if not defined, default authenticator is used). * Will enable authentication. * * @param explicitAuthenticator name of authenticator as configured in {@link Security} * @return new handler instance with configuration of this instance updated with this method */ public GrpcSecurityHandler authenticator(String explicitAuthenticator) { return builder(this).authenticator(explicitAuthenticator).build(); } /** * Use a named authorizer (as supported by security - if not defined, default authorizer is used, if none defined, all is * permitted). * Will enable authorization. * * @param explicitAuthorizer name of authorizer as configured in {@link Security} * @return new handler instance with configuration of this instance updated with this method */ public GrpcSecurityHandler authorizer(String explicitAuthorizer) { return builder(this).authorizer(explicitAuthorizer).build(); } /** * An array of allowed roles for this path - must have a security provider supporting roles (either authentication * or authorization provider). * This method enables authentication and authorization (you can disable them again by calling * {@link GrpcSecurityHandler#skipAuthorization()} * and {@link #skipAuthentication()} if needed). * * @param roles if subject is any of these roles, allow access * @return new handler instance with configuration of this instance updated with this method */ public GrpcSecurityHandler rolesAllowed(String... roles) { return builder(this).rolesAllowed(roles).authorize(true).authenticate(true).build(); } /** * If called, authentication failure will not abort request and will continue as anonymous (authentication is not optional * by default). * Will enable authentication. * * @return new handler instance with configuration of this instance updated with this method */ public GrpcSecurityHandler authenticationOptional() { return builder(this).authenticationOptional(true).build(); } /** * If called, request will go through authentication process - (authentication is disabled by default - it may be enabled * as a side effect of other methods, such as {@link #rolesAllowed(String...)}. * * @return new handler instance with configuration of this instance updated with this method */ public GrpcSecurityHandler authenticate() { return builder(this).authenticate(true).build(); } /** * If called, request will NOT go through authentication process. Use this when another method implies authentication * (such as {@link #rolesAllowed(String...)}) and yet it is not desired (e.g. everything is handled by authorization). * * @return new handler instance with configuration of this instance updated with this method */ public GrpcSecurityHandler skipAuthentication() { return builder(this).authenticate(false).build(); } /** * Register a custom object for security request(s). * This creates a hard dependency on a specific security provider, so use with care. * * @param object An object expected by security provider * @return new handler instance with configuration of this instance updated with this method */ public GrpcSecurityHandler customObject(Object object) { return builder(this).customObject(object).build(); } /** * Override for event-type, defaults to {@value #DEFAULT_AUDIT_EVENT_TYPE}. * * @param eventType audit event type to use * @return new handler instance with configuration of this instance updated with this method */ public GrpcSecurityHandler auditEventType(String eventType) { return builder(this).auditEventType(eventType).build(); } /** * Override for audit message format, defaults to {@value #DEFAULT_AUDIT_MESSAGE_FORMAT}. * * @param messageFormat audit message format to use * @return new handler instance with configuration of this instance updated with this method */ public GrpcSecurityHandler auditMessageFormat(String messageFormat) { return builder(this).auditMessageFormat(messageFormat).build(); } /** * If called, request will go through authorization process - (authorization is disabled by default - it may be enabled * as a side effect of other methods, such as {@link #rolesAllowed(String...)}. * * @return new handler instance with configuration of this instance updated with this method */ public GrpcSecurityHandler authorize() { return builder(this).authorize(true).build(); } /** * Skip authorization for this route. * Use this when authorization is implied by another method on this class (e.g. {@link #rolesAllowed(String...)} and * you want to explicitly forbid it. * * @return new handler instance with configuration of this instance updated with this method */ public GrpcSecurityHandler skipAuthorization() { return builder(this).authorize(false).build(); } /** * Audit this request for any method. Request is audited with event type {@link #DEFAULT_AUDIT_EVENT_TYPE}. *

* By default audit is enabled as follows (based on HTTP methods): *

    *
  • GET, HEAD - not audited
  • *
  • PUT, POST, DELETE - audited
  • *
  • any other method (e.g. custom methods) - audited
  • *
* Calling this method will override the default setting and audit any method this handler is registered for. * * @return new handler instance with configuration of this instance updated with this method */ public GrpcSecurityHandler audit() { return builder(this).audit(true).build(); } /** * Disable auditing of this request. Will override defaults and disable auditing for all methods this handler is registered * for. *

* By default audit is enabled as follows (based on HTTP methods): *

    *
  • GET, HEAD - not audited
  • *
  • PUT, POST, DELETE - audited
  • *
  • any other method (e.g. custom methods) - audited
  • *
* * @return new handler instance with configuration of this instance updated with this method */ public GrpcSecurityHandler skipAudit() { return builder(this).audit(false).build(); } /** * Obtain the roles allowed for this {@link GrpcSecurityHandler}. * * @return an {@link Optional} containing the the roles allowed for * this {@link GrpcSecurityHandler} if any have been configured */ Optional> getRolesAllowed() { return rolesAllowed.map(Collections::unmodifiableSet); } /** * Obtain the explicit authenticator for this {@link GrpcSecurityHandler}. * * @return an {@link Optional} containing the the explicit authenticator for * this {@link GrpcSecurityHandler} if any have been configured */ Optional getExplicitAuthenticator() { return explicitAuthenticator; } /** * Obtain the explicit authorizer for this {@link GrpcSecurityHandler}. * * @return an {@link Optional} containing the the explicit authorizer for * this {@link GrpcSecurityHandler} if any have been configured */ Optional getExplicitAuthorizer() { return explicitAuthorizer; } /** * Obtain whether this {@link GrpcSecurityHandler} performs authentication. * * @return an {@link Optional} containing {@code true} if this * {@link GrpcSecurityHandler} performs authentication */ Optional isAuthenticate() { return authenticate; } /** * Obtain whether this {@link GrpcSecurityHandler} allows anonymous access. * * @return an {@link Optional} containing {@code true} if this * {@link GrpcSecurityHandler} allows anonymous access */ Optional isAuthenticationOptional() { return authenticationOptional; } /** * Obtain whether this {@link GrpcSecurityHandler} performs authorization. * * @return an {@link Optional} containing {@code true} if this * {@link GrpcSecurityHandler} performs authorization */ Optional isAuthorize() { return authorize; } /** * Obtain whether this {@link GrpcSecurityHandler} audits security operations. * * @return an {@link Optional} containing {@code true} if this * {@link GrpcSecurityHandler} audits security operations */ Optional isAudited() { return audited; } /** * Obtain the audit event type override. * * @return an {@link Optional} containing the audit event type * override if one has been set */ Optional getAuditEventType() { return auditEventType; } /** * Obtain the audit message format override. * * @return an {@link Optional} containing the audit message format * override if one has been set */ Optional getAuditMessageFormat() { return auditMessageFormat; } private static final class AtxResult { private static final AtxResult PROCEED = new AtxResult(true); private static final AtxResult STOP = new AtxResult(false); private final boolean proceed; private AtxResult(boolean proceed) { this.proceed = proceed; } @SuppressWarnings("unused") private AtxResult(SecurityRequest ignored) { this.proceed = true; } } // WARNING: builder methods must not have side-effects, as they are used to build instance from configuration // if you want side effects, use methods on GrpcSecurityInterceptor private static final class Builder implements io.helidon.common.Builder { private Optional> rolesAllowed = Optional.empty(); private Optional> customObjects = Optional.empty(); private Optional config = Optional.empty(); private Optional explicitAuthenticator = Optional.empty(); private Optional explicitAuthorizer = Optional.empty(); private Optional authenticate = Optional.empty(); private Optional authenticationOptional = Optional.empty(); private Optional authorize = Optional.empty(); private Optional audited = Optional.empty(); private Optional auditEventType = Optional.empty(); private Optional auditMessageFormat = Optional.empty(); private boolean combined; private Builder() { } @Override public GrpcSecurityHandler build() { return new GrpcSecurityHandler(this); } private Builder combined() { this.combined = true; return this; } // add to this builder private Builder configureFrom(GrpcSecurityHandler handler) { handler.rolesAllowed.ifPresent(this::rolesAllowed); handler.customObjects.ifPresent(this::customObjects); handler.config.ifPresent(this::config); handler.explicitAuthenticator.ifPresent(this::authenticator); handler.explicitAuthorizer.ifPresent(this::authorizer); handler.authenticate.ifPresent(this::authenticate); handler.authenticationOptional.ifPresent(this::authenticationOptional); handler.audited.ifPresent(this::audit); handler.auditEventType.ifPresent(this::auditEventType); handler.auditMessageFormat.ifPresent(this::auditMessageFormat); handler.authorize.ifPresent(this::authorize); return this; } private Builder customObjects(ClassToInstanceStore store) { customObjects .ifPresentOrElse(myStore -> myStore.putAll(store), () -> { ClassToInstanceStore ctis = new ClassToInstanceStore<>(); ctis.putAll(store); this.customObjects = Optional.of(ctis); }); return this; } /** * Use a named authenticator (as supported by security - if not defined, default authenticator is used). * * @param explicitAuthenticator name of authenticator as configured in {@link Security} * @return updated builder instance */ Builder authenticator(String explicitAuthenticator) { this.explicitAuthenticator = Optional.of(explicitAuthenticator); return this; } /** * Use a named authorizer (as supported by security - if not defined, default authorizer is used, if none defined, all is * permitted). * * @param explicitAuthorizer name of authorizer as configured in {@link Security} * @return updated builder instance */ Builder authorizer(String explicitAuthorizer) { this.explicitAuthorizer = Optional.of(explicitAuthorizer); return this; } /** * An array of allowed roles for this path - must have a security provider supporting roles. * * @param roles if subject is any of these roles, allow access * @return updated builder instance */ Builder rolesAllowed(String... roles) { return rolesAllowed(Arrays.asList(roles)); } private Builder config(Config config) { this.config = Optional.of(config); return this; } /** * If called, authentication failure will not abort request and will continue as anonymous (defaults to false). * * @param isOptional whether authn is optional * @return updated builder instance */ Builder authenticationOptional(boolean isOptional) { this.authenticationOptional = Optional.of(isOptional); return this; } /** * If called, request will go through authentication process - defaults to false (even if authorize is true). * * @param authenticate whether to authenticate or not * @return updated builder instance */ Builder authenticate(boolean authenticate) { this.authenticate = Optional.of(authenticate); return this; } /** * Register a custom object for security request(s). * This creates a hard dependency on a specific security provider, so use with care. * * @param object An object expected by security provider * @return updated builder instance */ Builder customObject(Object object) { customObjects .ifPresentOrElse(store -> store.putInstance(object), () -> { ClassToInstanceStore ctis = new ClassToInstanceStore<>(); ctis.putInstance(object); customObjects = Optional.of(ctis); }); return this; } /** * Override for event-type, defaults to {@value #DEFAULT_AUDIT_EVENT_TYPE}. * * @param eventType audit event type to use * @return updated builder instance */ Builder auditEventType(String eventType) { this.auditEventType = Optional.of(eventType); return this; } /** * Override for audit message format, defaults to {@value #DEFAULT_AUDIT_MESSAGE_FORMAT}. * * @param messageFormat audit message format to use * @return updated builder instance */ Builder auditMessageFormat(String messageFormat) { this.auditMessageFormat = Optional.of(messageFormat); return this; } /** * Enable authorization for this route. * * @param authorize whether to authorize * @return updated builder instance */ Builder authorize(boolean authorize) { this.authorize = Optional.of(authorize); return this; } /** * Whether to audit this request - defaults to false, if enabled, request is audited with event type "request". * * @return updated builder instance */ Builder audit(boolean audited) { this.audited = Optional.of(audited); return this; } Builder rolesAllowed(Collection roles) { rolesAllowed.ifPresentOrElse(strings -> strings.addAll(roles), () -> { Set newRoles = new HashSet<>(roles); rolesAllowed = Optional.of(newRoles); }); return this; } } /** * An empty {@link ServerCall.Listener} used to terminate a call if * authentication fails. * * @param the type of the call */ static class EmptyListener extends ServerCall.Listener { } /** * A logging {@link ServerCall.Listener}. * * @param the request type */ private class AuditingListener extends ForwardingServerCallListener.SimpleForwardingServerCallListener { private CallWrapper call; private Metadata headers; private SecurityContext securityContext; private AuditingListener(ServerCall.Listener delegate, CallWrapper call, Metadata headers, SecurityContext securityContext) { super(delegate); this.call = call; this.headers = headers; this.securityContext = securityContext; } @Override public void onCancel() { processAudit(call, headers, securityContext, call.getCloseStatus()); } @Override public void onComplete() { processAudit(call, headers, securityContext, call.getCloseStatus()); } } private class CallWrapper extends ForwardingServerCall.SimpleForwardingServerCall { private Status closeStatus; private CallWrapper(ServerCall delegate) { super(delegate); } @Override public void close(Status status, Metadata trailers) { closeStatus = status; super.close(status, trailers); } Status getCloseStatus() { return closeStatus; } } }