diff --git a/src/main/java/org/ohdsi/webapi/cohortcharacterization/CcController.java b/src/main/java/org/ohdsi/webapi/cohortcharacterization/CcController.java index 47a9c36ee..d3d1f098a 100644 --- a/src/main/java/org/ohdsi/webapi/cohortcharacterization/CcController.java +++ b/src/main/java/org/ohdsi/webapi/cohortcharacterization/CcController.java @@ -500,7 +500,10 @@ public void unassignTag(@PathVariable("id") final long id, @PathVariable("tagId" */ @PostMapping(value = "/{id}/protectedtag") @Transactional - @PreAuthorize("isOwner(#id, COHORT_CHARACTERIZATION) or isPermitted('admin:tags') or isPermitted('write:cohort-characterization') or hasEntityAccess(#id, COHORT_CHARACTERIZATION, WRITE)") + @PreAuthorize(""" + (isOwner(#id, COHORT_CHARACTERIZATION) or isPermitted('write:cohort-characterization') or hasEntityAccess(#id, COHORT_CHARACTERIZATION, WRITE)) + and isPermitted('admin:tags') + """) public void assignPermissionProtectedTag(@PathVariable("id") final long id, @RequestBody final int tagId) { service.assignTag(id, tagId); } @@ -513,7 +516,10 @@ public void assignPermissionProtectedTag(@PathVariable("id") final long id, @Req */ @DeleteMapping(value = "/{id}/protectedtag/{tagId}") @Transactional - @PreAuthorize("isOwner(#id, COHORT_CHARACTERIZATION) or isPermitted('admin:tags') or isPermitted('write:cohort-characterization') or hasEntityAccess(#id, COHORT_CHARACTERIZATION, WRITE)") + @PreAuthorize(""" + (isOwner(#id, COHORT_CHARACTERIZATION) or isPermitted('write:cohort-characterization') or hasEntityAccess(#id, COHORT_CHARACTERIZATION, WRITE)) + and isPermitted('admin:tags') + """) public void unassignPermissionProtectedTag(@PathVariable("id") final long id, @PathVariable("tagId") final int tagId) { service.unassignTag(id, tagId); } diff --git a/src/main/java/org/ohdsi/webapi/cohortdefinition/CohortDefinitionService.java b/src/main/java/org/ohdsi/webapi/cohortdefinition/CohortDefinitionService.java index e545d21cd..7565040ea 100644 --- a/src/main/java/org/ohdsi/webapi/cohortdefinition/CohortDefinitionService.java +++ b/src/main/java/org/ohdsi/webapi/cohortdefinition/CohortDefinitionService.java @@ -1062,7 +1062,10 @@ public void unassignTag(@PathVariable("id") final Integer id, @PathVariable("tag */ @PostMapping(value = "/{id}/protectedtag", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - @PreAuthorize("isOwner(#id, COHORT_DEFINITION) or isPermitted('admin:tags') or isPermitted('write:cohort-definition') or hasEntityAccess(#id, COHORT_DEFINITION, WRITE)") + @PreAuthorize(""" + (isOwner(#id, COHORT_DEFINITION) or isPermitted('write:cohort-definition') or hasEntityAccess(#id, COHORT_DEFINITION, WRITE)) + and isPermitted('admin:tags') + """) public void assignPermissionProtectedTag(@PathVariable("id") final int id, @RequestBody final int tagId) { assignTag(id, tagId); } @@ -1076,7 +1079,10 @@ public void assignPermissionProtectedTag(@PathVariable("id") final int id, @Requ */ @DeleteMapping(value = "/{id}/protectedtag/{tagId}", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - @PreAuthorize("isOwner(#id, COHORT_DEFINITION) or isPermitted('admin:tags') or isPermitted('write:cohort-definition') or hasEntityAccess(#id, COHORT_DEFINITION, WRITE)") + @PreAuthorize(""" + (isOwner(#id, COHORT_DEFINITION) or isPermitted('write:cohort-definition') or hasEntityAccess(#id, COHORT_DEFINITION, WRITE)) + and isPermitted('admin:tags') + """) public void unassignPermissionProtectedTag(@PathVariable("id") final int id, @PathVariable("tagId") final int tagId) { unassignTag(id, tagId); } diff --git a/src/main/java/org/ohdsi/webapi/conceptset/ConceptSetService.java b/src/main/java/org/ohdsi/webapi/conceptset/ConceptSetService.java index d794ea0b8..ccf839257 100644 --- a/src/main/java/org/ohdsi/webapi/conceptset/ConceptSetService.java +++ b/src/main/java/org/ohdsi/webapi/conceptset/ConceptSetService.java @@ -717,7 +717,10 @@ public void unassignTag( */ @PostMapping(value = "/{id}/protectedtag", consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - @PreAuthorize("isOwner(#id, CONCEPT_SET) or isPermitted('write:conceptset') or isPermitted('admin:tags') or hasEntityAccess(#id, CONCEPT_SET, WRITE)") + @PreAuthorize(""" + (isOwner(#id, CONCEPT_SET) or isPermitted('write:conceptset') or hasEntityAccess(#id, CONCEPT_SET, WRITE)) + and isPermitted('admin:tags') + """) public void assignPermissionProtectedTag( @PathVariable("id") final int id, @RequestBody final int tagId) { @@ -735,7 +738,10 @@ public void assignPermissionProtectedTag( @DeleteMapping(value = "/{id}/protectedtag/{tagId}") @Transactional @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) - @PreAuthorize("isOwner(#id, CONCEPT_SET) or isPermitted('write:conceptset') or isPermitted('admin:tags') or hasEntityAccess(#id, CONCEPT_SET, WRITE)") + @PreAuthorize(""" + (isOwner(#id, CONCEPT_SET) or isPermitted('write:conceptset') or hasEntityAccess(#id, CONCEPT_SET, WRITE)) + and isPermitted('admin:tags') + """) public void unassignPermissionProtectedTag( @PathVariable("id") final int id, @PathVariable("tagId") final int tagId) { diff --git a/src/main/java/org/ohdsi/webapi/ircalc/IRAnalysisResource.java b/src/main/java/org/ohdsi/webapi/ircalc/IRAnalysisResource.java index 7d9d540bc..23aa8c057 100644 --- a/src/main/java/org/ohdsi/webapi/ircalc/IRAnalysisResource.java +++ b/src/main/java/org/ohdsi/webapi/ircalc/IRAnalysisResource.java @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.*; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import java.util.List; diff --git a/src/main/java/org/ohdsi/webapi/ircalc/IRAnalysisService.java b/src/main/java/org/ohdsi/webapi/ircalc/IRAnalysisService.java index 47a78e38e..c49ea73a3 100644 --- a/src/main/java/org/ohdsi/webapi/ircalc/IRAnalysisService.java +++ b/src/main/java/org/ohdsi/webapi/ircalc/IRAnalysisService.java @@ -804,14 +804,14 @@ public void unassignTag(final Integer id, final int tagId) { @Override @Transactional - @PreAuthorize("isOwner(#id, INCIDENCE_RATE) or isPermitted('admin:tags') or isPermitted('write:incidence') or hasEntityAccess(#id, INCIDENCE_RATE, WRITE)") + @PreAuthorize("(isOwner(#id, INCIDENCE_RATE) or isPermitted('write:incidence') or hasEntityAccess(#id, INCIDENCE_RATE, WRITE)) and isPermitted('admin:tags')") public void assignPermissionProtectedTag(final int id, final int tagId) { assignTag(id, tagId); } @Override @Transactional - @PreAuthorize("isOwner(#id, INCIDENCE_RATE) or isPermitted('admin:tags') or isPermitted('write:incidence') or hasEntityAccess(#id, INCIDENCE_RATE, WRITE)") + @PreAuthorize("(isOwner(#id, INCIDENCE_RATE) or isPermitted('write:incidence') or hasEntityAccess(#id, INCIDENCE_RATE, WRITE)) and isPermitted('admin:tags')") public void unassignPermissionProtectedTag(final int id, final int tagId) { unassignTag(id, tagId); } diff --git a/src/main/java/org/ohdsi/webapi/pathway/PathwayController.java b/src/main/java/org/ohdsi/webapi/pathway/PathwayController.java index 477d0a2df..403f64c03 100644 --- a/src/main/java/org/ohdsi/webapi/pathway/PathwayController.java +++ b/src/main/java/org/ohdsi/webapi/pathway/PathwayController.java @@ -499,7 +499,7 @@ public void unassignTag(@PathVariable("id") final int id, @PathVariable("tagId") */ @PostMapping(value = "/{id}/protectedtag", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - @PreAuthorize("isOwner(#id, PATHWAY_ANALYSIS) or isPermitted('admin:tags') or isPermitted('write:pathway') or hasEntityAccess(#id, PATHWAY_ANALYSIS, WRITE)") + @PreAuthorize("(isOwner(#id, PATHWAY_ANALYSIS) or isPermitted('write:pathway') or hasEntityAccess(#id, PATHWAY_ANALYSIS, WRITE)) and isPermitted('admin:tags')") public void assignPermissionProtectedTag(@PathVariable("id") final int id, @RequestBody final int tagId) { pathwayService.assignTag(id, tagId); } @@ -513,7 +513,7 @@ public void assignPermissionProtectedTag(@PathVariable("id") final int id, @Requ */ @DeleteMapping(value = "/{id}/protectedtag/{tagId}") @Transactional - @PreAuthorize("isOwner(#id, PATHWAY_ANALYSIS) or isPermitted('admin:tags') or isPermitted('write:pathway') or hasEntityAccess(#id, PATHWAY_ANALYSIS, WRITE)") + @PreAuthorize("(isOwner(#id, PATHWAY_ANALYSIS) or isPermitted('write:pathway') or hasEntityAccess(#id, PATHWAY_ANALYSIS, WRITE)) and isPermitted('admin:tags')") public void unassignPermissionProtectedTag(@PathVariable("id") final int id, @PathVariable("tagId") final int tagId) { pathwayService.unassignTag(id, tagId); } diff --git a/src/main/java/org/ohdsi/webapi/source/SourceService.java b/src/main/java/org/ohdsi/webapi/source/SourceService.java index 60c473007..1740d3ad4 100644 --- a/src/main/java/org/ohdsi/webapi/source/SourceService.java +++ b/src/main/java/org/ohdsi/webapi/source/SourceService.java @@ -6,28 +6,26 @@ import org.jasypt.encryption.pbe.PBEStringEncryptor; import org.jasypt.properties.PropertyValueEncryptionUtils; import org.ohdsi.sql.SqlTranslate; -import org.ohdsi.webapi.arachne.logging.event.AddDataSourceEvent; -import org.ohdsi.webapi.arachne.logging.event.ChangeDataSourceEvent; -import org.ohdsi.webapi.arachne.logging.event.DeleteDataSourceEvent; import org.ohdsi.webapi.common.DBMSType; import org.ohdsi.webapi.common.SourceMapKey; import org.ohdsi.webapi.exception.SourceDuplicateKeyException; import org.ohdsi.webapi.security.authz.AuthorizationService; +import org.ohdsi.webapi.security.authz.RoleEntity; import org.ohdsi.webapi.security.authz.UserEntity; -import org.ohdsi.webapi.security.identity.WebApiPrincipal; +import org.ohdsi.webapi.security.authz.access.AccessType; +import org.ohdsi.webapi.security.authz.access.EntityType; import org.ohdsi.webapi.service.AbstractDaoService; import org.ohdsi.webapi.service.VocabularyService; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.jdbc.CannotGetJdbcConnectionException; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.security.core.Authentication; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Component; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Transactional; @@ -47,9 +45,6 @@ import org.ohdsi.webapi.util.CacheHelper; -/** - * TODO: Need to add permission annotations to these methods - */ @RestController @RequestMapping("/source") @Transactional @@ -90,7 +85,6 @@ public void customize(CacheManager cacheManager) { private final GenericConversionService conversionService; private final VocabularyService vocabularyService; - private final ApplicationEventPublisher publisher; public SourceService(SourceRepository sourceRepository, SourceDaimonRepository sourceDaimonRepository, @@ -98,8 +92,7 @@ public SourceService(SourceRepository sourceRepository, JdbcTemplate jdbcTemplate, PBEStringEncryptor defaultStringEncryptor, GenericConversionService conversionService, - @Lazy VocabularyService vocabularyService, - ApplicationEventPublisher publisher) { + @Lazy VocabularyService vocabularyService) { this.sourceRepository = sourceRepository; this.sourceDaimonRepository = sourceDaimonRepository; this.authorizationService = authorizationService; @@ -107,7 +100,6 @@ public SourceService(SourceRepository sourceRepository, this.defaultStringEncryptor = defaultStringEncryptor; this.conversionService = conversionService; this.vocabularyService = vocabularyService; - this.publisher = publisher; } @EventListener(ApplicationReadyEvent.class) @@ -205,6 +197,7 @@ public ResponseEntity getSource(@PathVariable("key") final String so * @return */ @GetMapping(value = "/details/{sourceId}", produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("isPermitted('admin:source')") public ResponseEntity getSourceDetails(@PathVariable("sourceId") Integer sourceId) { Source source = sourceRepository.findBySourceId(sourceId); return ResponseEntity.ok(new SourceDetails(source)); @@ -221,6 +214,7 @@ public ResponseEntity getSourceDetails(@PathVariable("sourceId") */ @PostMapping(value = "", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @CacheEvict(cacheNames = CachingSetup.SOURCE_LIST_CACHE, allEntries = true) + @PreAuthorize("isPermitted('admin:source')") public ResponseEntity createSource( @RequestPart(value = "keyfile", required = false) MultipartFile keyfile, @RequestPart("source") SourceRequest source) throws Exception { @@ -258,9 +252,13 @@ public ResponseEntity createSource( sourceEntity.setCreatedDate(new Date()); try { Source saved = sourceRepository.saveAndFlush(sourceEntity); + // Sources have a role to grant write access to the source + AuthorizationService authSvc = this.getAuthorizationService(); + RoleEntity sourceRole = authSvc.addRole(getSourceRoleName(saved.getSourceKey()), true); + // we are going to default giving this role 'WRITE', but could possibly define a read-only and a writeable roles in the future. + authSvc.grantEntityAccess(EntityType.SOURCE, Long.valueOf(saved.getId().longValue()),sourceRole.getId(), AccessType.WRITE); invalidateCache(); SourceInfo sourceInfo = new SourceInfo(saved); - publisher.publishEvent(new AddDataSourceEvent(this, sourceEntity.getSourceId(), sourceEntity.getSourceName())); return ResponseEntity.ok(sourceInfo); } catch (PersistenceException ex) { throw new SourceDuplicateKeyException("You cannot use this Source Key, please use different one"); @@ -280,6 +278,7 @@ public ResponseEntity createSource( @PutMapping(value = "/{sourceId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @Transactional @CacheEvict(cacheNames = CachingSetup.SOURCE_LIST_CACHE, allEntries = true) + @PreAuthorize("isPermitted('admin:source')") public ResponseEntity updateSource( @PathVariable("sourceId") Integer sourceId, @RequestPart(value = "keyfile", required = false) MultipartFile keyfile, @@ -313,7 +312,6 @@ public ResponseEntity updateSource( // Delete MUST be called after fetching user or source data to prevent autoflush (see DefaultPersistEventListener.onPersist) sourceDaimonRepository.deleteAll(removed); Source result = sourceRepository.save(updated); - publisher.publishEvent(new ChangeDataSourceEvent(this, updated.getSourceId(), updated.getSourceName())); invalidateCache(); return ResponseEntity.ok(new SourceInfo(result)); } else { @@ -332,12 +330,13 @@ public ResponseEntity updateSource( @DeleteMapping("/{sourceId}") @Transactional @CacheEvict(cacheNames = CachingSetup.SOURCE_LIST_CACHE, allEntries = true) + @PreAuthorize("isPermitted('admin:source')") public ResponseEntity delete(@PathVariable("sourceId") Integer sourceId) throws Exception { Source source = sourceRepository.findBySourceId(sourceId); if (source != null) { sourceRepository.delete(source); - publisher.publishEvent(new DeleteDataSourceEvent(this, sourceId, source.getSourceName())); + // TODO: Deletes are 'soft-delete' so need to determine how to clean up the source's role. invalidateCache(); return ResponseEntity.ok().build(); } else { @@ -355,6 +354,7 @@ public ResponseEntity delete(@PathVariable("sourceId") Integer sourceId) t */ @GetMapping(value = "/connection/{key}", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional(noRollbackFor = CannotGetJdbcConnectionException.class) + @PreAuthorize("isPermitted('admin:source') or hasSourceAccess(#sourceKey, READ)") public ResponseEntity checkConnectionEndpoint(@PathVariable("key") final String sourceKey) { final Source source = findBySourceKey(sourceKey); checkConnection(source); @@ -389,6 +389,7 @@ public ResponseEntity> getPriorityDaimo */ @PostMapping(value = "/{sourceKey}/daimons/{daimonType}/set-priority", produces = MediaType.APPLICATION_JSON_VALUE) @CacheEvict(cacheNames = CachingSetup.SOURCE_LIST_CACHE, allEntries = true) + @PreAuthorize("isPermitted('admin:source')") public ResponseEntity updateSourcePriority( @PathVariable("sourceKey") final String sourceKey, @PathVariable("daimonType") final String daimonTypeName) { @@ -552,6 +553,10 @@ private boolean checkConnectionSafe(Source source) { } } + private String getSourceRoleName(String sourceKey) { + return String.format("Source user (%s)", sourceKey); + } + private class SortByKey implements Comparator { private boolean isAscending; diff --git a/src/main/java/org/ohdsi/webapi/tag/TagService.java b/src/main/java/org/ohdsi/webapi/tag/TagService.java index adb994feb..d8ce78d60 100644 --- a/src/main/java/org/ohdsi/webapi/tag/TagService.java +++ b/src/main/java/org/ohdsi/webapi/tag/TagService.java @@ -1,6 +1,7 @@ package org.ohdsi.webapi.tag; import org.apache.commons.lang3.StringUtils; +import org.ohdsi.webapi.security.authz.AuthorizationService; import org.ohdsi.webapi.service.AbstractDaoService; import org.ohdsi.webapi.tag.domain.Tag; import org.ohdsi.webapi.tag.domain.TagInfo; @@ -260,11 +261,11 @@ private void findParentGroup(Set groups, Set groupIds) { */ @GetMapping(value = "/assignmentPermissions", produces = MediaType.APPLICATION_JSON_VALUE) public AssignmentPermissionsDTO getAssignmentPermissions() { + AuthorizationService authSvc = this.getAuthorizationService(); final AssignmentPermissionsDTO tagPermission = new AssignmentPermissionsDTO(); - // TODO: determine what permission rules are being checked here for WebAPI 3.0 semantics. - // tagPermission.setAnyAssetMultiAssignPermitted(isAdmin()); - // tagPermission.setCanAssignProtectedTags(!isSecured() || TagSecurityUtils.canAssingProtectedTags()); - // tagPermission.setCanUnassignProtectedTags(!isSecured() || TagSecurityUtils.canUnassingProtectedTags()); + tagPermission.setAnyAssetMultiAssignPermitted(authSvc.isPermitted("admin:tags")); + tagPermission.setCanAssignProtectedTags(authSvc.isPermitted("admin:tags")); + tagPermission.setCanUnassignProtectedTags(authSvc.isPermitted("admin:tags")); return tagPermission; } diff --git a/src/main/resources/db/migration/postgresql/B3.0.0__webapi_baseline.sql b/src/main/resources/db/migration/postgresql/B3.0.0__webapi_baseline.sql index 6f5fef702..120635e56 100644 --- a/src/main/resources/db/migration/postgresql/B3.0.0__webapi_baseline.sql +++ b/src/main/resources/db/migration/postgresql/B3.0.0__webapi_baseline.sql @@ -2038,6 +2038,7 @@ FROM ( ('admin:tools', 'Manage Tools'), ('admin:security', 'Manage users, roles, permissions'), ('admin:cache', 'View and manage chache functions'), + ('admin:run-as', 'Run as another user'), ('create', 'Create any asset'), ('create:conceptset', 'Create concept sets'), ('create:cohort-definition', 'Create cohort definitions'), @@ -2072,7 +2073,7 @@ INSERT INTO ${ohdsiSchema}.sec_user (id, login, name, origin) VALUES (-1, 'anonymous', 'Anonymous', 'SYSTEM'); INSERT INTO ${ohdsiSchema}.sec_role (id, name, system_role) -VALUES (-1, 'anonymous', false); +VALUES (-1, 'anonymous', true); INSERT INTO ${ohdsiSchema}.sec_user_role (id, user_id, role_id, origin) VALUES (nextval('${ohdsiSchema}.sec_user_role_sequence'), -1, -1, 'SYSTEM'); diff --git a/src/main/resources/db/migration/postgresql/V2.99.0002__spring_security_migration.sql b/src/main/resources/db/migration/postgresql/V2.99.0002__spring_security_migration.sql index 4ac32c84a..a5a5d364c 100644 --- a/src/main/resources/db/migration/postgresql/V2.99.0002__spring_security_migration.sql +++ b/src/main/resources/db/migration/postgresql/V2.99.0002__spring_security_migration.sql @@ -6,7 +6,7 @@ INSERT INTO ${ohdsiSchema}.sec_user (id, login, name, origin) VALUES (-1, 'anonymous', 'Anonymous', 'SYSTEM'); INSERT INTO ${ohdsiSchema}.sec_role (id, name, system_role) -VALUES (-1, 'anonymous', false); +VALUES (-1, 'anonymous', true); INSERT INTO ${ohdsiSchema}.sec_user_role (id, user_id, role_id, origin) VALUES (nextval('${ohdsiSchema}.sec_user_role_sequence'), -1, -1, 'SYSTEM'); @@ -920,6 +920,7 @@ FROM ( ('admin:tools', 'Manage Tools'), ('admin:security', 'Manage users, roles, permissions'), ('admin:cache', 'View and manage chache functions'), + ('admin:run-as', 'Run as another user'), ('create', 'Create any asset'), ('create:conceptset', 'Create concept sets'), ('create:cohort-definition', 'Create cohort definitions'), @@ -956,14 +957,16 @@ WITH perm_map AS ( /* ---------- ADMIN-Privs ---------- */ WHEN p.value ~ '^source:\*:(put|post|delete)$' THEN 'admin:source' - WHEN p.value ~ '^(role:post|role:\*:(put|delete)|role:\*:users:\*:(put|delete)|role:\*:permissions:\*:(put|delete)|user:import:\*:(post|put|delete)|user:runas:post)$' + WHEN p.value ~ '^(role:post|role:\*:(put|delete)|role:\*:users:\*:(put|delete)|role:\*:permissions:\*:(put|delete)|user:import:\*:(post|put|delete))$' THEN 'admin:security' WHEN p.value ~ '^(tag:\*:(put|delete)|tag:(post|management)|tag:multi(Assign|Unassign):post)$' THEN 'admin:tags' WHEN p.value ~ '^(tool:\*:(put|delete)|tool:(post|put))$' THEN 'admin:tools' WHEN p.value ~ '^(cache:.*|cdmresults:clearcache:post)$' - THEN 'admin:cache' + THEN 'admin:cache' + WHEN p.value ~ '^(user:runas:post)$' + THEN 'admin:run-as' /* ---------- CREATE ---------- */ WHEN p.value ~ '^conceptset:post$'