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

io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider Maven / Gradle / Ivy

There is a newer version: 4.1.6
Show newest version
/*
 * Copyright (c) 2019, 2021 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.security.providers.idcs.mapper;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;

import io.helidon.config.Config;
import io.helidon.security.AuthenticationResponse;
import io.helidon.security.Grant;
import io.helidon.security.ProviderRequest;
import io.helidon.security.SecurityException;
import io.helidon.security.Subject;
import io.helidon.security.integration.common.RoleMapTracing;
import io.helidon.security.integration.common.SecurityTracing;
import io.helidon.security.providers.common.EvictableCache;
import io.helidon.security.providers.oidc.common.OidcConfig;
import io.helidon.security.spi.SecurityProvider;
import io.helidon.security.util.TokenHandler;

import jakarta.json.Json;
import jakarta.json.JsonArrayBuilder;
import jakarta.json.JsonBuilderFactory;
import jakarta.json.JsonObjectBuilder;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.Response;

/**
 * {@link io.helidon.security.spi.SubjectMappingProvider} to obtain roles from IDCS server for a user.
 * Supports multi tenancy in IDCS.
 *
 * @deprecated use {@link io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperRxProvider} instead
 */
@Deprecated(forRemoval = true, since = "2.4.0")
public class IdcsMtRoleMapperProvider extends IdcsRoleMapperProviderBase {
    /**
     * Name of the header containing the IDCS tenant. This is the default used, can be overriden
     * in builder by {@link IdcsMtRoleMapperProvider.Builder#idcsTenantTokenHandler(io.helidon.security.util.TokenHandler)}
     */
    protected static final String IDCS_TENANT_HEADER = "X-USER-IDENTITY-SERVICE-GUID";
    /**
     * Name of the header containing the IDCS app. This is the default used, can be overriden
     * in builder by {@link IdcsMtRoleMapperProvider.Builder#idcsAppNameTokenHandler(io.helidon.security.util.TokenHandler)}
     */
    protected static final String IDCS_APP_HEADER = "X-RESOURCE-SERVICE-INSTANCE-IDENTITY-APPNAME";

    private static final Logger LOGGER = Logger
            .getLogger(IdcsMtRoleMapperProvider.class.getName());
    private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());

    private final TokenHandler idcsTenantTokenHandler;
    private final TokenHandler idcsAppNameTokenHandler;
    private final EvictableCache> cache;
    private final MultitenancyEndpoints multitenantEndpoints;
    private final ConcurrentHashMap tokenCache = new ConcurrentHashMap<>();

    /**
     * Configure instance from any descendant of
     * {@link io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider.Builder}.
     *
     * @param builder containing the required configuration
     */
    protected IdcsMtRoleMapperProvider(Builder builder) {
        super(builder);

        this.idcsTenantTokenHandler = builder.idcsTenantTokenHandler;
        this.idcsAppNameTokenHandler = builder.idcsAppNameTokenHandler;
        this.cache = builder.cache;
        if (null == builder.multitentantEndpoints) {
            this.multitenantEndpoints = new DefaultMultitenancyEndpoints(builder.oidcConfig());
        } else {
            this.multitenantEndpoints = builder.multitentantEndpoints;
        }
    }

    /**
     * Creates a new builder to build instances of this class.
     *
     * @return a new fluent API builder.
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Creates an instance from configuration.
     * 

* Expects: *

    *
  • oidc-config to load an instance of {@link io.helidon.security.providers.oidc.common.OidcConfig}
  • *
  • cache-config (optional) to load an instance of {@link io.helidon.security.providers.common.EvictableCache} for role * caching
  • *
* * @param config configuration of this provider * @return a new instance configured from config */ public static SecurityProvider create(Config config) { return builder().config(config).build(); } /** * Enhance the subject with appropriate roles from IDCS. * * @param subject subject of the user (never null) * @param request provider request * @param previousResponse authenticated response (never null) * @return enhanced subject */ protected Subject enhance(Subject subject, ProviderRequest request, AuthenticationResponse previousResponse) { Optional maybeIdcsMtContext = extractIdcsMtContext(subject, request); if (!maybeIdcsMtContext.isPresent()) { LOGGER.finest(() -> "Missing multitenant information IDCS CONTEXT: " + maybeIdcsMtContext + ", subject: " + subject); return subject; } IdcsMtContext idcsMtContext = maybeIdcsMtContext.get(); String name = subject.principal().getName(); MtCacheKey cacheKey = new MtCacheKey(idcsMtContext, name); // double cache List serverGrants = cache.computeValue(cacheKey, () -> computeGrants(idcsMtContext.tenantId(), idcsMtContext.appId(), subject)) .orElseGet(List::of); List grants = new LinkedList<>(serverGrants); // additional grants may not be cached (leave this decision to overriding class) addAdditionalGrants(idcsMtContext.tenantId(), idcsMtContext.appId(), subject) .map(grants::addAll); return buildSubject(subject, grants); } private Optional> computeGrants(String idcsTenantId, String idcsAppName, Subject subject) { return getGrantsFromServer(idcsTenantId, idcsAppName, subject) .map(grants -> Collections.unmodifiableList(new LinkedList<>(grants))); } /** * Extract IDCS multitenancy context form the the request. * *

By default, the context is extracted from the headers using token handlers for * {@link Builder#idcsTenantTokenHandler(TokenHandler) tenant} and * {@link Builder#idcsAppNameTokenHandler(TokenHandler) app}. * @param subject Subject that is being mapped * @param request ProviderRequest context that is being mapped. * @return Optional with the context, empty if the context is not present in the request. */ protected Optional extractIdcsMtContext(Subject subject, ProviderRequest request) { return idcsTenantTokenHandler.extractToken(request.env().headers()) .flatMap(tenant -> idcsAppNameTokenHandler.extractToken(request.env().headers()) .map(app -> new IdcsMtContext(tenant, app))); } /** * Extension point to add additional grants to the subject being created. * * @param idcsTenantId IDCS tenant id * @param idcsAppName IDCS application name * @param subject subject of the user/service * @return list with new grants to add to the enhanced subject */ protected Optional> addAdditionalGrants(String idcsTenantId, String idcsAppName, Subject subject) { return Optional.empty(); } /** * Get grants from IDCS server. The result is cached. * * @param idcsTenantId ID of the IDCS tenant * @param idcsAppName Name of IDCS application * @param subject subject to get grants for * @return optional list of grants from server */ protected Optional> getGrantsFromServer(String idcsTenantId, String idcsAppName, Subject subject) { String subjectName = subject.principal().getName(); String subjectType = (String) subject.principal().abacAttribute("sub_type").orElse(defaultIdcsSubjectType()); RoleMapTracing tracing = SecurityTracing.get().roleMapTracing("idcs"); return getAppToken(idcsTenantId, tracing).flatMap(appToken -> { JsonObjectBuilder requestBuilder = JSON.createObjectBuilder() .add("mappingAttributeValue", subjectName) .add("subjectType", subjectType) .add("appName", idcsAppName) .add("includeMemberships", true); JsonArrayBuilder arrayBuilder = JSON.createArrayBuilder(); arrayBuilder.add("urn:ietf:params:scim:schemas:oracle:idcs:Asserter"); requestBuilder.add("schemas", arrayBuilder); Invocation.Builder reqBuilder = multitenantEndpoints.assertEndpoint(idcsTenantId).request(); tracing.findParent() .ifPresent(spanContext -> reqBuilder.property(PARENT_CONTEXT_CLIENT_PROPERTY, spanContext)); Response groupResponse = reqBuilder .header("Authorization", "Bearer " + appToken) .post(Entity.json(requestBuilder.build())); return processServerResponse(groupResponse, subjectName); }); } /** * Gets token from cache or from server. * * @param idcsTenantId id of tenant * @param tracing Role mapping tracing instance to correctly trace outbound calls * @return the token to be used to authenticate this service */ protected Optional getAppToken(String idcsTenantId, RoleMapTracing tracing) { // if cached and valid, use the cached token return tokenCache.computeIfAbsent(idcsTenantId, key -> new AppToken(multitenantEndpoints.tokenEndpoint(idcsTenantId))) .getToken(tracing); } /** * Get the {@link io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider.MultitenancyEndpoints} used * to get assertion and token endpoints of a multitenant IDCS. * * @return endpoints to use by this implementation */ protected MultitenancyEndpoints multitenancyEndpoints() { return multitenantEndpoints; } /** * Fluent API builder for {@link io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider}. * * @param type of a descendant of this builder */ public static class Builder> extends IdcsRoleMapperProviderBase.Builder> implements io.helidon.common.Builder, IdcsMtRoleMapperProvider> { private TokenHandler idcsAppNameTokenHandler = TokenHandler.forHeader(IDCS_APP_HEADER); private TokenHandler idcsTenantTokenHandler = TokenHandler.forHeader(IDCS_TENANT_HEADER); private MultitenancyEndpoints multitentantEndpoints; private EvictableCache> cache; @SuppressWarnings("unchecked") private B me = (B) this; /** * Default constructor. */ protected Builder() { } @Override public IdcsMtRoleMapperProvider build() { if (null == cache) { cache = EvictableCache.create(); } return new IdcsMtRoleMapperProvider(this); } @Override public B config(Config config) { super.config(config); config.get("cache-config").as(EvictableCache::>create).ifPresent(this::cache); config.get("idcs-tenant-handler").as(TokenHandler::create).ifPresent(this::idcsTenantTokenHandler); config.get("idcs-app-name-handler").as(TokenHandler::create).ifPresent(this::idcsAppNameTokenHandler); return me; } /** * Configure token handler for IDCS Application name. * By default the header {@value IdcsMtRoleMapperProvider#IDCS_APP_HEADER} is used. * * @param idcsAppNameTokenHandler new token handler to extract IDCS application name * @return updated builder instance */ public B idcsAppNameTokenHandler(TokenHandler idcsAppNameTokenHandler) { this.idcsAppNameTokenHandler = idcsAppNameTokenHandler; return me; } /** * Configure token handler for IDCS Tenant ID. * By default the header {@value IdcsMtRoleMapperProvider#IDCS_TENANT_HEADER} is used. * * @param idcsTenantTokenHandler new token handler to extract IDCS tenant ID * @return updated builder instance */ public B idcsTenantTokenHandler(TokenHandler idcsTenantTokenHandler) { this.idcsTenantTokenHandler = idcsTenantTokenHandler; return me; } /** * Replace default endpoint provider in multitenant IDCS setup. * * @param endpoints endpoints to retrieve tenant specific token and asserter endpoints * @return updated builder instance */ public B multitenantEndpoints(MultitenancyEndpoints endpoints) { this.multitentantEndpoints = endpoints; return me; } /** * Use explicit {@link io.helidon.security.providers.common.EvictableCache} for role caching. * * @param roleCache cache to use * @return updated builder instance */ public B cache(EvictableCache> roleCache) { this.cache = roleCache; return me; } } /** * Multitenant endpoints for accessing IDCS services. */ public interface MultitenancyEndpoints { /** * The tenant id of the infrastructure tenant. * * @return id of the tenant */ String idcsInfraTenantId(); /** * Asserter endpoint for a specific tenant. * * @param tenantId id of tenant to get the endpoint for * @return web target for the tenant */ WebTarget assertEndpoint(String tenantId); /** * Token endpoint for a specific tenant. * * @param tenantId id of tenant to get the endpoint for * @return web target for the tenant */ WebTarget tokenEndpoint(String tenantId); } /** * Default implementation of the * {@link io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider.MultitenancyEndpoints}. * Caches the endpoints per tenant. */ protected static class DefaultMultitenancyEndpoints implements MultitenancyEndpoints { private final String idcsInfraTenantId; private final String idcsInfraHostName; private final String urlPrefix; private final String assertUrlSuffix; private final String tokenUrlSuffix; private final Client appClient; private final Client generalClient; // we want to cache endpoints for each tenant private final ConcurrentHashMap assertEndpointCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap tokenEndpointCache = new ConcurrentHashMap<>(); /** * Creates endpoints from provided OIDC configuration using default URIs. *
*

    *
  • For Asserter endpoint: {@code /admin/v1/Asserter}
  • *
  • For Token endpoint: {@code /oauth2/v1/token?IDCS_CLIENT_TENANT=}
  • *
* * @param config IDCS base configuration */ protected DefaultMultitenancyEndpoints(OidcConfig config) { idcsInfraHostName = config.identityUri().getHost(); int index = idcsInfraHostName.indexOf('.'); if (index == -1) { throw new SecurityException("Configuration of multitenant IDCS is invalid. The identity host name should be " + "'tenant-id.identityServer' but is " + idcsInfraHostName); } idcsInfraTenantId = idcsInfraHostName.substring(0, index); urlPrefix = config.identityUri().getScheme() + "://"; this.assertUrlSuffix = "/admin/v1/Asserter"; this.tokenUrlSuffix = "/oauth2/v1/token?IDCS_CLIENT_TENANT="; this.generalClient = config.generalClient(); this.appClient = config.appClient(); } @Override public String idcsInfraTenantId() { return idcsInfraTenantId; } @Override public WebTarget assertEndpoint(String tenantId) { return assertEndpointCache.computeIfAbsent(tenantId, theKey -> { String url = urlPrefix + idcsInfraHostName.replaceAll(idcsInfraTenantId, tenantId) + assertUrlSuffix; LOGGER.finest(() -> "MT Asserter endpoint: " + url); return generalClient.target(url); }); } @Override public WebTarget tokenEndpoint(String tenantId) { return tokenEndpointCache.computeIfAbsent(tenantId, theKey -> { String url = urlPrefix + idcsInfraHostName.replaceAll(idcsInfraTenantId, tenantId) + tokenUrlSuffix + idcsInfraTenantId; LOGGER.finest(() -> "MT Token endpoint: " + url); return appClient.target(url); }); } } /** * Cache key for multitenant environments. * Used when caching user grants. * Suitable for use in maps and sets. */ public static class MtCacheKey { private final IdcsMtContext idcsMtContext; private final String username; /** * New (immutable) cache key. * * @param idcsTenantId IDCS tenant ID * @param idcsAppName IDCS application name * @param username username */ protected MtCacheKey(String idcsTenantId, String idcsAppName, String username) { this(new IdcsMtContext( Objects.requireNonNull(idcsTenantId, "IDCS Tenant id is mandatory"), Objects.requireNonNull(idcsAppName, "IDCS App id is mandatory")), username); } /** * New (immutable) cache key. * * @param idcsMtContext IDCS multitenancy context * @param username username */ protected MtCacheKey(IdcsMtContext idcsMtContext, String username) { Objects.requireNonNull(idcsMtContext, "IDCS Multitenancy Context is mandatory"); Objects.requireNonNull(username, "username is mandatory"); this.idcsMtContext = idcsMtContext; this.username = username; } /** * IDCS Tenant ID. * * @return tenant id of the cache record */ public String idcsTenantId() { return idcsMtContext.tenantId(); } /** * Username. * * @return username of the cache record */ public String username() { return username; } /** * IDCS Application ID. * * @return application id of the cache record */ public String idcsAppName() { return idcsMtContext.appId(); } /** * IDCS Multitenancy context. * * @return IDCS multitenancy context of the cache record */ public IdcsMtContext idcsMtContext() { return idcsMtContext; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof MtCacheKey)) { return false; } MtCacheKey cacheKey = (MtCacheKey) o; return idcsMtContext.equals(cacheKey.idcsMtContext) && username.equals(cacheKey.username); } @Override public int hashCode() { return Objects.hash(idcsMtContext, username); } } }