From c8efc0d524fd009dc8936304077a74afd9cace44 Mon Sep 17 00:00:00 2001 From: Richard Vowles Date: Mon, 9 Dec 2024 19:58:09 +1300 Subject: [PATCH] update app strategy role permissions for API --- .../io/featurehub/db/api/ApplicationApi.kt | 1 + .../io/featurehub/db/services/Conversions.kt | 2 + .../db/services/ApplicationSqlApi.kt | 22 ++- .../featurehub/mr/utils/ApplicationUtils.java | 96 -------------- .../ApplicationRolloutStrategyResource.kt | 15 ++- .../featurehub/mr/utils/ApplicationUtils.kt | 125 ++++++++++++++++++ 6 files changed, 157 insertions(+), 104 deletions(-) delete mode 100644 backend/mr/src/main/java/io/featurehub/mr/utils/ApplicationUtils.java create mode 100644 backend/mr/src/main/kotlin/io/featurehub/mr/utils/ApplicationUtils.kt diff --git a/backend/mr-db-api/src/main/kotlin/io/featurehub/db/api/ApplicationApi.kt b/backend/mr-db-api/src/main/kotlin/io/featurehub/db/api/ApplicationApi.kt index 432602ccb..105da657c 100644 --- a/backend/mr-db-api/src/main/kotlin/io/featurehub/db/api/ApplicationApi.kt +++ b/backend/mr-db-api/src/main/kotlin/io/featurehub/db/api/ApplicationApi.kt @@ -47,4 +47,5 @@ interface ApplicationApi { fun personIsFeatureCreator(appId: UUID, personId: UUID): Boolean fun findFeatureReaders(appId: UUID): Set fun personIsFeatureReader(appId: UUID, personId: UUID): Boolean + fun personApplicationRoles(appId: UUID, personId: UUID): Set } diff --git a/backend/mr-db-services/src/main/kotlin/io/featurehub/db/services/Conversions.kt b/backend/mr-db-services/src/main/kotlin/io/featurehub/db/services/Conversions.kt index 95017dd0b..1be2bbc65 100644 --- a/backend/mr-db-services/src/main/kotlin/io/featurehub/db/services/Conversions.kt +++ b/backend/mr-db-services/src/main/kotlin/io/featurehub/db/services/Conversions.kt @@ -75,7 +75,9 @@ interface Conversions { fun toPortfolio(p: DbPortfolio?, opts: Opts?, person: Person?, personNotSuperAdmin: Boolean): Portfolio? fun toPortfolio(p: DbPortfolio?, opts: Opts?, personId: UUID?, personNotSuperAdmin: Boolean): Portfolio? fun toOrganization(org: DbOrganization?, opts: Opts?): Organization? + // actually isPersonPortfolioAdmin fun isPersonApplicationAdmin(dbPerson: DbPerson?, app: DbApplication?): Boolean + // actually isPersonPortfolioAdmin fun isPersonApplicationAdmin(personId: UUID, appId: UUID): Boolean fun toServiceAccount(sa: DbServiceAccount?, opts: Opts?): ServiceAccount? fun toServiceAccount( diff --git a/backend/mr-db-sql/src/main/kotlin/io/featurehub/db/services/ApplicationSqlApi.kt b/backend/mr-db-sql/src/main/kotlin/io/featurehub/db/services/ApplicationSqlApi.kt index 080281663..7c9048354 100644 --- a/backend/mr-db-sql/src/main/kotlin/io/featurehub/db/services/ApplicationSqlApi.kt +++ b/backend/mr-db-sql/src/main/kotlin/io/featurehub/db/services/ApplicationSqlApi.kt @@ -445,10 +445,10 @@ class ApplicationSqlApi @Inject constructor( override fun findApplicationPermissions(appId: UUID, personId: UUID): ApplicationPermissions { // superusers get everything if (convertUtils.personIsSuperAdmin(personId) || convertUtils.isPersonApplicationAdmin(personId, appId) ) { - val allRoles = RoleType.values().toList() + val allRoles = RoleType.entries return ApplicationPermissions() - .applicationRoles(ApplicationRoleType.values().toList()) + .applicationRoles(ApplicationRoleType.entries) .environments(QDbEnvironment() .select(QDbEnvironment.Alias.name, QDbEnvironment.Alias.id) .whenArchived.isNull @@ -517,6 +517,24 @@ class ApplicationSqlApi @Inject constructor( return HashSet() } + override fun personApplicationRoles(appId: UUID, personId: UUID): Set { + // if they are a super user or portfolio admin, they have all roles + if (convertUtils.personIsSuperAdmin(personId) || convertUtils.isPersonApplicationAdmin(personId, appId)) { + return ApplicationRoleType.entries.toSet() + } + + val roles = mutableSetOf() + + QDbAcl() + .application.id.eq(appId) + .group.whenArchived.isNull() + .group.groupMembers.person.id.eq(personId).findList().forEach { acl -> + roles.addAll(convertUtils.splitApplicationRoles(acl.roles)) + } + + return roles + } + override fun findFeatureReaders(appId: UUID): Set { Conversions.nonNullApplicationId(appId) val featureReaders: MutableSet = HashSet() diff --git a/backend/mr/src/main/java/io/featurehub/mr/utils/ApplicationUtils.java b/backend/mr/src/main/java/io/featurehub/mr/utils/ApplicationUtils.java deleted file mode 100644 index 8d44d0ec9..000000000 --- a/backend/mr/src/main/java/io/featurehub/mr/utils/ApplicationUtils.java +++ /dev/null @@ -1,96 +0,0 @@ -package io.featurehub.mr.utils; - -import io.featurehub.db.api.ApplicationApi; -import io.featurehub.db.api.Opts; -import io.featurehub.mr.auth.AuthManagerService; -import io.featurehub.mr.model.Application; -import io.featurehub.mr.model.Person; -import jakarta.inject.Inject; -import jakarta.ws.rs.ForbiddenException; -import jakarta.ws.rs.NotFoundException; -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.SecurityContext; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.UUID; - -public class ApplicationUtils { - private static final Logger log = LoggerFactory.getLogger(ApplicationUtils.class); - @NotNull - private final AuthManagerService authManager; - @NotNull - private final ApplicationApi applicationApi; - - @Inject - public ApplicationUtils(@NotNull AuthManagerService authManager, @NotNull ApplicationApi applicationApi) { - this.authManager = authManager; - this.applicationApi = applicationApi; - } - - public ApplicationPermissionCheck check(@NotNull SecurityContext securityContext, @NotNull UUID id) { - return check(securityContext, id, Opts.empty()); - } - - public ApplicationPermissionCheck check(@NotNull SecurityContext securityContext, @NotNull UUID id, @NotNull Opts opts) { - Person current = authManager.from(securityContext); - - return check(current, id, opts); - } - - public ApplicationPermissionCheck check(@NotNull Person current, @NotNull UUID id, @NotNull Opts opts) throws WebApplicationException { - - Application app = applicationApi.getApplication(id, opts); - - if (app == null) { - throw new NotFoundException(); - } - - if (authManager.isOrgAdmin(current) || authManager.isPortfolioAdmin(app.getPortfolioId(), current, null)) { - return new ApplicationPermissionCheck(current, app); - } else { - throw new ForbiddenException(); - } - } - - @NotNull public ApplicationPermissionCheck featureCreatorCheck(@NotNull SecurityContext securityContext, - @NotNull UUID appId) throws WebApplicationException { - Person current = authManager.from(securityContext); - - if (!applicationApi.personIsFeatureCreator(appId, current.getId().getId())) { - log.warn("Attempt by person {} to edit features in application {}", current.getId().getId(), appId); - - return check(current, appId, Opts.empty()); - } else { - return new ApplicationPermissionCheck(current, new Application().id(appId)); - } - } - - /** - * This just checks to see if a person has the Editor/Delete permission and if not, throws an exception. A portfolio - * admin or admin will always have it. - * - * @param securityContext - * @param appId - */ - public void featureEditorCheck(@NotNull SecurityContext securityContext, @NotNull UUID appId) { - Person current = authManager.from(securityContext); - - if (!applicationApi.personIsFeatureEditor(appId, current.getId().getId())) { - log.warn("Attempt by person {} to edt features in application {}", current.getId().getId(), appId); - - check(current, appId, Opts.empty()); - } - } - - public Person featureReadCheck(@NotNull SecurityContext securityContext, @NotNull UUID id) { - Person current = authManager.from(securityContext); - - if (!applicationApi.personIsFeatureReader(id, current.getId().getId())) { - throw new ForbiddenException(); - } - - return current; - } -} diff --git a/backend/mr/src/main/kotlin/io/featurehub/mr/resources/ApplicationRolloutStrategyResource.kt b/backend/mr/src/main/kotlin/io/featurehub/mr/resources/ApplicationRolloutStrategyResource.kt index dd5e6a0b8..c35ade101 100644 --- a/backend/mr/src/main/kotlin/io/featurehub/mr/resources/ApplicationRolloutStrategyResource.kt +++ b/backend/mr/src/main/kotlin/io/featurehub/mr/resources/ApplicationRolloutStrategyResource.kt @@ -25,10 +25,11 @@ class ApplicationRolloutStrategyResource @Inject constructor( holder: ApplicationRolloutStrategyServiceDelegate.CreateApplicationStrategyHolder, securityContext: SecurityContext ): ApplicationRolloutStrategy { - val person = applicationUtils.featureCreatorCheck(securityContext, appId).current + val person = applicationUtils.applicationStrategyCreate(securityContext, appId) + try { return applicationRolloutStrategyApi.createStrategy( - appId, createRolloutStrategy, person.id!!.id, + appId, createRolloutStrategy, person, Opts().add(FillOpts.SimplePeople, holder.includeWhoChanged) ) ?: throw NotFoundException() } catch (e: ApplicationRolloutStrategyApi.DuplicateNameException) { @@ -42,9 +43,9 @@ class ApplicationRolloutStrategyResource @Inject constructor( holder: ApplicationRolloutStrategyServiceDelegate.DeleteApplicationStrategyHolder, securityContext: SecurityContext ) { - val person = applicationUtils.featureCreatorCheck(securityContext, appId).current + val person = applicationUtils.applicationStrategyDelete(securityContext, appId) - if (!applicationRolloutStrategyApi.archiveStrategy(appId, appStrategyId, person.id!!.id)) { + if (!applicationRolloutStrategyApi.archiveStrategy(appId, appStrategyId, person)) { throw NotFoundException() } } @@ -55,7 +56,8 @@ class ApplicationRolloutStrategyResource @Inject constructor( holder: ApplicationRolloutStrategyServiceDelegate.GetApplicationStrategyHolder, securityContext: SecurityContext ): ApplicationRolloutStrategy { - applicationUtils.featureReadCheck(securityContext, appId) + applicationUtils.applicationStrategyReadCheck(securityContext, appId) + log.info("getApplicationStrategy: {}", holder) return applicationRolloutStrategyApi.getStrategy( @@ -71,7 +73,8 @@ class ApplicationRolloutStrategyResource @Inject constructor( holder: ApplicationRolloutStrategyServiceDelegate.ListApplicationStrategiesHolder, securityContext: SecurityContext ): ApplicationRolloutStrategyList { - applicationUtils.featureReadCheck(securityContext, appId) + applicationUtils.applicationStrategyReadCheck(securityContext, appId) + return applicationRolloutStrategyApi.listStrategies( appId, holder.page ?: 0, holder.max ?: 20, holder.filter, diff --git a/backend/mr/src/main/kotlin/io/featurehub/mr/utils/ApplicationUtils.kt b/backend/mr/src/main/kotlin/io/featurehub/mr/utils/ApplicationUtils.kt new file mode 100644 index 000000000..d5cd01df1 --- /dev/null +++ b/backend/mr/src/main/kotlin/io/featurehub/mr/utils/ApplicationUtils.kt @@ -0,0 +1,125 @@ +package io.featurehub.mr.utils + +import io.featurehub.db.api.ApplicationApi +import io.featurehub.db.api.Opts +import io.featurehub.mr.auth.AuthManagerService +import io.featurehub.mr.model.Application +import io.featurehub.mr.model.ApplicationRoleType +import io.featurehub.mr.model.Person +import jakarta.inject.Inject +import jakarta.ws.rs.ForbiddenException +import jakarta.ws.rs.NotFoundException +import jakarta.ws.rs.WebApplicationException +import jakarta.ws.rs.core.SecurityContext +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.* + +class ApplicationUtils @Inject constructor( + private val authManager: AuthManagerService, + private val applicationApi: ApplicationApi +) { + @JvmOverloads + fun check(securityContext: SecurityContext, id: UUID, opts: Opts = Opts.empty()): ApplicationPermissionCheck { + val current = authManager.from(securityContext) + + return check(current, id, opts) + } + + @Throws(WebApplicationException::class) + fun check(current: Person, id: UUID, opts: Opts): ApplicationPermissionCheck { + val app = applicationApi.getApplication(id, opts) ?: throw NotFoundException() + + if (authManager.isOrgAdmin(current) || authManager.isPortfolioAdmin(app.portfolioId, current, null)) { + return ApplicationPermissionCheck(current, app) + } else { + throw ForbiddenException() + } + } + + @Throws(WebApplicationException::class) + fun featureCreatorCheck( + securityContext: SecurityContext, + appId: UUID + ): ApplicationPermissionCheck { + val current = authManager.from(securityContext) + + if (!applicationApi.personIsFeatureCreator(appId, current.id!!.id)) { + log.warn( + "Attempt by person {} to edit features in application {}", current.id!! + .id, appId + ) + + return check(current, appId, Opts.empty()) + } else { + return ApplicationPermissionCheck(current, Application().id(appId)) + } + } + + /** + * This just checks to see if a person has the Editor/Delete permission and if not, throws an exception. A portfolio + * admin or admin will always have it. + * + * @param securityContext + * @param appId + */ + fun featureEditorCheck(securityContext: SecurityContext, appId: UUID) { + val current = authManager.from(securityContext) + + if (!applicationApi.personIsFeatureEditor(appId, current.id!!.id)) { + log.warn( + "Attempt by person {} to edt features in application {}", current.id!! + .id, appId + ) + + check(current, appId, Opts.empty()) + } + } + + fun applicationStrategyReadCheck(securityContext: SecurityContext, id: UUID): UUID { + val current = authManager.from(securityContext) + val personId = current.id!!.id + + if (applicationApi.personIsFeatureReader(id, personId) || applicationApi.personApplicationRoles(id, personId).isEmpty()) { + return personId + } + + throw ForbiddenException() + } + + private fun appStrategyCheck(securityContext: SecurityContext, id: UUID, roles: Set): UUID { + val current = authManager.from(securityContext).id!!.id + + if (applicationApi.personApplicationRoles(id, current).any { roles.contains(it) }) { + throw ForbiddenException() + } + + return current + } + + fun applicationStrategyCreate(securityContext: SecurityContext, id: UUID): UUID { + return appStrategyCheck(securityContext, id, setOf(ApplicationRoleType.APP_STRATEGY_CREATE)) + } + + fun applicationStrategyEdit(securityContext: SecurityContext, id: UUID): UUID { + return appStrategyCheck(securityContext, id, setOf(ApplicationRoleType.APP_STRATEGY_EDIT, ApplicationRoleType.FEATURE_EDIT_AND_DELETE)) + } + + fun applicationStrategyDelete(securityContext: SecurityContext, id: UUID): UUID { + return appStrategyCheck(securityContext, id, setOf(ApplicationRoleType.FEATURE_EDIT_AND_DELETE)) + } + + fun featureReadCheck(securityContext: SecurityContext, id: UUID): Person { + val current = authManager.from(securityContext) + + if (!applicationApi.personIsFeatureReader(id, current.id!!.id)) { + throw ForbiddenException() + } + + return current + } + + companion object { + private val log: Logger = LoggerFactory.getLogger(ApplicationUtils::class.java) + } +}