Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter cohorts and concepts by permission #2245

Merged
merged 24 commits into from Jul 28, 2023

Conversation

rkboyce
Copy link
Contributor

@rkboyce rkboyce commented Apr 4, 2023

No description provided.

Comment on lines 95 to 102
@Path("/access/{entityType}/{entityId}")
@Path("/access/{entityType}/{entityId}/{role}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public List<RoleDTO> listAccessesForEntity(
@PathParam("entityType") EntityType entityType,
@PathParam("entityId") Integer entityId
@PathParam("entityId") Integer entityId,
@PathParam("role") String role
) throws Exception {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method has changed in a backwards breaking way, I think we should leave the existing method alone and add a new one that takes the /role as a param. It looks like the default action is write, so we could include a new route that takes role, and call a common method that takes role as params:

listAccessForEntity(...) {
  listAccessForEntityRole(..., AccessType.Write)
}

listAccessForEntityRole(...) {
  /// this method takes the implemnetation for listAccessesForEntity but now handles the role type
}

Also, the param type for role param could be a AccessType enum, but would have to experiment if the /{role} as /WRITE would map properly to the enum.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, there's some overloading of terms 'role' where in this context, it is 'READ', 'WRITE' for 'role' but in other places we have 'Admin' as a role and 'Atlas User' as a role...but the latter roles maps to a shiro 'group' (which you assign permissions to'....we could probably interchange groups and roles, however, the AccessType is neither a group or a role...so...I don't have a solution here, just pointing out the differences in context.

It's almost like we're talking about a 'permission type' .. permission to read, permission to write.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I addressed this the overloading of the 'role' string by using 'permType' as the parameter. The function name is now listAccessesForEntityByPermType used for the /access/{entityType}/{entityId}/{permType} endpoint with WRITE as the default permission type sent from the client. This distinguishes it from listAccessesForEntity which is used for the /access/suggest endpoint. I did not get a chance to test if the AccessType enum would work instead of string "READ" and "WRITE".

@chrisknoll chrisknoll changed the title Filter cohorts and concepts Filter cohorts and concepts by permission Apr 25, 2023
@chrisknoll
Copy link
Collaborator

@rkboyce , I noticed when we did the walkthrough and we enabled the filter on the server, the asset list returned nothing until I created a new asset and it got the read perm assigned to it. I wanted to ask: have we thought about considering 'read access' if you have write? This way, if we enable it, anything I've been granted write access will appear, plus anything that someone grants me read access to. Basically the filter becomes if (canRead || canWrite| then show item. Is that possible or was there a reason why we had to explicitly look for the granted permission? The only reason why I bring this is that it seems if you can write to something then you can also read it...

rkboyce and others added 12 commits June 19, 2023 15:09
…n used to tell the WebAPI to do filtering based on READ permissions. The new property is called security.defaultGlobalReadPermissions
…n used to tell the WebAPI to do filtering based on READ permissions. The new property is called security.defaultGlobalReadPermissions
… on getting the cohortdefinition filtering to work. Still need to fix the case where another user grants read permission to a single cohort definition to another user and then that definition is visible. Currently, somehow, all definitions authored by the granting user are showing in the grantee's Atlas. Once worked out, the task will be to repeat the steps for every one of the following files: src/main/java/org/ohdsi/webapi/security/model/CohortCharacterizationPermissionSchema.java src/main/java/org/ohdsi/webapi/security/model/CohortDefinitionPermissionSchema.java src/main/java/org/ohdsi/webapi/security/model/ConceptSetPermissionSchema.java src/main/java/org/ohdsi/webapi/security/model/EstimationPermissionSchema.java src/main/java/org/ohdsi/webapi/security/model/FeatureAnalysisPermissionSchema.java src/main/java/org/ohdsi/webapi/security/model/IncidenceRatePermissionSchema.java src/main/java/org/ohdsi/webapi/security/model/PathwayAnalysisPermissionSchema.java src/main/java/org/ohdsi/webapi/security/model/PredictionPermissionSchema.java src/main/java/org/ohdsi/webapi/security/model/ReusablePermissionSchema.java src/main/java/org/ohdsi/webapi/security/model/TagPermissionSchema.java
… apply the concept filtering to other artifact types
…D permissions - tested and not working completely. Strange permissions issue that appears to be entirely client-side.
@rkboyce
Copy link
Contributor Author

rkboyce commented Jul 23, 2023

In response to the last comment about "have we thought about considering 'read access' if you have write" -- yes, it is implemented that way and seems to function correctly based on my tests.

This updated draft pull request implements the READ permissions filtering across all the following applications in Atlas:

  • concept sets
  • Cohort definition
  • Cohort characterization
  • Incident Rates
  • Estimation
  • Prediction

All have functioned as expected : things are not accessible to a user unless 1) they create the entity (which sets READ and WRITE), or 2) they are given READ permission to the entity

NOTE: These changes will need to come with a new system role that the admins could select which I am calling the 'Read Restricted Atlas User' role. I think the following query could be the basis for adding SQL to flyway to create this role:

select distinct sp.*
from ohdsi.sec_role_permission srp 
  inner join ohdsi.sec_permission sp on srp.permission_id = sp.id 
where srp.role_id in (6,10) -- 'cohort reader', 'Atlas Users'
 and sp.value not in 
    (  
    	'conceptset:*:get',
    	'conceptset:*:expression:get',
    	'conceptset:*:version:*:expression:get',    	       
    	--
        'cohortdefinition:*:get',
        'cohortdefinition:*:info:get',
        'cohortdefinition:*:version:get',
        'cohortdefinition:*:version:*:get',        
        --        
	        'cohort-characterization:*:get',
                'cohort-characterization:*:generation:get',
		'cohort-characterization:generation:*:get',
		'cohort-characterization:design:get',
		'cohort-characterization:*:design:get',
		'cohort-characterization:design:*:get',
		'cohort-characterization:*:version:get',
		'cohort-characterization:*:version:*:get',
		--
		'pathway-analysis:*:get',
		'pathway-analysis:*:generation:get',
		'pathway-analysis:generation:*:get',
		'pathway-analysis:generation:*:result:get',
		'pathway-analysis:generation:*:design:get',
		'pathway-analysis:*:version:get',
		'pathway-analysis:*:version:*:get'
		--
		'ir:*:get',
		'ir:*:copy:get',
		'ir:*:info:get',
		'ir:*:design:get',
		'ir:*:version:get',
		'ir:*:version:*:get'
		--		
		'estimation:*:get',
		'estimation:*:copy:get',
		'estimation:*:download:get',
		'estimation:*:export:get',
		'estimation:*:generation:get'
               'comparativecohortanalysis:*:get',
		--
		'prediction:*:get',
		'prediction:*:copy:get',
		'prediction:*:download:get',
		'prediction:*:export:get',
		'prediction:*:generation:get',
		'prediction:*:exists:get',
                'plp:*:get'
    )
order by sp.value 
;

pom.xml Outdated
<!-- If defaultGlobalReadPermissions is set to true (default), then all users can see every artifact. -->
<!-- If it is set to false, WebAPI will filter out the artifacts that a user does not explicitly have -->
<!-- read permissions to -->
<security.defaultGlobalReadPermissions>false</security.defaultGlobalReadPermissions>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should default this to true, and rely on settings.xml to override this setting. This will maintain current behavior between versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corrected now and tested that setting the variable false in the WebAPIConfig/settings.xml works

permissionService.fillWriteAccess(entity, dto);
return dto;
});
if (defaultglobalreadpermissions == true) { // don't filter based on read permissions
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a place wehre we want to have proper stayle: defaultGlobalReadPermissions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corrected

Comment on lines 104 to 123
if (defaultglobalreadpermissions == true) { // don't filter based on read permissions
return StreamSupport.stream(service.getAnalysisList().spliterator(), false)
.map(analysis -> {
EstimationShortDTO dto = conversionService.convert(analysis, EstimationShortDTO.class);
permissionService.fillWriteAccess(analysis, dto);
permissionService.fillReadAccess(analysis, dto);
return dto;
})
.collect(Collectors.toList());
} else {
return StreamSupport.stream(service.getAnalysisList().spliterator(), false)
.filter(candidateEstimation -> permissionService.hasReadAccess(candidateEstimation))
.map(analysis -> {
EstimationShortDTO dto = conversionService.convert(analysis, EstimationShortDTO.class);
permissionService.fillWriteAccess(analysis, dto);
permissionService.fillReadAccess(analysis, dto);
return dto;
})
.collect(Collectors.toList());
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this was mentioned before but you can put the filter conditional in the stream expression:

      return StreamSupport.stream(service.getAnalysisList().spliterator(), false)
	.filter(!defaultglobalreadpermissions  ? candidateEstimation -> permissionService.hasReadAccess(candidateEstimation) : candidateEstimation -> true)
	.map(analysis -> {
	    EstimationShortDTO dto = conversionService.convert(analysis, EstimationShortDTO.class);
	    permissionService.fillWriteAccess(analysis, dto);
	    permissionService.fillReadAccess(analysis, dto);
	    return dto;
	  })
	.collect(Collectors.toList());

Comment on lines -95 to +110
@Path("/access/{entityType}/{entityId}")
@Path("/access/{entityType}/{entityId}/{permType}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public List<RoleDTO> listAccessesForEntity(
public List<RoleDTO> listAccessesForEntityByPermType(
@PathParam("entityType") EntityType entityType,
@PathParam("entityId") Integer entityId
@PathParam("entityId") Integer entityId,
@PathParam("permType") String permType
) throws Exception {

permissionService.checkCommonEntityOwnership(entityType, entityId);

Set<String> permissionTemplates = permissionService.getTemplatesForType(entityType, AccessType.WRITE).keySet();
Set<String> permissionTemplates = null;
if (permType == "WRITE") {
permissionTemplates = permissionService.getTemplatesForType(entityType, AccessType.WRITE).keySet();
} else {
permissionTemplates = permissionService.getTemplatesForType(entityType, AccessType.READ).keySet();
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For backwards compatibility, you should maintain the endpoint /access/entityType/entityId, tho this will default to the perm type of 'WRITE'. So, revert this change to leave listAccessForEntity, but have this function call over to 'listAccessForEntityByPermType) with the perType paramater as AccessType.WRITE. In addition, i believe permType should be the enum, not the string, but will need to do some tests if the Java REST api will automatically serialize the ENUM input. The issue is that you could pass a perm type of 'BlahBlah' and it will assume that is 'READ' via the else statement.

Comment on lines 409 to 432

return definitions.stream()
.map(def -> {
CohortMetadataDTO dto = conversionService.convert(def, CohortMetadataImplDTO.class);
permissionService.fillWriteAccess(def, dto);
return dto;
})
.collect(Collectors.toList());
if (defaultglobalreadpermissions == true) { // don't filter based on read permissions
return definitions.stream()
.map(def -> {
CohortMetadataDTO dto = conversionService.convert(def, CohortMetadataImplDTO.class);
permissionService.fillWriteAccess(def, dto);
permissionService.fillReadAccess(def, dto);
return dto;
})
.collect(Collectors.toList());
} else { // filter out cohortdefinitions that the user does not have read access to
return definitions.stream()
.filter(candidateCohortDef -> permissionService.hasReadAccess(candidateCohortDef))
.map(def -> {
CohortMetadataDTO dto = conversionService.convert(def, CohortMetadataImplDTO.class);
permissionService.fillWriteAccess(def, dto);
permissionService.fillReadAccess(def, dto);
return dto;
})
.collect(Collectors.toList());
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use conditional filter here.

Comment on lines 138 to 158
if (defaultglobalreadpermissions == true) { // don't filter based on read permissions
return getTransactionTemplate().execute(
transactionStatus -> StreamSupport.stream(getConceptSetRepository().findAll().spliterator(), false)
.map(conceptSet -> {
ConceptSetDTO dto = conversionService.convert(conceptSet, ConceptSetDTO.class);
permissionService.fillWriteAccess(conceptSet, dto);
permissionService.fillReadAccess(conceptSet, dto);
return dto;
})
.collect(Collectors.toList()));
} else { // filter out conceptsets that the user does not have read access to
return getTransactionTemplate().execute(
transactionStatus -> StreamSupport.stream(getConceptSetRepository().findAll().spliterator(), false)
.filter(candidateConceptSet -> permissionService.hasReadAccess(candidateConceptSet))
.map(conceptSet -> {
ConceptSetDTO dto = conversionService.convert(conceptSet, ConceptSetDTO.class);
permissionService.fillWriteAccess(conceptSet, dto);
permissionService.fillReadAccess(conceptSet, dto);
return dto;
})
.collect(Collectors.toList()));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use conditional filter here.

Comment on lines 348 to 372
if (defaultglobalreadpermissions == true) { // don't filter based on read permissions
return getTransactionTemplate().execute(transactionStatus -> {
Iterable<IncidenceRateAnalysis> analysisList = this.irAnalysisRepository.findAll();
return StreamSupport.stream(analysisList.spliterator(), false)
.map(analysis -> {
IRAnalysisShortDTO dto = conversionService.convert(analysis, IRAnalysisShortDTO.class);
permissionService.fillWriteAccess(analysis, dto);
permissionService.fillReadAccess(analysis, dto);
return dto;
})
.collect(Collectors.toList());
});
.collect(Collectors.toList());
});
} else { // filter out entities that the user does not have read permissions to view
return getTransactionTemplate().execute(transactionStatus -> {
Iterable<IncidenceRateAnalysis> analysisList = this.irAnalysisRepository.findAll();
return StreamSupport.stream(analysisList.spliterator(), false)
.filter(candidateIRAnalysis -> permissionService.hasReadAccess(candidateIRAnalysis))
.map(analysis -> {
IRAnalysisShortDTO dto = conversionService.convert(analysis, IRAnalysisShortDTO.class);
permissionService.fillWriteAccess(analysis, dto);
permissionService.fillReadAccess(analysis, dto);
return dto;
})
.collect(Collectors.toList());
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use conditional filter here, will simplify code so that you only have the one getTransactionTemplarte() call, but within the stream you filter conditionally before mapping.

src/main/resources/application.properties Outdated Show resolved Hide resolved
Fixed the default value of DefaultGlobalReadPermissions to be true in pom.xm and to be camel case formatted throughout the code
@rkboyce
Copy link
Contributor Author

rkboyce commented Jul 25, 2023

A detail worth mentioning - were were curious what would happen if a person without read access knew the URL to an entity and tried a GET. I tested as follows:

  1. logged in as an admin, created a brand new entity (did it for a few types e.g., cohort definition, estimation, etc)

  2. copied the URL

  3. logged out from the admin account and logged in to a user account without access to the entity

  4. put the URL in the browser

The result is an empty page with no message at all. The server simply returns a 403 with an empty body in the response. Atlas renders nothing. So, I think it is Ok for now.

@chrisknoll
Copy link
Collaborator

Yes, that's fine. Ok, if you're done with your part, I will merge this into a new feature branch under WebAPI so I can put in the final changes. Let me know if you are ready for me to do that.

@chrisknoll chrisknoll changed the base branch from master to issue-2300 July 28, 2023 18:40
@chrisknoll chrisknoll dismissed their stale review July 28, 2023 18:41

Merging into feature branch in WebAPI.

@chrisknoll chrisknoll marked this pull request as ready for review July 28, 2023 18:41
@chrisknoll chrisknoll merged commit 550c088 into OHDSI:issue-2300 Jul 28, 2023
2 checks passed
@rkboyce
Copy link
Contributor Author

rkboyce commented Jul 28, 2023

All good here!

@chrisknoll
Copy link
Collaborator

The new PRs are ready, if you could pull both the Atlas and WebAPI branches, @rkboyce , and give it a shot in your local env, and report in the other PR threads if it is good or if you found issues. Thanks.

chrisknoll added a commit that referenced this pull request Aug 14, 2023
The new property is called security.defaultGlobalReadPermissions which is true by default.

Co-authored-by: Richard D Boyce, PhD <rdb20@pitt.edu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants