
dev.dsf.fhir.authorization.AbstractAuthorizationRule Maven / Gradle / Ivy
package dev.dsf.fhir.authorization;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.OrganizationAffiliation;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import ca.uhn.fhir.model.api.annotation.ResourceDef;
import dev.dsf.common.auth.conf.Identity;
import dev.dsf.fhir.authentication.FhirServerRole;
import dev.dsf.fhir.authentication.OrganizationProvider;
import dev.dsf.fhir.authorization.read.ReadAccessHelper;
import dev.dsf.fhir.dao.CodeSystemDao;
import dev.dsf.fhir.dao.OrganizationDao;
import dev.dsf.fhir.dao.ResourceDao;
import dev.dsf.fhir.dao.provider.DaoProvider;
import dev.dsf.fhir.help.ParameterConverter;
import dev.dsf.fhir.search.PageAndCount;
import dev.dsf.fhir.search.PartialResult;
import dev.dsf.fhir.search.SearchQuery;
import dev.dsf.fhir.search.SearchQueryParameterError;
import dev.dsf.fhir.service.ReferenceResolver;
import dev.dsf.fhir.service.ResourceReference;
import dev.dsf.fhir.service.ResourceReference.ReferenceType;
public abstract class AbstractAuthorizationRule>
implements AuthorizationRule, InitializingBean
{
private static final Logger logger = LoggerFactory.getLogger(AbstractAuthorizationRule.class);
protected static final String ORGANIZATION_IDENTIFIER_SYSTEM = "http://dsf.dev/sid/organization-identifier";
protected final Class resourceType;
protected final DaoProvider daoProvider;
protected final String serverBase;
protected final ReferenceResolver referenceResolver;
protected final OrganizationProvider organizationProvider;
protected final ReadAccessHelper readAccessHelper;
protected final ParameterConverter parameterConverter;
public AbstractAuthorizationRule(Class resourceType, DaoProvider daoProvider, String serverBase,
ReferenceResolver referenceResolver, OrganizationProvider organizationProvider,
ReadAccessHelper readAccessHelper, ParameterConverter parameterConverter)
{
this.resourceType = resourceType;
this.daoProvider = daoProvider;
this.serverBase = serverBase;
this.referenceResolver = referenceResolver;
this.organizationProvider = organizationProvider;
this.readAccessHelper = readAccessHelper;
this.parameterConverter = parameterConverter;
}
@Override
public void afterPropertiesSet() throws Exception
{
Objects.requireNonNull(resourceType, "resourceType");
Objects.requireNonNull(daoProvider, "daoProvider");
Objects.requireNonNull(serverBase, "serverBase");
Objects.requireNonNull(referenceResolver, "referenceResolver");
Objects.requireNonNull(organizationProvider, "organizationProvider");
Objects.requireNonNull(readAccessHelper, "readAccessHelper");
Objects.requireNonNull(parameterConverter, "parameterConverter");
}
@Override
public Class getResourceType()
{
return resourceType;
}
protected String getResourceTypeName()
{
return getResourceType().getAnnotation(ResourceDef.class).name();
}
@SuppressWarnings("unchecked")
protected final D getDao()
{
return (D) daoProvider.getDao(resourceType).orElseThrow();
}
@Override
public final Optional reasonCreateAllowed(Identity identity, R newResource)
{
try (Connection connection = daoProvider.newReadOnlyAutoCommitTransaction())
{
return reasonCreateAllowed(connection, identity, newResource);
}
catch (SQLException e)
{
logger.debug("Error while accessing database", e);
logger.warn("Error while accessing database: {} - {}", e.getClass().getName(), e.getMessage());
throw new RuntimeException(e);
}
}
@Override
public final Optional reasonReadAllowed(Identity identity, R existingResource)
{
try (Connection connection = daoProvider.newReadOnlyAutoCommitTransaction())
{
return reasonReadAllowed(connection, identity, existingResource);
}
catch (SQLException e)
{
logger.debug("Error while accessing database", e);
logger.warn("Error while accessing database: {} - {}", e.getClass().getName(), e.getMessage());
throw new RuntimeException(e);
}
}
protected List getAffiliations(Connection connection, String organizationIdentifierValue)
{
if (organizationIdentifierValue == null)
return Collections.emptyList();
try
{
return daoProvider.getOrganizationAffiliationDao()
.readActiveNotDeletedByMemberOrganizationIdentifierIncludingOrganizationIdentifiersWithTransaction(
connection, organizationIdentifierValue);
}
catch (SQLException e)
{
logger.debug("Error while accessing database", e);
logger.warn("Error while accessing database: {} - {}", e.getClass().getName(), e.getMessage());
throw new RuntimeException(e);
}
}
@Override
public final Optional reasonUpdateAllowed(Identity identity, R oldResource, R newResource)
{
try (Connection connection = daoProvider.newReadOnlyAutoCommitTransaction())
{
return reasonUpdateAllowed(connection, identity, oldResource, newResource);
}
catch (SQLException e)
{
logger.debug("Error while accessing database", e);
logger.warn("Error while accessing database: {} - {}", e.getClass().getName(), e.getMessage());
throw new RuntimeException(e);
}
}
@Override
public final Optional reasonDeleteAllowed(Identity identity, R oldResource)
{
try (Connection connection = daoProvider.newReadOnlyAutoCommitTransaction())
{
return reasonDeleteAllowed(connection, identity, oldResource);
}
catch (SQLException e)
{
logger.debug("Error while accessing database", e);
logger.warn("Error while accessing database: {} - {}", e.getClass().getName(), e.getMessage());
throw new RuntimeException(e);
}
}
protected final boolean organizationWithIdentifierExists(Connection connection, Identifier organizationIdentifier)
{
String iSystem = organizationIdentifier.getSystem();
String iValue = organizationIdentifier.getValue();
Map> queryParameters = Map.of("identifier",
Collections.singletonList(iSystem + "|" + iValue));
OrganizationDao dao = daoProvider.getOrganizationDao();
SearchQuery query = dao.createSearchQueryWithoutUserFilter(PageAndCount.exists())
.configureParameters(queryParameters);
List uQp = query.getUnsupportedQueryParameters();
if (!uQp.isEmpty())
{
logger.warn("Unable to search for Organization: Unsupported query parameters: {}", uQp);
throw new IllegalStateException("Unable to search for Organization: Unsupported query parameters.");
}
try
{
PartialResult result = dao.searchWithTransaction(connection, query);
return result.getTotal() >= 1;
}
catch (SQLException e)
{
logger.debug("Unable to search for Organization", e);
logger.warn("Unable to search for Organization: {} - {}", e.getClass().getName(), e.getMessage());
throw new RuntimeException("Unable to search for Organization", e);
}
}
protected final boolean roleExists(Connection connection, Coding coding)
{
String cSystem = coding.getSystem();
String cVersion = coding.getVersion();
String cCode = coding.getCode();
Map> queryParameters = Map.of("url",
Collections.singletonList(cSystem + (coding.hasVersion() ? "|" + cVersion : "")));
CodeSystemDao dao = daoProvider.getCodeSystemDao();
SearchQuery query = dao.createSearchQueryWithoutUserFilter(PageAndCount.single())
.configureParameters(queryParameters);
List uQp = query.getUnsupportedQueryParameters();
if (!uQp.isEmpty())
{
logger.warn("Unable to search for CodeSystem: Unsupported query parameters: {}", uQp);
throw new IllegalStateException("Unable to search for CodeSystem: Unsupported query parameters");
}
try
{
PartialResult result = dao.searchWithTransaction(connection, query);
return result.getTotal() >= 1 && hasCode(result.getPartialResult().get(0), cCode);
}
catch (SQLException e)
{
logger.debug("Unable to search for CodeSystem", e);
logger.warn("Unable to search for CodeSystem: {} - {}", e.getClass().getName(), e.getMessage());
throw new RuntimeException("Unable to search for CodeSystem", e);
}
}
private boolean hasCode(CodeSystem codeSystem, String cCode)
{
return codeSystem.getConcept().stream().filter(ConceptDefinitionComponent::hasCode)
.map(ConceptDefinitionComponent::getCode).anyMatch(c -> c.equals(cCode));
}
protected final boolean isCurrentIdentityPartOfReferencedOrganizations(Connection connection, Identity identity,
String referenceLocation, Collection extends Reference> references)
{
return isCurrentIdentityPartOfReferencedOrganizations(connection, identity, referenceLocation,
references.stream());
}
protected final boolean isCurrentIdentityPartOfReferencedOrganizations(Connection connection, Identity identity,
String referenceLocation, Stream extends Reference> references)
{
return references.anyMatch(
r -> isCurrentIdentityPartOfReferencedOrganization(connection, identity, referenceLocation, r));
}
protected final boolean isCurrentIdentityPartOfReferencedOrganization(Connection connection, Identity identity,
String referenceLocation, Reference reference)
{
if (reference == null)
{
logger.warn("Null reference while checking if user part of referenced organization");
return false;
}
else
{
ResourceReference resReference = new ResourceReference(referenceLocation, reference, Organization.class);
ReferenceType type = resReference.getType(serverBase);
if (!EnumSet.of(ReferenceType.LITERAL_INTERNAL, ReferenceType.LOGICAL).contains(type))
{
logger.warn("Reference of type {} not supported while checking if user part of referenced organization",
type);
return false;
}
Optional resource = referenceResolver.resolveReference(identity, resReference, connection);
if (resource.isPresent() && resource.get() instanceof Organization)
{
// ignoring updates (version changes) to the organization id
boolean sameOrganization = identity.getOrganization().getIdElement().getIdPart()
.equals(resource.get().getIdElement().getIdPart());
if (!sameOrganization)
logger.warn(
"Current user not part of organization {} while checking if user part of referenced organization",
resource.get().getIdElement().getValue());
return sameOrganization;
}
else
{
logger.warn(
"Reference to organization could not be resolved while checking if user part of referenced organization");
return false;
}
}
}
protected final boolean isLocalOrganization(Organization organization)
{
if (organization == null || !organization.hasIdElement())
return false;
return organizationProvider.getLocalOrganization()
.map(localOrg -> localOrg.getIdElement().equals(organization.getIdElement())).orElse(false);
}
@SafeVarargs
protected final Optional createIfLiteralInternalOrLogicalReference(String referenceLocation,
Reference reference, Class extends Resource>... referenceTypes)
{
ResourceReference r = new ResourceReference(referenceLocation, reference, referenceTypes);
ReferenceType type = r.getType(serverBase);
if (EnumSet.of(ReferenceType.LITERAL_INTERNAL, ReferenceType.LOGICAL).contains(type))
return Optional.of(r);
else
return Optional.empty();
}
protected final Optional resolveReference(Connection connection, Identity identity,
Optional reference)
{
return reference.flatMap(ref -> referenceResolver.resolveReference(identity, ref, connection));
}
@Override
public Optional reasonPermanentDeleteAllowed(Identity identity, R oldResource)
{
try (Connection connection = daoProvider.newReadOnlyAutoCommitTransaction())
{
return reasonPermanentDeleteAllowed(connection, identity, oldResource);
}
catch (SQLException e)
{
logger.debug("Error while accessing database", e);
logger.warn("Error while accessing database: {} - {}", e.getClass().getName(), e.getMessage());
throw new RuntimeException(e);
}
}
@Override
public final Optional reasonSearchAllowed(Identity identity)
{
if (identity.hasDsfRole(FhirServerRole.SEARCH))
{
logger.info("Search of {} authorized for identity '{}'", getResourceTypeName(), identity.getName());
return Optional.of("Identity has role " + FhirServerRole.SEARCH);
}
else
{
logger.warn("Search of {} unauthorized for identity '{}', no role {}", getResourceTypeName(),
identity.getName(), FhirServerRole.SEARCH);
return Optional.empty();
}
}
@Override
public final Optional reasonHistoryAllowed(Identity identity)
{
if (identity.hasDsfRole(FhirServerRole.HISTORY))
{
logger.info("History of {} authorized for identity '{}'", getResourceTypeName(), identity.getName());
return Optional.of("Identity has role " + FhirServerRole.HISTORY);
}
else
{
logger.warn("History of {} unauthorized for identity '{}', no role {}", getResourceTypeName(),
identity.getName(), FhirServerRole.HISTORY);
return Optional.empty();
}
}
@Override
public Optional reasonPermanentDeleteAllowed(Connection connection, Identity identity, R oldResource)
{
final String resourceId = oldResource.getIdElement().getIdPart();
final long resourceVersion = oldResource.getIdElement().getVersionIdPartAsLong();
if (identity.isLocalIdentity() && identity.hasDsfRole(FhirServerRole.PERMANENT_DELETE)
&& reasonDeleteAllowed(connection, identity, oldResource).isPresent())
{
logger.info("Permanent delete of {}/{}/_history/{} authorized for identity '{}'", getResourceTypeName(),
resourceId, resourceVersion, identity.getName());
return Optional.of("Identity is local identity and has role " + FhirServerRole.PERMANENT_DELETE);
}
else
{
logger.warn(
"Permanent delete of {}/{}/_history/{} unauthorized for identity '{}', not a local identity or no role {}",
getResourceTypeName(), resourceId, resourceVersion, identity.getName(),
FhirServerRole.PERMANENT_DELETE);
return Optional.empty();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy