Skip to content

Commit

Permalink
apply limits to Practitioner permissions; IF practitioner launch spec…
Browse files Browse the repository at this point in the history
…ifies patient context THEN practitioner access limited to patient compartments linked to the context patient
  • Loading branch information
timcoffman committed Sep 24, 2024
1 parent d03e417 commit 2cea1e9
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 25 deletions.
39 changes: 35 additions & 4 deletions src/main/java/edu/ohsu/cmp/ecp/sds/Permissions.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,27 @@

public final class Permissions {
private final IIdType authorizedUserId;
private final Optional<Permissions.ReadSpecificPatient> readSpecificPatient ;
private final Optional<Permissions.ReadAllPatients> readAllPatients ;
private final Optional<Permissions.ReadAndWriteSpecificPatient> readAndWriteSpecificPatient ;

public Permissions( Permissions.ReadAllPatients readAllPatients ) {
this.readAllPatients = Optional.of(readAllPatients);
authorizedUserId = readAllPatients.authorizedUserId() ;
this.readSpecificPatient = Optional.empty();
this.readAndWriteSpecificPatient = Optional.empty();
}

public Permissions( Permissions.ReadSpecificPatient readSpecificPatient ) {
this.readAllPatients = Optional.empty();
this.readSpecificPatient = Optional.of( readSpecificPatient );
authorizedUserId = readSpecificPatient.authorizedUserId() ;
this.readAndWriteSpecificPatient = Optional.empty();
}

public Permissions( Permissions.ReadAndWriteSpecificPatient readAndWriteSpecificPatient) {
this.readAllPatients = Optional.empty();
this.readSpecificPatient = Optional.empty();
this.readAndWriteSpecificPatient = Optional.of( readAndWriteSpecificPatient );
authorizedUserId = readAndWriteSpecificPatient.authorizedUserId() ;
}
Expand All @@ -28,6 +38,11 @@ public IIdType authorizedUserId() {
public Optional<Permissions.ReadAllPatients> readAllPatients() {
return readAllPatients ;
}

public Optional<Permissions.ReadSpecificPatient> readSpecificPatient() {
return readSpecificPatient ;
}

public Optional<Permissions.ReadAndWriteSpecificPatient> readAndWriteSpecificPatient() {
return readAndWriteSpecificPatient ;
}
Expand All @@ -44,12 +59,12 @@ public IIdType authorizedUserId() {
}
}

public static final class ReadAndWriteSpecificPatient {
public static final class ReadSpecificPatient {
private final IIdType authorizedUserId;

private final UserIdentity patientId;

public ReadAndWriteSpecificPatient(IIdType authorizedUserId, UserIdentity patientId) {
public ReadSpecificPatient(IIdType authorizedUserId, UserIdentity patientId) {
this.authorizedUserId = authorizedUserId;
this.patientId = patientId;
}
Expand All @@ -62,8 +77,24 @@ public UserIdentity patientId() {
return patientId ;
}

public Permissions.ReadAndWriteSpecificPatient withUpdatedPatientIdentity( UserIdentity replacementPatientId ) {
return new ReadAndWriteSpecificPatient( authorizedUserId, replacementPatientId ) ;
}

public static final class ReadAndWriteSpecificPatient {
private final IIdType authorizedUserId;

private final UserIdentity patientId;

public ReadAndWriteSpecificPatient(IIdType authorizedUserId, UserIdentity patientId) {
this.authorizedUserId = authorizedUserId;
this.patientId = patientId;
}

public IIdType authorizedUserId() {
return authorizedUserId ;
}

public UserIdentity patientId() {
return patientId ;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@ public interface SupplementalDataStoreAuth {

void addAuthCapability(IBaseConformance theCapabilityStatement, URI authorizeUri, URI tokenUri );

public interface LaunchContext {
IIdType getPatient() ;
}

public interface AuthorizationProfile {

IIdType getAuthorizedUserId() ;
IIdType getTargetPatientId() ;

LaunchContext getLaunchContext() ;

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public AuthorizationProfile authorizationProfile(RequestDetails theRequestDetail
String authorizedResourceType = authorizedUserId.getResourceType();

if ( "Practitioner".equalsIgnoreCase(authorizedResourceType) )
return SupplementalDataStoreAuthProfile.forPractitioner( authorizedUserId ) ;
return SupplementalDataStoreAuthProfile.forPractitioner( authorizedUserId, launchContextFromAuthentication(authentication) ) ;

if ( "Patient".equalsIgnoreCase(authorizedResourceType) )
return SupplementalDataStoreAuthProfile.forPatient( authorizedUserId ) ;
Expand All @@ -40,8 +40,7 @@ public AuthorizationProfile authorizationProfile(RequestDetails theRequestDetail
throw new AuthenticationException(Msg.code(644) + "Principal \"" + authorizedUserId + "\" Not Authorized For Any Patient");

}

private IIdType authorizedUserIdFromAuthentication(Authentication authentication) {
private OAuth2AuthenticatedPrincipal oauth2PrincipalFromAuthentication(Authentication authentication) {
if (null == authentication)
throw new AuthenticationException(Msg.code(644) + "Missing or Invalid Authorization");

Expand All @@ -53,12 +52,41 @@ private IIdType authorizedUserIdFromAuthentication(Authentication authentication
return null;

OAuth2AuthenticatedPrincipal oauth2Principal = (OAuth2AuthenticatedPrincipal) authentication.getPrincipal();
return oauth2Principal ;
}

private IIdType authorizedUserIdFromAuthentication(Authentication authentication) {
OAuth2AuthenticatedPrincipal oauth2Principal = oauth2PrincipalFromAuthentication(authentication);
if ( null == oauth2Principal )
return null ;

Object subject = oauth2Principal.getAttribute("sub");
if (null == subject)
throw new AuthenticationException(Msg.code(644) + "Missing or Invalid Subject");

return idFromSubject(subject.toString());
}


private LaunchContext launchContextFromAuthentication(Authentication authentication) {
OAuth2AuthenticatedPrincipal oauth2Principal = oauth2PrincipalFromAuthentication(authentication);
if ( null == oauth2Principal )
return null ;

Object contextPatient = oauth2Principal.getAttribute("patient");
if (null == contextPatient)
return null;

IIdType contextPatientId = idFromSubject(contextPatient.toString());

return new LaunchContext() {

@Override
public IIdType getPatient() {
return contextPatientId;
}

};
}

protected abstract IIdType idFromSubject( String subject ) ;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,30 @@
import org.hl7.fhir.instance.model.api.IIdType;

import edu.ohsu.cmp.ecp.sds.SupplementalDataStoreAuth.AuthorizationProfile;
import edu.ohsu.cmp.ecp.sds.SupplementalDataStoreAuth.LaunchContext;

public class SupplementalDataStoreAuthProfile implements AuthorizationProfile {

private final IIdType authorizedUserId;
private final IIdType targetPatientId;
private final LaunchContext launchContext;

public SupplementalDataStoreAuthProfile(IIdType authorizedUserId, IIdType targetPatientId ) {
public SupplementalDataStoreAuthProfile(IIdType authorizedUserId, IIdType targetPatientId, LaunchContext launchContext ) {
this.authorizedUserId = authorizedUserId;
this.targetPatientId = targetPatientId;
this.launchContext = launchContext;
}

public static AuthorizationProfile forPatient( IIdType userAndPatientId ) {
return new SupplementalDataStoreAuthProfile( userAndPatientId, userAndPatientId ) ;
return new SupplementalDataStoreAuthProfile( userAndPatientId, userAndPatientId, null ) ;
}

public static AuthorizationProfile forOtherPatient( IIdType userId, IIdType patientId ) {
return new SupplementalDataStoreAuthProfile( userId, patientId ) ;
return new SupplementalDataStoreAuthProfile( userId, patientId, null ) ;
}

public static AuthorizationProfile forPractitioner( IIdType practitionerId ) {
return new SupplementalDataStoreAuthProfile( practitionerId, null ) ;
public static AuthorizationProfile forPractitioner( IIdType practitionerId, LaunchContext launchContext ) {
return new SupplementalDataStoreAuthProfile( practitionerId, null, launchContext ) ;
}

@Override
Expand All @@ -36,4 +39,9 @@ public IIdType getTargetPatientId() {
return targetPatientId;
}

@Override
public LaunchContext getLaunchContext() {
return launchContext;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ public class SupplementalDataStoreAuthorizationInterceptor extends Authorization
private static IAuthRuleBuilder ruleBuilder() {
return new RuleBuilder();
}

@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
List<IAuthRule> rules = new ArrayList<>() ;

ruleBuilder()
.allow( "capability statement" )
.metadata()
Expand All @@ -55,16 +55,19 @@ public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {

return rules ;
}

private List<IAuthRule> buildRuleListForPermissions(Permissions permissions) {

if ( null == permissions ) {
/* return early, no details of the authorization are available */
return ruleBuilder()
.denyAll("expected user to be authorized for patient read and/or write")
.build();
}

if ( permissions.readSpecificPatient().isPresent() )
return buildRuleListForPermissions( permissions.readSpecificPatient().get() ) ;

if ( permissions.readAllPatients().isPresent() )
return buildRuleListForPermissions( permissions.readAllPatients().get() ) ;

Expand Down Expand Up @@ -100,6 +103,40 @@ private List<IAuthRule> buildRuleListForPermissions( Permissions.ReadAllPatients
return rules ;
}

private List<IAuthRule> buildRuleListForPermissions( Permissions.ReadSpecificPatient readSpecificPatient ) {
List<IAuthRule> rules = new ArrayList<>() ;

readSpecificPatient.patientId().localUserId().ifPresent( localPatientId -> {

/* permit access to all sds-local records for specific patient */
inspectPatientCompartment( true, localPatientId )
.forEach( rules::add );
if ( localPatientId.hasBaseUrl() ) {
inspectPatientCompartment( true, localPatientId.toUnqualifiedVersionless() )
.forEach( rules::add );
}
/* permit access to all sds-local linkages that link to specific patient */
inspectLinkages( true, localPatientId.toUnqualifiedVersionless() )
.forEach( rules::add ) ;

});

/* permit access to all sds-foreign records for specific patient in each partition */
for (IIdType nonLocalPatientId : readSpecificPatient.patientId().nonLocalUserIds() ) {
inspectPatientCompartment( false, nonLocalPatientId )
.forEach( rules::add ) ;
if ( nonLocalPatientId.hasBaseUrl() )
inspectPatientCompartment( false, nonLocalPatientId.toUnqualifiedVersionless() )
.forEach( rules::add ) ;

/* permit access to all sds-foreign linkages that link to specific patient */
inspectLinkages( false, nonLocalPatientId )
.forEach( rules::add ) ;
}

return rules ;
}

private List<IAuthRule> buildRuleListForPermissions( Permissions.ReadAndWriteSpecificPatient readAndWriteSpecificPatients ) {
List<IAuthRule> rules = new ArrayList<>() ;

Expand All @@ -113,7 +150,7 @@ private List<IAuthRule> buildRuleListForPermissions( Permissions.ReadAndWriteSpe
;

}

readAndWriteSpecificPatients.patientId().localUserId().ifPresent( localPatientId -> {

/* permit access to all sds-local records for specific patient */
Expand Down Expand Up @@ -249,6 +286,19 @@ private List<IAuthRule> managePatientCompartment( boolean isLocal, IIdType patie
return rules ;
}

private List<IAuthRule> inspectPatientCompartment( boolean isLocal, IIdType patientId ) {
List<IAuthRule> rules = new ArrayList<>() ;

ruleBuilder()
.allow( describePatientPermission("read", isLocal, patientId) )
.read().allResources().inCompartment("Patient", patientId)
.build()
.forEach( rules::add )
;

return rules ;
}

private List<IAuthRule> manageLinkages( boolean isLocal, IIdType patientId ) {
/* omitting the resource type DOES break the Linkage search */
String filterForLinkageByItem = "item=" + patientId.toUnqualifiedVersionless().toString();
Expand All @@ -266,4 +316,15 @@ private List<IAuthRule> manageLinkages( boolean isLocal, IIdType patientId ) {
;
}

private List<IAuthRule> inspectLinkages( boolean isLocal, IIdType patientId ) {
/* omitting the resource type DOES break the Linkage search */
String filterForLinkageByItem = "item=" + patientId.toUnqualifiedVersionless().toString();

return ruleBuilder()
.allow( describePatientPermission("read linkages for", isLocal, patientId) )
.read().resourcesOfType("Linkage").withFilter( filterForLinkageByItem )
.build()
;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import edu.ohsu.cmp.ecp.sds.SupplementalDataStoreAuth.AuthorizationProfile;
import edu.ohsu.cmp.ecp.sds.SupplementalDataStoreAuth.LaunchContext;
import edu.ohsu.cmp.ecp.sds.base.FhirResourceComparison;

@Interceptor
Expand Down Expand Up @@ -61,7 +62,7 @@ public void identifyPermissions(RequestDetails theRequestDetails) {
if ( "Practitioner".equalsIgnoreCase( authorizedUserId.getResourceType() ) ) {
theRequestDetails.setAttribute(
REQUEST_ATTR_PERMISSIONS,
permissionsForPractitioner( authorizedUserId )
permissionsForPractitioner( authorizedUserId, authProfile.getLaunchContext() )
);
} else {
theRequestDetails.setAttribute(
Expand Down Expand Up @@ -94,10 +95,24 @@ private UserIdentity buildUserIdentity( Optional<IIdType> localUserId, IIdType b
return new UserIdentity(basisUserId, localUserId, nonLocalPatientIds) ;
}

private Permissions permissionsForPractitioner( IIdType authorizedUserId ) {
private Permissions permissionsForPractitioner( IIdType authorizedUserId, LaunchContext launchContext ) {
if ( null != launchContext && null != launchContext.getPatient() ) {
return permissionsForPractitionerInPatientContext(authorizedUserId, launchContext.getPatient()) ;
} else {
return permissionsForPractitionerWithoutContext( authorizedUserId ) ;
}
}

private Permissions permissionsForPractitionerWithoutContext( IIdType authorizedUserId ) {
return new Permissions( new Permissions.ReadAllPatients(authorizedUserId) ) ;
}

private Permissions permissionsForPractitionerInPatientContext( IIdType authorizedUserId, IIdType launchPatientContext ) {
UserIdentity contextPatientId = buildUserIdentity( launchPatientContext );

return new Permissions( new Permissions.ReadSpecificPatient(authorizedUserId, contextPatientId ) ) ;
}

private Permissions permissionsForPatient( IIdType authorizedUserId, IIdType authorizedPatientId, RequestDetails theRequestDetails ) {


Expand Down
Loading

0 comments on commit 2cea1e9

Please sign in to comment.