-
Notifications
You must be signed in to change notification settings - Fork 168
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
Filter cohorts and concepts by permission #2245
Conversation
…a user only sees what their role has permission to read. This initial commit has a big issue in that a person who authors a conceptset cannot see the concept set unless a new permission is added. To be fixed.
…i-ohdsi/WebAPI into filter_cohorts_and_concepts
@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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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".
@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 |
merging upstream
…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
…efinitions. Tests OK
…D permissions - tested and not working completely. Strange permissions issue that appears to be entirely client-side.
…timation but that were not obvious previously
…' string to a more clear 'permType
…' string to a more clear 'permType
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:
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:
|
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> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
src/main/java/org/ohdsi/webapi/cohortcharacterization/CcController.java
Outdated
Show resolved
Hide resolved
permissionService.fillWriteAccess(entity, dto); | ||
return dto; | ||
}); | ||
if (defaultglobalreadpermissions == true) { // don't filter based on read permissions |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
corrected
src/main/java/org/ohdsi/webapi/estimation/EstimationController.java
Outdated
Show resolved
Hide resolved
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()); | ||
} |
There was a problem hiding this comment.
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());
@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(); | ||
} |
There was a problem hiding this comment.
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.
|
||
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()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use conditional filter here.
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())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use conditional filter here.
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()); | ||
}); |
There was a problem hiding this comment.
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.
Fixed the default value of DefaultGlobalReadPermissions to be true in pom.xm and to be camel case formatted throughout the code
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:
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. |
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. |
All good here! |
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. |
No description provided.