CMSgov/dpc-app

View on GitHub
dpc-api/src/main/java/gov/cms/dpc/api/auth/macaroonauth/MacaroonsAuthenticator.java

Summary

Maintainability
A
1 hr
Test Coverage
F
12%
package gov.cms.dpc.api.auth.macaroonauth;

import ca.uhn.fhir.rest.client.api.IGenericClient;
import com.google.inject.name.Named;
import gov.cms.dpc.api.auth.DPCAuthCredentials;
import gov.cms.dpc.api.auth.OrganizationPrincipal;
import gov.cms.dpc.fhir.DPCIdentifierSystem;
import gov.cms.dpc.fhir.DPCResourceType;
import io.dropwizard.auth.Authenticator;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Organization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.util.*;

/**
 * Implementation of {@link Authenticator} which matches an {@link Organization} to the given Macaroon (base64 encoded string
 * If no {@link Organization} is found, this returns an empty optional, which signifies and authorization failure.
 */
public class MacaroonsAuthenticator implements Authenticator<DPCAuthCredentials, OrganizationPrincipal> {

    private static final Logger logger = LoggerFactory.getLogger(MacaroonsAuthenticator.class);

    private final IGenericClient client;

    @Inject
    public MacaroonsAuthenticator(@Named("attribution") IGenericClient client) {
        this.client = client;
    }

    @Override
    public Optional<OrganizationPrincipal> authenticate(DPCAuthCredentials credentials) {
        logger.debug("Performing token authentication");

        // If we don't have a path authorizer, just return the principal
        final OrganizationPrincipal principal = new OrganizationPrincipal(credentials.getOrganization());
        if (credentials.getPathAuthorizer() == null) {
            logger.debug("No path authorizer is present, returning principal");
            return Optional.of(principal);
        }

        // If we're an organization, we just check the org ID against the path value and see if it matches
        if (credentials.getPathAuthorizer().type() == DPCResourceType.Organization) {
            return validateOrganization(principal, credentials);
        }

        // Otherwise, try to lookup the matching resource
        logger.debug("Looking up resource {} in path authorizer. With value: {}", credentials.getPathAuthorizer().type(), credentials.getPathAuthorizer().pathParam());
        Map<String, List<String>> searchParams = new HashMap<>();
        searchParams.put("_id", Collections.singletonList(credentials.getPathValue()));
        searchParams.put("organization", Collections.singletonList(credentials.getOrganization().getId()));

        // Special handling of Group resources, which use tags instead of resource properties.
        // TODO: Remove with DPC-552
        if (credentials.getPathAuthorizer().type() == DPCResourceType.Group) {
            searchParams.put("_tag", Collections.singletonList(String.format("%s|%s", DPCIdentifierSystem.DPC.getSystem(), credentials.getOrganization().getId())));
        }
        final Bundle bundle = this.client
                .search()
                .forResource(credentials.getPathAuthorizer().type().toString())
                .whereMap(searchParams)
                .returnBundle(Bundle.class)
                .encodedJson()
                .execute();

        if (bundle.getTotal() == 0) {
            return Optional.empty();
        }

        return Optional.of(principal);
    }

    private Optional<OrganizationPrincipal> validateOrganization(OrganizationPrincipal principal, DPCAuthCredentials credentials) {
        final String orgID = credentials.getOrganization().getId();
        final String pathValue = credentials.getPathValue();
        logger.debug("Validating Organization {} matches path value: {}", orgID, pathValue);
        return orgID.equals("Organization/" + pathValue) ?
                Optional.of(principal) : Optional.empty();
    }
}