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

com.azure.cosmos.implementation.BaseAuthorizationTokenProvider Maven / Gradle / Ivy

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.cosmos.implementation;

import com.azure.cosmos.implementation.directconnectivity.HttpUtils;
import com.azure.cosmos.CosmosKeyCredential;
import org.apache.commons.lang3.StringUtils;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;

/**
 * This class is used internally by both client (for generating the auth header with master/system key) and by the GATEWAY when
 * verifying the auth header in the Azure Cosmos DB database service.
 */
public class BaseAuthorizationTokenProvider implements AuthorizationTokenProvider {

    private static final String AUTH_PREFIX = "type=master&ver=1.0&sig=";
    private final CosmosKeyCredential cosmosKeyCredential;
    private final Mac macInstance;

    //  stores current master key's hashcode for performance reasons.
    private int masterKeyHashCode;

    public BaseAuthorizationTokenProvider(CosmosKeyCredential cosmosKeyCredential) {
        this.cosmosKeyCredential = cosmosKeyCredential;
        this.macInstance = getMacInstance();
    }

    private static String getResourceSegment(ResourceType resourceType) {
        switch (resourceType) {
        case Attachment:
            return Paths.ATTACHMENTS_PATH_SEGMENT;
        case Database:
            return Paths.DATABASES_PATH_SEGMENT;
        case Conflict:
            return Paths.CONFLICTS_PATH_SEGMENT;
        case Document:
            return Paths.DOCUMENTS_PATH_SEGMENT;
        case DocumentCollection:
            return Paths.COLLECTIONS_PATH_SEGMENT;
        case Offer:
            return Paths.OFFERS_PATH_SEGMENT;
        case Permission:
            return Paths.PERMISSIONS_PATH_SEGMENT;
        case StoredProcedure:
            return Paths.STORED_PROCEDURES_PATH_SEGMENT;
        case Trigger:
            return Paths.TRIGGERS_PATH_SEGMENT;
        case UserDefinedFunction:
            return Paths.USER_DEFINED_FUNCTIONS_PATH_SEGMENT;
        case User:
            return Paths.USERS_PATH_SEGMENT;
        case PartitionKeyRange:
            return Paths.PARTITION_KEY_RANGES_PATH_SEGMENT;
        case Media:
            return Paths.MEDIA_PATH_SEGMENT;
        case DatabaseAccount:
            return "";
        default:
            return null;
        }
    }

    /**
     * This API is a helper method to create auth header based on client request using masterkey.
     *
     * @param verb                 the verb.
     * @param resourceIdOrFullName the resource id or full name
     * @param resourceType         the resource type.
     * @param headers              the request headers.
     * @return the key authorization signature.
     */
    public String generateKeyAuthorizationSignature(String verb,
            String resourceIdOrFullName,
            ResourceType resourceType,
            Map headers) {
        return this.generateKeyAuthorizationSignature(verb, resourceIdOrFullName,
                BaseAuthorizationTokenProvider.getResourceSegment(resourceType).toLowerCase(), headers);
    }

    /**
     * This API is a helper method to create auth header based on client request using masterkey.
     *
     * @param verb                 the verb
     * @param resourceIdOrFullName the resource id or full name
     * @param  resourceSegment     the resource segment
     * @param headers              the request headers
     * @return the key authorization signature
     */
    public String generateKeyAuthorizationSignature(String verb,
            String resourceIdOrFullName,
            String resourceSegment,
            Map headers) {
        if (verb == null || verb.isEmpty()) {
            throw new IllegalArgumentException("verb");
        }

        if (resourceIdOrFullName == null) {
            resourceIdOrFullName = "";
        }

        if (resourceSegment == null) {
            throw new IllegalArgumentException("resourceSegment");
        }

        if (headers == null) {
            throw new IllegalArgumentException("headers");
        }

        if (StringUtils.isEmpty(this.cosmosKeyCredential.getKey())) {
            throw new IllegalArgumentException("key credentials cannot be empty");
        }

        if(!PathsHelper.isNameBased(resourceIdOrFullName)) {
            resourceIdOrFullName = resourceIdOrFullName.toLowerCase(Locale.ROOT);
        }

        // Skipping lower casing of resourceId since it may now contain "ID" of the resource as part of the FullName
        StringBuilder body = new StringBuilder();
        body.append(verb.toLowerCase())
                .append('\n')
                .append(resourceSegment)
                .append('\n')
                .append(resourceIdOrFullName)
                .append('\n');

        if (headers.containsKey(HttpConstants.HttpHeaders.X_DATE)) {
            body.append(headers.get(HttpConstants.HttpHeaders.X_DATE).toLowerCase());
        }

        body.append('\n');

        if (headers.containsKey(HttpConstants.HttpHeaders.HTTP_DATE)) {
            body.append(headers.get(HttpConstants.HttpHeaders.HTTP_DATE).toLowerCase());
        }

        body.append('\n');

        Mac mac = getMacInstance();

        byte[] digest = mac.doFinal(body.toString().getBytes());

        String auth = Utils.encodeBase64String(digest);

        return AUTH_PREFIX + auth;
    }

    /**
     * This API is a helper method to create auth header based on client request using resourceTokens.
     *
     * @param resourceTokens the resource tokens.
     * @param path           the path.
     * @param resourceId     the resource id.
     * @return the authorization token.
     */
    public String getAuthorizationTokenUsingResourceTokens(Map resourceTokens,
            String path,
            String resourceId) {
        if (resourceTokens == null) {
            throw new IllegalArgumentException("resourceTokens");
        }

        String resourceToken = null;
        if (resourceTokens.containsKey(resourceId) && resourceTokens.get(resourceId) != null) {
            resourceToken = resourceTokens.get(resourceId);
        } else if (StringUtils.isEmpty(path) || StringUtils.isEmpty(resourceId)) {
            if (resourceTokens.size() > 0) {
                resourceToken = resourceTokens.values().iterator().next();
            }
        } else {
            // Get the last resource id from the path and use that to find the corresponding token.
            String[] pathParts = StringUtils.split(path, "/");
            String[] resourceTypes = {"dbs", "colls", "docs", "sprocs", "udfs", "triggers", "users", "permissions",
                    "attachments", "media", "conflicts"};
            HashSet resourceTypesSet = new HashSet();
            Collections.addAll(resourceTypesSet, resourceTypes);

            for (int i = pathParts.length - 1; i >= 0; --i) {

                if (!resourceTypesSet.contains(pathParts[i]) && resourceTokens.containsKey(pathParts[i])) {
                    resourceToken = resourceTokens.get(pathParts[i]);
                }
            }
        }

        return resourceToken;
    }
    public String generateKeyAuthorizationSignature(String verb, URI uri, Map headers) {
        if (StringUtils.isEmpty(verb)) {
            throw new IllegalArgumentException(String.format(RMResources.StringArgumentNullOrEmpty, "verb"));
        }

        if (uri == null) {
            throw new IllegalArgumentException("uri");
        }

        if (headers == null) {
            throw new IllegalArgumentException("headers");
        }
        PathInfo pathInfo = new PathInfo(false, StringUtils.EMPTY, StringUtils.EMPTY, false);
        getResourceTypeAndIdOrFullName(uri, pathInfo);
        return generateKeyAuthorizationSignatureNew(verb, pathInfo.resourceIdOrFullName, pathInfo.resourcePath,
                headers);
    }

    private String generateKeyAuthorizationSignatureNew(String verb, String resourceIdValue, String resourceType,
                                                        Map headers) {
        if (StringUtils.isEmpty(verb)) {
            throw new IllegalArgumentException(String.format(RMResources.StringArgumentNullOrEmpty, "verb"));
        }

        if (resourceType == null) {
            throw new IllegalArgumentException(String.format(RMResources.StringArgumentNullOrEmpty, "resourceType")); // can be empty
        }

        if (headers == null) {
            throw new IllegalArgumentException("headers");
        }
        // Order of the values included in the message payload is a protocol that
        // clients/BE need to follow exactly.
        // More headers can be added in the future.
        // If any of the value is optional, it should still have the placeholder value
        // of ""
        // OperationType -> ResourceType -> ResourceId/OwnerId -> XDate -> Date

        String authResourceId = getAuthorizationResourceIdOrFullName(resourceType, resourceIdValue);
        String payLoad = generateMessagePayload(verb, authResourceId, resourceType, headers);
        Mac mac = this.getMacInstance();
        byte[] digest = mac.doFinal(payLoad.getBytes());
        String authorizationToken = Utils.encodeBase64String(digest);
        String authtoken = AUTH_PREFIX + authorizationToken;
        return HttpUtils.urlEncode(authtoken);
    }

    private Mac getMacInstance() {
        int masterKeyLatestHashCode = this.cosmosKeyCredential.getKeyHashCode();

        //  Master key has changed, or this is the first time we are getting mac instance
        if (masterKeyLatestHashCode != this.masterKeyHashCode) {
            byte[] masterKeyBytes = this.cosmosKeyCredential.getKey().getBytes();
            byte[] masterKeyDecodedBytes = Utils.Base64Decoder.decode(masterKeyBytes);
            SecretKey signingKey = new SecretKeySpec(masterKeyDecodedBytes, "HMACSHA256");
            try {
                Mac macInstance = Mac.getInstance("HMACSHA256");
                macInstance.init(signingKey);
                //  Update the master key hash code
                this.masterKeyHashCode = masterKeyLatestHashCode;
                return macInstance;
            } catch (NoSuchAlgorithmException | InvalidKeyException e) {
                throw new IllegalStateException(e);
            }
        } else {
            //  Master key hasn't changed, return the cloned mac instance
            try {
                return (Mac)this.macInstance.clone();
            } catch (CloneNotSupportedException e) {
                throw new IllegalStateException(e);
            }
        }
    }

    private String generateMessagePayload(String verb, String resourceId, String resourceType,
            Map headers) {
        String xDate = headers.get(HttpConstants.HttpHeaders.X_DATE);
        String date = headers.get(HttpConstants.HttpHeaders.HTTP_DATE);
        // At-least one of date header should present
        // https://docs.microsoft.com/en-us/rest/api/documentdb/access-control-on-documentdb-resources
        if (StringUtils.isEmpty(xDate) && (StringUtils.isEmpty(date) || StringUtils.isWhitespace(date))) {
            headers.put(HttpConstants.HttpHeaders.X_DATE, Utils.nowAsRFC1123());
            xDate = Utils.nowAsRFC1123();
        }

        // for name based, it is case sensitive, we won't use the lower case
        if (!PathsHelper.isNameBased(resourceId)) {
            resourceId = resourceId.toLowerCase();
        }

        StringBuilder payload = new StringBuilder();
        payload.append(verb.toLowerCase())
                .append('\n')
                .append(resourceType.toLowerCase())
                .append('\n')
                .append(resourceId)
                .append('\n')
                .append(xDate.toLowerCase())
                .append('\n')
                .append(StringUtils.isEmpty(xDate) ? date.toLowerCase() : "")
                .append('\n');

        return payload.toString();
    }

    private String getAuthorizationResourceIdOrFullName(String resourceType, String resourceIdOrFullName) {
        if (StringUtils.isEmpty(resourceType) || StringUtils.isEmpty(resourceIdOrFullName)) {
            return resourceIdOrFullName;
        }
        if (PathsHelper.isNameBased(resourceIdOrFullName)) {
            // resource fullname is always end with name (not type segment like docs/colls).
            return resourceIdOrFullName;
        }

        if (resourceType.equalsIgnoreCase(Paths.OFFERS_PATH_SEGMENT)
                || resourceType.equalsIgnoreCase(Paths.PARTITIONS_PATH_SEGMENT)
                || resourceType.equalsIgnoreCase(Paths.TOPOLOGY_PATH_SEGMENT)
                || resourceType.equalsIgnoreCase(Paths.RID_RANGE_PATH_SEGMENT)) {
            return resourceIdOrFullName;
        }

        ResourceId parsedRId = ResourceId.parse(resourceIdOrFullName);
        if (resourceType.equalsIgnoreCase(Paths.DATABASES_PATH_SEGMENT)) {
            return parsedRId.getDatabaseId().toString();
        } else if (resourceType.equalsIgnoreCase(Paths.USERS_PATH_SEGMENT)) {
            return parsedRId.getUserId().toString();
        } else if (resourceType.equalsIgnoreCase(Paths.COLLECTIONS_PATH_SEGMENT)) {
            return parsedRId.getDocumentCollectionId().toString();
        } else if (resourceType.equalsIgnoreCase(Paths.DOCUMENTS_PATH_SEGMENT)) {
            return parsedRId.getDocumentId().toString();
        } else {
            // leaf node
            return resourceIdOrFullName;
        }
    }

    private void getResourceTypeAndIdOrFullName(URI uri, PathInfo pathInfo) {
        if (uri == null) {
            throw new IllegalArgumentException("uri");
        }

        pathInfo.resourcePath = StringUtils.EMPTY;
        pathInfo.resourceIdOrFullName = StringUtils.EMPTY;

        String[] segments = StringUtils.split(uri.toString(), Constants.Properties.PATH_SEPARATOR);
        if (segments == null || segments.length < 1) {
            throw new IllegalArgumentException(RMResources.InvalidUrl);
        }
        // Authorization code is fine with Uri not having resource id and path.
        // We will just return empty in that case
        String pathAndQuery = StringUtils.EMPTY ;
        if(StringUtils.isNotEmpty(uri.getPath())) {
            pathAndQuery+= uri.getPath();
        }
        if(StringUtils.isNotEmpty(uri.getQuery())) {
            pathAndQuery+="?";
            pathAndQuery+= uri.getQuery();
        }
        if (!PathsHelper.tryParsePathSegments(pathAndQuery, pathInfo, null)) {
            pathInfo.resourcePath = StringUtils.EMPTY;
            pathInfo.resourceIdOrFullName = StringUtils.EMPTY;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy