com.google.api.tools.framework.importers.swagger.AuthBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.google.api.tools.framework.importers.swagger.AuthBuilder.java

Source

/*
 * Copyright (C) 2016 Google Inc.
 *
 * 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 com.google.api.tools.framework.importers.swagger;

import com.google.api.AuthProvider;
import com.google.api.AuthRequirement;
import com.google.api.Authentication;
import com.google.api.AuthenticationRule;
import com.google.api.Usage;
import com.google.api.UsageRule;
import com.google.api.tools.framework.model.Diag;
import com.google.api.tools.framework.model.DiagCollector;
import com.google.api.tools.framework.model.Location;
import com.google.api.tools.framework.model.SimpleLocation;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;

import io.swagger.models.Operation;
import io.swagger.models.SecurityRequirement;
import io.swagger.models.Swagger;
import io.swagger.models.auth.ApiKeyAuthDefinition;
import io.swagger.models.auth.In;
import io.swagger.models.auth.SecuritySchemeDefinition;

import java.lang.reflect.Type;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Class to create {@link AuthenticationRule} from swagger operations.
 */
final class AuthBuilder {
    private static final String JWKS_SWAGGER_EXTENSION = "x-jwks_uri";
    private static final String OAUTH_ISSUER_SWAGGER_EXTENSION = "x-issuer";
    private static final String SECURITY_REQUIREMENT_EXTENSION = "x-security";

    private final DiagCollector diagCollector;
    private final List<AuthenticationRule> authRules = Lists.newArrayList();
    private final List<UsageRule> usageRules = Lists.newArrayList();
    private final Set<String> apiKeyDefinitions = new LinkedHashSet<>();
    private boolean requiresApiKeyAtTopLevel = false;
    private final Map<String, AuthProvider> authProviders = Maps.newLinkedHashMap();
    private final String namespacePrefix;

    AuthBuilder(String namespace, DiagCollector diagCollector) {
        this.diagCollector = diagCollector;
        this.namespacePrefix = (namespace.isEmpty() || namespace.endsWith(".")) ? namespace : namespace + ".";
    }

    /**
     * Returns the generated {@link AuthenticationRule}s.
     */
    Authentication getAuthentication() {
        Authentication.Builder authenticationBuilder = Authentication.newBuilder();
        authenticationBuilder.addAllProviders(authProviders.values());
        authenticationBuilder.addAllRules(authRules);
        return authenticationBuilder.build();
    }

    Usage getUsage() {
        Usage.Builder usageBuilder = Usage.newBuilder();
        usageBuilder.addAllRules(usageRules);
        return usageBuilder.build();
    }

    /**
     * Returns whether vendor extension contains the particular extension.
     */
    private static <T> boolean hasVendorExtension(Map<String, Object> vendorExtensions, String extensionName,
            Class<T> clazz) {
        if (vendorExtensions != null) {
            Object extensionValue = vendorExtensions.get(extensionName);
            if (extensionValue != null && clazz.isInstance(extensionValue)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Creates {@link AuthProvider} from Swagger SecuritySchemeDefinition.
     */
    void addAuthProvider(String securitySchemaName, SecuritySchemeDefinition securitySchema) {
        if (securitySchema == null) {
            return;
        }
        if (securitySchema.getType().equalsIgnoreCase("oauth2")) {
            AuthProvider.Builder authProviderBuilder = AuthProvider.newBuilder();
            authProviderBuilder.setId(securitySchemaName);
            if (hasVendorExtension(securitySchema.getVendorExtensions(), OAUTH_ISSUER_SWAGGER_EXTENSION,
                    String.class)) {
                authProviderBuilder.setIssuer(
                        (String) securitySchema.getVendorExtensions().get(OAUTH_ISSUER_SWAGGER_EXTENSION));
            }
            if (hasVendorExtension(securitySchema.getVendorExtensions(), JWKS_SWAGGER_EXTENSION, String.class)) {
                authProviderBuilder
                        .setJwksUri((String) securitySchema.getVendorExtensions().get(JWKS_SWAGGER_EXTENSION));
            }
            authProviders.put(securitySchemaName, authProviderBuilder.build());
        } else if (securitySchema.getType().equalsIgnoreCase("apiKey")) {
            ApiKeyAuthDefinition apiKeyDef = (ApiKeyAuthDefinition) securitySchema;
            if (isValidApiKeyDefinition(apiKeyDef)) {
                apiKeyDefinitions.add(securitySchemaName);
            }
        } else {
            diagCollector.addDiag(Diag.warning(SimpleLocation.UNKNOWN,
                    "Security Schema '%s' is not supported. Only support schema are OAuth2", securitySchemaName));
        }
    }

    /**
     * Checks if the defined apiKey is valid or not. Only apiKey definition with name as 'key' and
     * 'in' as 'query' are allowed"
     */
    private boolean isValidApiKeyDefinition(ApiKeyAuthDefinition apiKeydef) {
        if (apiKeydef.getName().equalsIgnoreCase("key") || apiKeydef.getIn() == In.QUERY) {
            return true;
        } else {
            diagCollector.addDiag(Diag.warning(SimpleLocation.UNKNOWN,
                    "apiKey '%s' is ignored. Only apiKey with 'name' as 'key' and 'in' as 'query' are "
                            + "supported",
                    apiKeydef.getName()));
            return false;
        }
    }

    /**
     * Creates {@link AuthRequirement} from Swagger requirements per operations.
     */
    public void addAuthRule(Operation operation, String operationType, String swaggerPath) {
        addOAuthRules(operation, operationType, swaggerPath);
        addApiKeyRules(operation, operationType, swaggerPath);
    }

    private void addApiKeyRules(Operation operation, String operationType, String swaggerPath) {
        addUsageRulePerMethod(operation.getSecurity(), operationType, swaggerPath,
                namespacePrefix + NameConverter.operationIdToMethodName(operation.getOperationId()));
    }

    /**
     * Creates {@link AuthRequirement} from Swagger requirements per operations.
     */
    private void addOAuthRules(Operation operation, String operationType, String swaggerPath) {
        AuthenticationRule.Builder builder = AuthenticationRule.newBuilder();
        if (convertSecurityRequirementExtension(operation.getVendorExtensions(), builder,
                SwaggerToService.createOperationLocation(operationType, swaggerPath))) {
            builder.setSelector(
                    namespacePrefix + NameConverter.operationIdToMethodName(operation.getOperationId()));
            authRules.add(builder.build());
        }
    }

    /**
     * Adds auth security requirement for the entire service.
     */
    public void addSecurityRequirementExtensionForEntireService(Swagger swagger) {
        AuthenticationRule.Builder builder = AuthenticationRule.newBuilder();
        if (convertSecurityRequirementExtension(swagger.getVendorExtensions(), builder,
                new SimpleLocation("Swagger Spec"))) {
            builder.setSelector("*");
            authRules.add(builder.build());
        }
    }

    /**
     * Creates a AuthenticationRule from security requirement extension.
     */
    private boolean convertSecurityRequirementExtension(Map<String, Object> extensions,
            AuthenticationRule.Builder authenticationRuleBuilder, Location location) {
        if (hasVendorExtension(extensions, SECURITY_REQUIREMENT_EXTENSION, List.class)) {
            List<Map<String, SecurityReq>> securityRequirements = getSecurityRequirements(
                    extensions.get(SECURITY_REQUIREMENT_EXTENSION));
            if (securityRequirements == null) {
                diagCollector
                        .addDiag(
                                Diag.error(location,
                                        "Extension %s does not have the valid value. Please check "
                                                + "the documentation for its schema",
                                        SECURITY_REQUIREMENT_EXTENSION));
                return false;
            }

            for (Map<String, SecurityReq> schemas : securityRequirements) {
                for (Map.Entry<String, SecurityReq> schema : schemas.entrySet()) {
                    String authSchemaName = schema.getKey();
                    if (!authProviders.containsKey(authSchemaName)) {
                        diagCollector.addDiag(Diag.error(location,
                                "Schema '%s' referenced in extension %s does not have the "
                                        + "valid value. Please check the documentation for its schema.",
                                authSchemaName, SECURITY_REQUIREMENT_EXTENSION));
                        return false;
                    }
                    List<String> audiences = schema.getValue().getAudiences();
                    if (audiences == null) {
                        diagCollector.addDiag(Diag.error(location,
                                "Extension %s does not have the valid value. Please "
                                        + "check the documentation for its schema",
                                SECURITY_REQUIREMENT_EXTENSION));
                        return false;
                    }
                    AuthRequirement.Builder authRequirement = AuthRequirement.newBuilder();
                    authRequirement.setProviderId(authSchemaName);
                    authRequirement.setAudiences(Joiner.on(",").join(audiences));
                    authenticationRuleBuilder.addRequirements(authRequirement);
                }
            }
            return true;
        }
        return false;
    }

    /**
     * Verified if the extension schema is correct.
     */
    private List<Map<String, SecurityReq>> getSecurityRequirements(Object securityReqExt) {
        Gson gson = new GsonBuilder().create();
        ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
        Type typeSecurityReqExtension = new TypeToken<List<Map<String, SecurityReq>>>() {
        }.getType();
        try {
            String jsonString = ow.writeValueAsString(securityReqExt);
            return gson.fromJson(jsonString, typeSecurityReqExtension);
        } catch (JsonProcessingException | JsonParseException ex) {
            return null;
        }
    }

    /**
     * Class representing authentication schema in security requirement extension in swagger.
     */
    private static final class SecurityReq {
        // List of audiences
        private List<String> audiences;

        public void setAudiences(List<String> audience) {
            this.audiences = audience;
        }

        public List<String> getAudiences() {
            return audiences;
        }
    }

    private void addUsageRulePerMethod(Iterable<Map<String, List<String>>> securityRequirements,
            String operationType, String swaggerPath, String selector) {
        boolean perMethodApiKeyRequired = isApiKeyRequired(securityRequirements, requiresApiKeyAtTopLevel);
        if (!perMethodApiKeyRequired) {
            diagCollector.addDiag(Diag.warning(SwaggerToService.createOperationLocation(operationType, swaggerPath),
                    "Operation does not require an API key; callers may invoke the method "
                            + "without specifying an associated API-consuming project."));
        }
        usageRules.add(UsageRule.newBuilder().setSelector(selector)
                .setAllowUnregisteredCalls(!perMethodApiKeyRequired).build());
    }

    public void addSecurityRequirementForEntireService(Iterable<SecurityRequirement> securityRequirements) {
        if (securityRequirements == null) {
            requiresApiKeyAtTopLevel = false;
        } else {
            requiresApiKeyAtTopLevel = isApiKeyRequired(
                    Iterables.transform(securityRequirements, SecurityRequirementsExtractor.INSTANCE), false);
        }
        usageRules.add(UsageRule.newBuilder().setSelector("*").setAllowUnregisteredCalls(!requiresApiKeyAtTopLevel)
                .build());
    }

    private enum SecurityRequirementsExtractor implements Function<SecurityRequirement, Map<String, List<String>>> {
        INSTANCE;

        @Override
        public Map<String, List<String>> apply(SecurityRequirement reqs) {
            return reqs.getRequirements();
        }
    }

    private boolean isApiKeyRequired(Iterable<Map<String, List<String>>> securityRequirements,
            boolean topLevelApiKeyValue) {
        if (securityRequirements == null) {
            // If there're no specific security requirements, use the API
            // default.
            return topLevelApiKeyValue;
        }

        boolean apiKeyRequired = false;
        for (Map<String, List<String>> securityReq : securityRequirements) {
            if (securityReq == null) {
                // Null requirements: this should never happen, but if it
                // does, assume that it's a set of requirements without an API
                // key definition section, which means an API key is not
                // required (Swagger default).
                return false;
            }

            boolean foundApiKeyDefinition = false;
            for (String schema : apiKeyDefinitions) {
                if (securityReq.containsKey(schema)) {
                    // The security requirement contains a requirement.
                    foundApiKeyDefinition = true;
                    break;
                }
            }
            if (!foundApiKeyDefinition) {
                // We found at least one security requirement with no API key
                // section, so an API key is not required.
                return false;
            }

            // We've found at least one security requirement with an API key
            // definition; if we don't find a security requirement without
            // an API key definition (i.e. a security requirement which
            // causes an early return from this function), an API key is
            // required.
            apiKeyRequired = true;
        }

        return apiKeyRequired;
    }
}