diff --git a/local-dev/api-data-watcher-pusher/api-data/01-populate-api-data-lagoon-demo.gql b/local-dev/api-data-watcher-pusher/api-data/01-populate-api-data-lagoon-demo.gql index 3abfd9d1fe..42cef18437 100644 --- a/local-dev/api-data-watcher-pusher/api-data/01-populate-api-data-lagoon-demo.gql +++ b/local-dev/api-data-watcher-pusher/api-data/01-populate-api-data-lagoon-demo.gql @@ -665,6 +665,15 @@ mutation PopulateApi { id } + UserExamplePlatformViewer: addUser( + input: { + email: "platformviewer@example.com" + comment: "platform viewer user" + } + ) { + id + } + LagoonDemoGroup: addGroup( input: { name: "lagoon-demo-group" diff --git a/local-dev/k3d-seed-data/00-populate-kubernetes.gql b/local-dev/k3d-seed-data/00-populate-kubernetes.gql index a74aaa4283..7e2e94ab19 100644 --- a/local-dev/k3d-seed-data/00-populate-kubernetes.gql +++ b/local-dev/k3d-seed-data/00-populate-kubernetes.gql @@ -70,6 +70,15 @@ mutation PopulateApi { id } + UserExamplePlatformViewer: addUser( + input: { + email: "platformviewer@example.com" + comment: "platform viewer user" + } + ) { + id + } + LagoonDemoGroup: addGroup( input: { name: "lagoon-demo-group" diff --git a/local-dev/k3d-seed-data/seed-users.sh b/local-dev/k3d-seed-data/seed-users.sh index b4ef3d685e..52316fd70d 100644 --- a/local-dev/k3d-seed-data/seed-users.sh +++ b/local-dev/k3d-seed-data/seed-users.sh @@ -10,7 +10,7 @@ function is_keycloak_running { function configure_user_passwords { LAGOON_DEMO_USERS=("guest@example.com" "reporter@example.com" "developer@example.com" "maintainer@example.com" "owner@example.com") - LAGOON_DEMO_ORG_USERS=("orguser@example.com" "orgviewer@example.com" "orgadmin@example.com" "orgowner@example.com" "platformowner@example.com") + LAGOON_DEMO_ORG_USERS=("orguser@example.com" "orgviewer@example.com" "orgadmin@example.com" "orgowner@example.com" "platformviewer@example.com" "platformowner@example.com") for i in ${LAGOON_DEMO_USERS[@]} do @@ -30,6 +30,11 @@ function configure_platformowner { /opt/keycloak/bin/kcadm.sh add-roles --uusername platformowner@example.com --rolename platform-owner --config $CONFIG_PATH --target-realm Lagoon } +function configure_platformviewer { + echo Configuring platform viewer role + /opt/keycloak/bin/kcadm.sh add-roles --uusername platformviewer@example.com --rolename platform-viewer --config $CONFIG_PATH --target-realm Lagoon +} + function configure_keycloak { until is_keycloak_running; do echo Keycloak still not running, waiting 5 seconds @@ -45,6 +50,7 @@ function configure_keycloak { configure_user_passwords configure_platformowner + configure_platformviewer echo "Config of Keycloak users done" } diff --git a/services/api/src/apolloServer.js b/services/api/src/apolloServer.js index 625ad84ddb..fc5a6d36ff 100644 --- a/services/api/src/apolloServer.js +++ b/services/api/src/apolloServer.js @@ -128,11 +128,22 @@ const apolloServer = new ApolloServer({ let groupRoleProjectIds = [] const keycloakGrant = grant let legacyGrant = legacyCredentials ? legacyCredentials : null + let platformOwner = false + let platformViewer = false if (keycloakGrant) { // get all the users keycloak groups, do this early to reduce the number of times this is called otherwise keycloakUsersGroups = await User.User(modelClients).getAllGroupsForUser(keycloakGrant.access_token.content.sub); serviceAccount = await keycloakGrantManager.obtainFromClientCredentials(); currentUser = await User.User(modelClients).loadUserById(keycloakGrant.access_token.content.sub); + const userRoleMapping = await keycloakAdminClient.users.listRealmRoleMappings({id: currentUser.id}) + for (const role of userRoleMapping) { + if (role.name == "platform-owner") { + platformOwner = true + } + if (role.name == "platform-viewer") { + platformViewer = true + } + } // grab the users project ids and roles in the first request groupRoleProjectIds = await User.User(modelClients).getAllProjectsIdsForUser(currentUser.id, keycloakUsersGroups); } @@ -153,6 +164,10 @@ const apolloServer = new ApolloServer({ EnvironmentModel: EnvironmentModel.EnvironmentModel(modelClients) }, keycloakUsersGroups, + adminScopes: { + platformOwner: platformOwner, + platformViewer: platformViewer, + }, }; }, onDisconnect: (websocket, context) => { @@ -192,11 +207,22 @@ const apolloServer = new ApolloServer({ let groupRoleProjectIds = [] const keycloakGrant = req.kauth ? req.kauth.grant : null let legacyGrant = req.legacyCredentials ? req.legacyCredentials : null + let platformOwner = false + let platformViewer = false if (keycloakGrant) { // get all the users keycloak groups, do this early to reduce the number of times this is called otherwise keycloakUsersGroups = await User.User(modelClients).getAllGroupsForUser(keycloakGrant.access_token.content.sub); serviceAccount = await keycloakGrantManager.obtainFromClientCredentials(); currentUser = await User.User(modelClients).loadUserById(keycloakGrant.access_token.content.sub); + const userRoleMapping = await keycloakAdminClient.users.listRealmRoleMappings({id: currentUser.id}) + for (const role of userRoleMapping) { + if (role.name == "platform-owner") { + platformOwner = true + } + if (role.name == "platform-viewer") { + platformViewer = true + } + } // grab the users project ids and roles in the first request groupRoleProjectIds = await User.User(modelClients).getAllProjectsIdsForUser(currentUser.id, keycloakUsersGroups); await User.User(modelClients).userLastAccessed(currentUser); @@ -210,34 +236,6 @@ const apolloServer = new ApolloServer({ const hasPermission = req.kauth ? keycloakHasPermission(req.kauth.grant, requestCache, modelClients, serviceAccount, currentUser, groupRoleProjectIds) : legacyHasPermission(req.legacyCredentials) - let projectViewAll = false - let groupViewAll = false - let environmentViewAll = false - let deploytargetViewAll = false - try { - await hasPermission("project","viewAll") - projectViewAll = true - } catch(err) { - // do nothing - } - try { - await hasPermission("group","viewAll") - groupViewAll = true - } catch(err) { - // do nothing - } - try { - await hasPermission("environment","viewAll") - environmentViewAll = true - } catch(err) { - // do nothing - } - try { - await hasPermission("openshift","viewAll") - deploytargetViewAll = true - } catch(err) { - // do nothing - } return { keycloakAdminClient, @@ -268,10 +266,8 @@ const apolloServer = new ApolloServer({ }, keycloakUsersGroups, adminScopes: { - projectViewAll: projectViewAll, - groupViewAll: groupViewAll, - environmentViewAll: environmentViewAll, - deploytargetViewAll: deploytargetViewAll, + platformOwner: platformOwner, + platformViewer: platformViewer, }, }; } diff --git a/services/api/src/clients/pubSub.ts b/services/api/src/clients/pubSub.ts index b400d6edf2..779c94e44c 100644 --- a/services/api/src/clients/pubSub.ts +++ b/services/api/src/clients/pubSub.ts @@ -36,7 +36,7 @@ const createSubscribe = (events): ResolverFn => async ( info ) => { const { environment } = args; - const { sqlClientPool, hasPermission } = context; + const { sqlClientPool, hasPermission, adminScopes } = context; const rows = await query( sqlClientPool, @@ -46,9 +46,12 @@ const createSubscribe = (events): ResolverFn => async ( const project = path([0, 'project'], rows); try { - await hasPermission('environment', 'view', { - project - }); + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { + await hasPermission('environment', 'view', { + project + }); + } } catch (err) { throw new ForbiddenError(err.message); } diff --git a/services/api/src/resources/backup/resolvers.ts b/services/api/src/resources/backup/resolvers.ts index 5d14712096..931c02156f 100644 --- a/services/api/src/resources/backup/resolvers.ts +++ b/services/api/src/resources/backup/resolvers.ts @@ -131,7 +131,8 @@ export const getBackupsByEnvironmentId: ResolverFn = async ( sqlClientPool ).getEnvironmentById(environmentId); - if (!adminScopes.projectViewAll) { + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { await hasPermission('backup', 'view', { project: environment.project }); diff --git a/services/api/src/resources/deployment/resolvers.ts b/services/api/src/resources/deployment/resolvers.ts index 589232a336..28908dbc9c 100644 --- a/services/api/src/resources/deployment/resolvers.ts +++ b/services/api/src/resources/deployment/resolvers.ts @@ -148,7 +148,7 @@ export const getDeploymentsByBulkId: ResolverFn = async ( export const getDeploymentsByFilter: ResolverFn = async ( root, input, - { sqlClientPool, hasPermission, models, keycloakGrant, keycloakUsersGroups } + { sqlClientPool, hasPermission, models, keycloakGrant, keycloakUsersGroups, adminScopes } ) => { const { openshifts, deploymentStatus = ["NEW", "PENDING", "RUNNING", "QUEUED"] } = input; @@ -205,7 +205,8 @@ export const getDeploymentsByEnvironmentId: ResolverFn = async ( sqlClientPool ).getEnvironmentById(eid); - if (!adminScopes.projectViewAll) { + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { await hasPermission('deployment', 'view', { project: environment.project }); @@ -1132,7 +1133,7 @@ export const deployEnvironmentPromote: ResolverFn = async ( export const switchActiveStandby: ResolverFn = async ( root, { input: { project: projectInput } }, - { sqlClientPool, hasPermission, keycloakGrant, legacyGrant } + { sqlClientPool, hasPermission, keycloakGrant, legacyGrant, adminScopes } ) => { const project = await projectHelpers(sqlClientPool).getProjectByProjectInput( projectInput diff --git a/services/api/src/resources/deploytargetconfig/resolvers.ts b/services/api/src/resources/deploytargetconfig/resolvers.ts index 7e2efdc1f3..b5e1a51e90 100644 --- a/services/api/src/resources/deploytargetconfig/resolvers.ts +++ b/services/api/src/resources/deploytargetconfig/resolvers.ts @@ -14,7 +14,7 @@ import { Helpers as organizationHelpers } from '../organization/helpers'; export const getDeployTargetConfigById = async ( root, args, - { sqlClientPool, hasPermission } + { sqlClientPool, hasPermission, adminScopes } ) => { const deployTargetConfig = await Helpers(sqlClientPool).getDeployTargetConfigById(args.id); @@ -25,7 +25,7 @@ export const getDeployTargetConfigById = async ( // since deploytargetconfigs are associated to a project // re-use the existing `project:view` permissions check, since the same sorts of fields // are viewable by the same permissions at the project scope - await projectHelpers(sqlClientPool).checkOrgProjectViewPermission(hasPermission, deployTargetConfig.project) + await projectHelpers(sqlClientPool).checkOrgProjectViewPermission(hasPermission, deployTargetConfig.project, adminScopes) return deployTargetConfig; }; @@ -33,7 +33,7 @@ export const getDeployTargetConfigById = async ( export const getDeployTargetConfigsByProjectId: ResolverFn = async ( project, args, - { sqlClientPool, hasPermission, keycloakGrant, models } + { sqlClientPool, hasPermission, keycloakGrant, models, adminScopes } ) => { let pid = args.project; @@ -44,7 +44,7 @@ export const getDeployTargetConfigsByProjectId: ResolverFn = async ( // since deploytargetconfigs are associated to a project // re-use the existing `project:view` permissions check, since the same sorts of fields // are viewable by the same permissions at the project scope - await projectHelpers(sqlClientPool).checkOrgProjectViewPermission(hasPermission, pid) + await projectHelpers(sqlClientPool).checkOrgProjectViewPermission(hasPermission, pid, adminScopes) const rows = await query(sqlClientPool, Sql.selectDeployTargetConfigsByProjectId(pid)); diff --git a/services/api/src/resources/env-variables/resolvers.ts b/services/api/src/resources/env-variables/resolvers.ts index 884e1d3a31..cddd1e6620 100644 --- a/services/api/src/resources/env-variables/resolvers.ts +++ b/services/api/src/resources/env-variables/resolvers.ts @@ -13,7 +13,8 @@ export const getEnvVarsByProjectId: ResolverFn = async ( { sqlClientPool, hasPermission, adminScopes }, info ) => { - if (!adminScopes.projectViewAll) { + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { const index = info.fieldNodes[0].selectionSet.selections.findIndex(item => item.name.value === "value"); if (index != -1) { await hasPermission('env_var', 'project:viewValue', { @@ -56,7 +57,8 @@ export const getEnvVarsByEnvironmentId: ResolverFn = async ( sqlClientPool ).getEnvironmentById(eid) - if (!adminScopes.projectViewAll) { + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { const index = info.fieldNodes[0].selectionSet.selections.findIndex(item => item.name.value === "value"); if (index != -1) { await hasPermission( @@ -405,7 +407,8 @@ export const getEnvVariablesByProjectEnvironmentName: ResolverFn = async ( ); const environment = environmentRows[0]; - if (!adminScopes.projectViewAll) { + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { if (index != -1) { await hasPermission( 'env_var', @@ -445,7 +448,8 @@ export const getEnvVariablesByProjectEnvironmentName: ResolverFn = async ( } } else if (projectName) { // is project - if (!adminScopes.projectViewAll) { + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { if (index != -1) { await hasPermission('env_var', 'project:viewValue', { project: projectId diff --git a/services/api/src/resources/environment/resolvers.ts b/services/api/src/resources/environment/resolvers.ts index b8d923c34e..2b5a5203de 100644 --- a/services/api/src/resources/environment/resolvers.ts +++ b/services/api/src/resources/environment/resolvers.ts @@ -17,7 +17,7 @@ import { getUserProjectIdsFromRoleProjectIds } from '../../util/auth'; export const getEnvironmentByName: ResolverFn = async ( root, args, - { sqlClientPool, hasPermission } + { sqlClientPool, hasPermission, adminScopes } ) => { if (args.includeDeleted == undefined) { @@ -33,9 +33,12 @@ export const getEnvironmentByName: ResolverFn = async ( return null; } - await hasPermission('environment', 'view', { - project: args.project - }); + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { + await hasPermission('environment', 'view', { + project: args.project + }); + } return environment; }; @@ -43,17 +46,19 @@ export const getEnvironmentByName: ResolverFn = async ( export const getEnvironmentById = async ( root, args, - { sqlClientPool, hasPermission } + { sqlClientPool, hasPermission, adminScopes } ) => { const environment = await Helpers(sqlClientPool).getEnvironmentById(args.id); if (!environment) { return null; } - - await hasPermission('environment', 'view', { - project: environment.project - }); + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { + await hasPermission('environment', 'view', { + project: environment.project + }); + } return environment; }; @@ -65,7 +70,8 @@ export const getEnvironmentsByProjectId: ResolverFn = async ( ) => { const { id: pid } = project; - if (!adminScopes.projectViewAll) { + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { await hasPermission('environment', 'view', { project: pid }); @@ -89,7 +95,7 @@ export const getEnvironmentsByProjectId: ResolverFn = async ( export const getEnvironmentByDeploymentId: ResolverFn = async ( { id: deployment_id }, args, - { sqlClientPool, hasPermission } + { sqlClientPool, hasPermission, adminScopes } ) => { const rows = await query(sqlClientPool, Sql.selectEnvironmentByDeploymentId(deployment_id)) const withK8s = Helpers(sqlClientPool).aliasOpenshiftToK8s(rows); @@ -99,9 +105,12 @@ export const getEnvironmentByDeploymentId: ResolverFn = async ( return null; } - await hasPermission('environment', 'view', { - project: environment.project - }); + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { + await hasPermission('environment', 'view', { + project: environment.project + }); + } return environment; }; @@ -109,7 +118,7 @@ export const getEnvironmentByDeploymentId: ResolverFn = async ( export const getEnvironmentByTaskId: ResolverFn = async ( { id: task_id }, args, - { sqlClientPool, hasPermission } + { sqlClientPool, hasPermission, adminScopes } ) => { const rows = await query(sqlClientPool, Sql.selectEnvironmentByTaskId(task_id)) const withK8s = Helpers(sqlClientPool).aliasOpenshiftToK8s(rows); @@ -119,9 +128,12 @@ export const getEnvironmentByTaskId: ResolverFn = async ( return null; } - await hasPermission('environment', 'view', { - project: environment.project - }); + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { + await hasPermission('environment', 'view', { + project: environment.project + }); + } return environment; }; @@ -129,7 +141,7 @@ export const getEnvironmentByTaskId: ResolverFn = async ( export const getEnvironmentByBackupId: ResolverFn = async ( { id: backup_id }, args, - { sqlClientPool, hasPermission } + { sqlClientPool, hasPermission, adminScopes } ) => { const rows = await query(sqlClientPool, Sql.selectEnvironmentByBackupId(backup_id)) const withK8s = Helpers(sqlClientPool).aliasOpenshiftToK8s(rows); @@ -139,9 +151,12 @@ export const getEnvironmentByBackupId: ResolverFn = async ( return null; } - await hasPermission('environment', 'view', { - project: environment.project - }); + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { + await hasPermission('environment', 'view', { + project: environment.project + }); + } return environment; }; @@ -206,7 +221,7 @@ export const getEnvironmentHitsMonthByEnvironmentId: ResolverFn = async ( export const getEnvironmentByOpenshiftProjectName: ResolverFn = async ( root, args, - { sqlClientPool, hasPermission } + { sqlClientPool, hasPermission, adminScopes } ) => { const rows = await query(sqlClientPool, Sql.selectEnvironmentByOpenshiftProjectName(args.openshiftProjectName)); @@ -218,9 +233,12 @@ export const getEnvironmentByOpenshiftProjectName: ResolverFn = async ( return null; } - await hasPermission('environment', 'view', { - project: environment.project - }); + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { + await hasPermission('environment', 'view', { + project: environment.project + }); + } return environment; }; diff --git a/services/api/src/resources/fact/resolvers.ts b/services/api/src/resources/fact/resolvers.ts index 8cb5ceefb5..24511e021a 100644 --- a/services/api/src/resources/fact/resolvers.ts +++ b/services/api/src/resources/fact/resolvers.ts @@ -18,7 +18,8 @@ export const getFactsByEnvironmentId: ResolverFn = async ( sqlClientPool ).getEnvironmentById(environmentId); - if (!adminScopes.projectViewAll) { + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { await hasPermission('fact', 'view', { project: environment.project }); @@ -127,13 +128,14 @@ export const getProjectsByFactSearch: ResolverFn = async ( let userProjectIds: number[]; - if (!adminScopes.projectViewAll) { + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { const userProjectRoles = await models.UserModel.getAllProjectsIdsForUser(keycloakGrant.access_token.content.sub, keycloakUsersGroups); userProjectIds = getUserProjectIdsFromRoleProjectIds(userProjectRoles); } - const count = await getFactFilteredProjectsCount(input, userProjectIds, sqlClientPool, adminScopes.projectViewAll); - const rows = await getFactFilteredProjects(input, userProjectIds, sqlClientPool, adminScopes.projectViewAll); + const count = await getFactFilteredProjectsCount(input, userProjectIds, sqlClientPool, adminScopes); + const rows = await getFactFilteredProjects(input, userProjectIds, sqlClientPool, adminScopes); return { projects: rows, count }; } @@ -145,13 +147,14 @@ export const getEnvironmentsByFactSearch: ResolverFn = async ( ) => { let userProjectIds: number[]; - if (!adminScopes.projectViewAll) { + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { const userProjectRoles = await models.UserModel.getAllProjectsIdsForUser(keycloakGrant.access_token.content.sub, keycloakUsersGroups); userProjectIds = getUserProjectIdsFromRoleProjectIds(userProjectRoles); } - const count = await getFactFilteredEnvironmentsCount(input, userProjectIds, sqlClientPool, adminScopes.projectViewAll); - const rows = await getFactFilteredEnvironments(input, userProjectIds, sqlClientPool, adminScopes.projectViewAll); + const count = await getFactFilteredEnvironmentsCount(input, userProjectIds, sqlClientPool, adminScopes); + const rows = await getFactFilteredEnvironments(input, userProjectIds, sqlClientPool, adminScopes); return { environments: rows, count }; } @@ -171,7 +174,8 @@ export const processAddFacts = async (facts, sqlClientPool, hasPermission, admin }, []); // admin bypass to skip heavy haspermission checks - if (!adminScopes.projectViewAll) { + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { let projectIds = [] for (let i = 0; i < environments.length; i++) { const env = await environmentHelpers(sqlClientPool).getEnvironmentById( @@ -325,7 +329,8 @@ export const addFactsByName: ResolverFn = async ( } let lagoonProject = await projectHelpers(sqlClientPool).getProjectIdByName(project); - if (!adminScopes.projectViewAll) { + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { await hasPermission('environment', 'view', { project: lagoonProject }); @@ -528,13 +533,13 @@ export const deleteAllFactReferencesByFactId: ResolverFn = async ( }; -export const getFactFilteredEnvironmentIds = async (filterDetails: any, projectIdSubset: number[], sqlClientPool, isAdmin) => { - return R.map(p => R.prop("id", p), await getFactFilteredEnvironments(filterDetails, projectIdSubset, sqlClientPool, isAdmin)); +export const getFactFilteredEnvironmentIds = async (filterDetails: any, projectIdSubset: number[], sqlClientPool, adminScopes) => { + return R.map(p => R.prop("id", p), await getFactFilteredEnvironments(filterDetails, projectIdSubset, sqlClientPool, adminScopes)); }; -const getFactFilteredProjects = async (filterDetails: any, projectIdSubset: number[], sqlClientPool, isAdmin: boolean) => { +const getFactFilteredProjects = async (filterDetails: any, projectIdSubset: number[], sqlClientPool, adminScopes: any) => { let factQuery = knex('project').distinct('project.*').innerJoin('environment', 'environment.project', 'project.id'); - factQuery = buildConditionsForFactSearchQuery(filterDetails, factQuery, projectIdSubset, isAdmin); + factQuery = buildConditionsForFactSearchQuery(filterDetails, factQuery, projectIdSubset, adminScopes); factQuery = setQueryLimit(filterDetails, factQuery); factQuery = factQuery.orderBy('project.name', 'asc'); @@ -542,18 +547,18 @@ const getFactFilteredProjects = async (filterDetails: any, projectIdSubset: numb return rows; } -const getFactFilteredProjectsCount = async (filterDetails: any, projectIdSubset: number[], sqlClientPool, isAdmin: boolean) => { +const getFactFilteredProjectsCount = async (filterDetails: any, projectIdSubset: number[], sqlClientPool, adminScopes: any) => { let factQuery = knex('project').countDistinct({ count: 'project.id'}).innerJoin('environment', 'environment.project', 'project.id'); - factQuery = buildConditionsForFactSearchQuery(filterDetails, factQuery, projectIdSubset, isAdmin); + factQuery = buildConditionsForFactSearchQuery(filterDetails, factQuery, projectIdSubset, adminScopes); const rows = await query(sqlClientPool, factQuery.toString()); return rows[0].count; } -const getFactFilteredEnvironments = async (filterDetails: any, projectIdSubset: number[], sqlClientPool, isAdmin: boolean) => { +const getFactFilteredEnvironments = async (filterDetails: any, projectIdSubset: number[], sqlClientPool, adminScopes: any) => { let factQuery = knex('environment').distinct('environment.*').innerJoin('project', 'environment.project', 'project.id'); - factQuery = buildConditionsForFactSearchQuery(filterDetails, factQuery, projectIdSubset, isAdmin); + factQuery = buildConditionsForFactSearchQuery(filterDetails, factQuery, projectIdSubset, adminScopes); factQuery = setQueryLimit(filterDetails, factQuery); factQuery = factQuery.orderBy('project.name', 'asc'); @@ -561,15 +566,15 @@ const getFactFilteredEnvironments = async (filterDetails: any, projectIdSubset: return rows; } -const getFactFilteredEnvironmentsCount = async (filterDetails: any, projectIdSubset: number[], sqlClientPool, isAdmin: boolean) => { +const getFactFilteredEnvironmentsCount = async (filterDetails: any, projectIdSubset: number[], sqlClientPool, adminScopes: any) => { let factQuery = knex('environment').countDistinct({ count: 'environment.id'}).innerJoin('project', 'environment.project', 'project.id'); - factQuery = buildConditionsForFactSearchQuery(filterDetails, factQuery, projectIdSubset, isAdmin); + factQuery = buildConditionsForFactSearchQuery(filterDetails, factQuery, projectIdSubset, adminScopes); const rows = await query(sqlClientPool, factQuery.toString()); return rows[0].count; } -const buildConditionsForFactSearchQuery = (filterDetails: any, factQuery: any, projectIdSubset: number[], isAdmin: boolean = false, byPassLimits: boolean = false) => { +const buildConditionsForFactSearchQuery = (filterDetails: any, factQuery: any, projectIdSubset: number[], adminScopes: any, byPassLimits: boolean = false) => { if (filterDetails.filters && filterDetails.filters.length > 0) { filterDetails.filters.forEach((filter, i) => { @@ -618,7 +623,7 @@ const buildConditionsForFactSearchQuery = (filterDetails: any, factQuery: any, p }) } - if (projectIdSubset && !isAdmin) { + if (projectIdSubset && (!adminScopes.platformOwner && !adminScopes.platformViewer)) { factQuery = factQuery.andWhere('project', 'IN', projectIdSubset); } diff --git a/services/api/src/resources/group/resolvers.ts b/services/api/src/resources/group/resolvers.ts index aa64b896c7..77b9eab7c3 100644 --- a/services/api/src/resources/group/resolvers.ts +++ b/services/api/src/resources/group/resolvers.ts @@ -19,7 +19,7 @@ export const getAllGroups: ResolverFn = async ( { hasPermission, models, keycloakGrant, keycloakUsersGroups, adminScopes } ) => { // use the admin scope check instead of `hasPermission` for speed - if (adminScopes.groupViewAll) { + if (adminScopes.platformOwner && adminScopes.platformViewer) { try { if (name) { @@ -99,7 +99,7 @@ export const getGroupRolesByUserId: ResolverFn =async ( { hasPermission, models, keycloakGrant, keycloakUsersGroups, adminScopes } ) => { // use the admin scope check instead of `hasPermission` for speed - if (adminScopes.groupViewAll) { + if (adminScopes.platformOwner && adminScopes.platformViewer) { try { const queryUserGroups = await models.UserModel.getAllGroupsForUser(uid); let groups = [] @@ -192,7 +192,7 @@ export const getGroupsByProjectId: ResolverFn = async ( { hasPermission, sqlClientPool, models, keycloakGrant, keycloakUsersGroups, adminScopes } ) => { // use the admin scope check instead of `hasPermission` for speed - if (adminScopes.groupViewAll) { + if (adminScopes.platformOwner && adminScopes.platformViewer) { try { const projectGroups = await Helpers(sqlClientPool).selectGroupsByProjectId(models, pid) return projectGroups; @@ -260,7 +260,7 @@ export const getGroupsByUserId: ResolverFn = async ( { hasPermission, models, keycloakGrant, keycloakUsersGroups, adminScopes } ) => { // use the admin scope check instead of `hasPermission` for speed - if (adminScopes.groupViewAll) { + if (adminScopes.platformOwner && adminScopes.platformViewer) { try { const queryUserGroups = await models.UserModel.getAllGroupsForUser(uid); @@ -283,7 +283,7 @@ export const getGroupByName: ResolverFn = async ( { models, hasPermission, keycloakGrant, keycloakUsersGroups, adminScopes } ) => { // use the admin scope check instead of `hasPermission` for speed - if (adminScopes.groupViewAll) { + if (adminScopes.platformOwner && adminScopes.platformViewer) { try { const group = await models.GroupModel.loadGroupByName(name); return group; @@ -354,7 +354,7 @@ export const addGroup: ResolverFn = async ( } } else { // otherwise fall back - if (DISABLE_NON_ORGANIZATION_GROUP_CREATION == "false" || adminScopes.projectViewAll) { + if (DISABLE_NON_ORGANIZATION_GROUP_CREATION == "false" || adminScopes.platformOwner) { await hasPermission('group', 'add'); } else { throw new Error( @@ -390,7 +390,7 @@ export const addGroup: ResolverFn = async ( await models.GroupModel.addProjectToGroup(null, group); // if the user is not an admin, or an organization add, then add the user as an owner to the group - if (!adminScopes.projectViewAll && !input.organization && keycloakGrant) { + if (!adminScopes.platformOwner && !input.organization && keycloakGrant) { const user = await models.UserModel.loadUserById( keycloakGrant.access_token.content.sub ); @@ -748,7 +748,7 @@ export const getAllProjectsInGroup: ResolverFn = async ( } = models; // use the admin scope check instead of `hasPermission` for speed - if (adminScopes.groupViewAll) { + if (adminScopes.platformOwner && adminScopes.platformViewer) { try { // get group from all keycloak groups apollo context const group = await loadGroupByIdOrName(groupInput); diff --git a/services/api/src/resources/insight/resolvers.ts b/services/api/src/resources/insight/resolvers.ts index a7d4c2463d..46692a0837 100644 --- a/services/api/src/resources/insight/resolvers.ts +++ b/services/api/src/resources/insight/resolvers.ts @@ -132,7 +132,8 @@ export const getInsightsFilesByEnvironmentId: ResolverFn = async ( sqlClientPool ).getEnvironmentById(parseInt(eid)); - if (!adminScopes.projectViewAll) { + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { await hasPermission('environment', 'view', { project: environmentData.project }); diff --git a/services/api/src/resources/notification/resolvers.ts b/services/api/src/resources/notification/resolvers.ts index e2c5a09427..4a1a5c6453 100644 --- a/services/api/src/resources/notification/resolvers.ts +++ b/services/api/src/resources/notification/resolvers.ts @@ -56,7 +56,7 @@ const checkOrgNotificationPermission = async (hasPermission, input, adminScopes) ); } } else { - if (DISABLE_NON_ORGANIZATION_NOTIFICATION_ASSIGNMENT == "false" || adminScopes.projectViewAll) { + if (DISABLE_NON_ORGANIZATION_NOTIFICATION_ASSIGNMENT == "false" || adminScopes.platformOwner) { await hasPermission('notification', 'add'); } else { throw new Error( diff --git a/services/api/src/resources/openshift/resolvers.ts b/services/api/src/resources/openshift/resolvers.ts index c3734dd34d..19fa0501c9 100644 --- a/services/api/src/resources/openshift/resolvers.ts +++ b/services/api/src/resources/openshift/resolvers.ts @@ -134,7 +134,8 @@ export const getOpenshiftByProjectId: ResolverFn = async ( { sqlClientPool, hasPermission, adminScopes } ) => { - if (!adminScopes.openshiftViewAll) { + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { await hasPermission('openshift', 'view', { project: pid }); @@ -174,8 +175,8 @@ export const getOpenshiftByEnvironmentId: ResolverFn = async ( sqlClientPool ).getProjectByEnvironmentId(eid); - // check permissions on the project - if (!adminScopes.openshiftViewAll) { + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { await hasPermission('openshift', 'view', { project: project.project }); diff --git a/services/api/src/resources/problem/resolvers.ts b/services/api/src/resources/problem/resolvers.ts index 3d41f7f07a..1fef04d08b 100644 --- a/services/api/src/resources/problem/resolvers.ts +++ b/services/api/src/resources/problem/resolvers.ts @@ -104,7 +104,8 @@ export const getProblemsByEnvironmentId: ResolverFn = async ( sqlClientPool ).getEnvironmentById(environmentId); - if (!adminScopes.projectViewAll) { + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { await hasPermission('problem', 'view', { project: environment.project }); diff --git a/services/api/src/resources/project/helpers.ts b/services/api/src/resources/project/helpers.ts index 69c7d89e83..1e899cbee1 100644 --- a/services/api/src/resources/project/helpers.ts +++ b/services/api/src/resources/project/helpers.ts @@ -6,28 +6,30 @@ import { Sql } from './sql'; // import { logger } from '../../loggers/logger'; export const Helpers = (sqlClientPool: Pool) => { - const checkOrgProjectViewPermission = async (hasPermission, pid) => { - // helper that allows fall through of permission check - // for viewProject:organization to view:project - // this allows queries to be performed by organization owners - // then falling through to the default project view for general users - const rows = await query(sqlClientPool, Sql.selectProject(pid)); - const project = rows[0]; - if (project.organization != null) { - try { - await hasPermission('organization', 'viewProject', { - organization: project.organization - }); - // if the organization owner has permission to view project, return - return - } catch (err) { - // otherwise fall through to project view permission check + const checkOrgProjectViewPermission = async (hasPermission, pid, adminScopes) => { + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { + // helper that allows fall through of permission check + // for viewProject:organization to view:project + // this allows queries to be performed by organization owners + // then falling through to the default project view for general users + const rows = await query(sqlClientPool, Sql.selectProject(pid)); + const project = rows[0]; + if (project.organization != null) { + try { + await hasPermission('organization', 'viewProject', { + organization: project.organization + }); + // if the organization owner has permission to view project, return + return + } catch (err) { + // otherwise fall through to project view permission check + } } + // finally check the user view:project permission + await hasPermission('project', 'view', { + project: project.id + }); } - // finally check the user view:project permission - await hasPermission('project', 'view', { - project: project.id - }); } const checkOrgProjectUpdatePermission = async (hasPermission, pid) => { // helper checks the permission to updateProject:organization diff --git a/services/api/src/resources/project/resolvers.ts b/services/api/src/resources/project/resolvers.ts index 4e98a2dba6..9607c987c5 100644 --- a/services/api/src/resources/project/resolvers.ts +++ b/services/api/src/resources/project/resolvers.ts @@ -111,7 +111,7 @@ export const getAllProjects: ResolverFn = async ( export const getProjectByEnvironmentId: ResolverFn = async ( { id: eid }, args, - { sqlClientPool, hasPermission } + { sqlClientPool, hasPermission, adminScopes } ) => { const rows = await query(sqlClientPool, Sql.selectProjectByEnvironmentID(eid)); @@ -119,15 +119,14 @@ export const getProjectByEnvironmentId: ResolverFn = async ( const project = withK8s[0]; - await Helpers(sqlClientPool).checkOrgProjectViewPermission(hasPermission, project.id) - + await Helpers(sqlClientPool).checkOrgProjectViewPermission(hasPermission, project.id, adminScopes) return project; }; export const getProjectById: ResolverFn = async ( { project: pid }, args, - { sqlClientPool, hasPermission } + { sqlClientPool, hasPermission, adminScopes } ) => { const rows = await query(sqlClientPool, Sql.selectProjectById(pid)); @@ -135,7 +134,7 @@ export const getProjectById: ResolverFn = async ( const project = withK8s[0]; - await Helpers(sqlClientPool).checkOrgProjectViewPermission(hasPermission, project.id) + await Helpers(sqlClientPool).checkOrgProjectViewPermission(hasPermission, project.id, adminScopes) return project; }; @@ -143,7 +142,7 @@ export const getProjectById: ResolverFn = async ( export const getProjectByGitUrl: ResolverFn = async ( root, args, - { sqlClientPool, hasPermission } + { sqlClientPool, hasPermission, adminScopes } ) => { const rows = await query(sqlClientPool, Sql.selectProjectByGitUrl(args.gitUrl)); @@ -151,7 +150,7 @@ export const getProjectByGitUrl: ResolverFn = async ( const project = withK8s[0]; - await Helpers(sqlClientPool).checkOrgProjectViewPermission(hasPermission, project.id) + await Helpers(sqlClientPool).checkOrgProjectViewPermission(hasPermission, project.id, adminScopes) return project; }; @@ -159,7 +158,7 @@ export const getProjectByGitUrl: ResolverFn = async ( export const getProjectByName: ResolverFn = async ( root, args, - { sqlClientPool, hasPermission } + { sqlClientPool, hasPermission, adminScopes } ) => { const rows = await query(sqlClientPool, Sql.selectProjectByName(args.name)); @@ -170,7 +169,7 @@ export const getProjectByName: ResolverFn = async ( return null; } - await Helpers(sqlClientPool).checkOrgProjectViewPermission(hasPermission, project.id) + await Helpers(sqlClientPool).checkOrgProjectViewPermission(hasPermission, project.id, adminScopes) return project; }; @@ -231,7 +230,7 @@ export const addProject = async ( // Add the user who submitted this request to the project let userAlreadyHasAccess = false; - if (adminScopes.projectViewAll) { + if (adminScopes.platformOwner) { userAlreadyHasAccess = true } if (input.organization != null) { @@ -270,7 +269,7 @@ export const addProject = async ( } } } else { - if (DISABLE_NON_ORGANIZATION_PROJECT_CREATION == "false" || adminScopes.projectViewAll) { + if (DISABLE_NON_ORGANIZATION_PROJECT_CREATION == "false" || adminScopes.platformOwner) { await hasPermission('project', 'add'); } else { throw new Error( @@ -346,14 +345,14 @@ export const addProject = async ( // check if a user has permission to disable deployments of a project or not let deploymentsDisabled = 0; if (input.deploymentsDisabled) { - if (adminScopes.projectViewAll) { + if (adminScopes.platformOwner) { deploymentsDisabled = input.deploymentsDisabled } } let buildImage = null; if (input.buildImage) { - if (adminScopes.projectViewAll) { + if (adminScopes.platformOwner) { buildImage = input.buildImage } else { throw new Error('Setting build image is only available to administrators.'); @@ -362,7 +361,7 @@ export const addProject = async ( let sharedBaasBucket = null; if(typeof input.sharedBaasBucket == "boolean") { - if (adminScopes.projectViewAll) { + if (adminScopes.platformOwner) { sharedBaasBucket = input.sharedBaasBucket } else { throw new Error('Setting shared baas bucket is only available to administrators.'); @@ -647,20 +646,20 @@ export const updateProject: ResolverFn = async ( // check if a user has permission to disable deployments of a project or not if (deploymentsDisabled) { - if (!adminScopes.projectViewAll) { + if (!adminScopes.platformOwner) { throw new Error('Disabling deployments is only available to administrators.'); } } if(typeof sharedBaasBucket == "boolean") { - if (!adminScopes.projectViewAll) { + if (!adminScopes.platformOwner) { throw new Error('Setting shared baas bucket is only available to administrators.'); } } // check if a user has permission to change the build image of a project or not if (buildImage) { - if (!adminScopes.projectViewAll) { + if (!adminScopes.platformOwner) { throw new Error('Setting build image is only available to administrators.'); } } @@ -686,7 +685,7 @@ export const updateProject: ResolverFn = async ( // renaming projects is prohibited because lagoon uses the project name for quite a few things // which if changed can have unintended consequences for any existing environments if (patch.name) { - if (!adminScopes.projectViewAll) { + if (!adminScopes.platformOwner) { throw new Error('Project renaming is only available to administrators.'); } } diff --git a/services/api/src/resources/task/advancedtasktoolbox.test.ts b/services/api/src/resources/task/advancedtasktoolbox.test.ts index 413c1bd422..4c4679d297 100644 --- a/services/api/src/resources/task/advancedtasktoolbox.test.ts +++ b/services/api/src/resources/task/advancedtasktoolbox.test.ts @@ -53,7 +53,7 @@ describe('advancedtasktoolbox', () => { //This user has permission to view tasks on let hasPermissions = mockHasPermission([{resource: 'task', scope: 'view', attributes: {project: 1}}]) - let ath = advancedTaskFunctionFactory({}, hasPermissions, {}, {}, environmentHelpers, {}); + let ath = advancedTaskFunctionFactory({}, hasPermissions, {}, {}, environmentHelpers, {}, {}); test('test user is granted permission when invoking a project she has access to', () => { return expect(ath.permissions.canUserSeeTaskDefinition({environment: 1})).resolves.toBe(true); diff --git a/services/api/src/resources/task/advancedtasktoolbox.ts b/services/api/src/resources/task/advancedtasktoolbox.ts index 33dd5c110a..1b769ae655 100644 --- a/services/api/src/resources/task/advancedtasktoolbox.ts +++ b/services/api/src/resources/task/advancedtasktoolbox.ts @@ -15,17 +15,17 @@ import { sqlClientPool } from '../../clients/sqlClient'; -export const advancedTaskFunctions = (sqlClientPool, models, hasPermission = null) => { +export const advancedTaskFunctions = (sqlClientPool, models, hasPermission = null, adminScopes) => { //Here we use partial application to generate a query runner //This allows us to replace whatever is doing the running with some mocked object if we need. const queryRunner = R.partial(query, [sqlClientPool]); //Note, following the injection functionality above, we also pass through environment and project helpers that can be mocked. - return advancedTaskFunctionFactory(queryRunner, hasPermission, models, Sql, environmentHelpers(sqlClientPool), projectHelpers(sqlClientPool)); + return advancedTaskFunctionFactory(queryRunner, hasPermission, models, Sql, environmentHelpers(sqlClientPool), projectHelpers(sqlClientPool), adminScopes); } -export const advancedTaskFunctionFactory = (queryRunner, hasPermission = null, models, Sql, environmentHelpers, projectHelpers) => { +export const advancedTaskFunctionFactory = (queryRunner, hasPermission = null, models, Sql, environmentHelpers, projectHelpers, adminScopes) => { //This provides a reasonable alternative to simply throwing errors if permission checks fail. const tryCatchHaspermission = async (resource, scope, attributes: IKeycloakAuthAttributes) => { @@ -60,13 +60,19 @@ export const advancedTaskFunctionFactory = (queryRunner, hasPermission = null, m //either project, environment, or group will be - we have to run different checks for each possibility if(advancedTaskDefinition.environment !== null) { let env = await environmentHelpers.getEnvironmentById(advancedTaskDefinition.environment); + if (adminScopes.platformViewer) { + return true; + } return await tryCatchHaspermission('task', 'view', { project: env.project }); } else if (advancedTaskDefinition.project !== null) { - return await tryCatchHaspermission('task', 'view', { - project: advancedTaskDefinition.project - }); + if (adminScopes.platformViewer) { + return true; + } + return await tryCatchHaspermission('task', 'view', { + project: advancedTaskDefinition.project + }); } else if (advancedTaskDefinition.groupName !== null) { const group = await models.GroupModel.loadGroupByIdOrName({ diff --git a/services/api/src/resources/task/helpers.ts b/services/api/src/resources/task/helpers.ts index d3243810d3..ac995485a0 100644 --- a/services/api/src/resources/task/helpers.ts +++ b/services/api/src/resources/task/helpers.ts @@ -12,7 +12,7 @@ import { Helpers as environmentHelpers } from '../environment/helpers'; import { logger } from '../../loggers/logger'; export const Helpers = (sqlClientPool: Pool, hasPermission) => { - const getTaskById = async (TaskID: number) => { + const getTaskById = async (TaskID: number, adminScopes) => { const queryString = knex('task') .where('id', '=', TaskID) .toString(); @@ -25,9 +25,12 @@ export const Helpers = (sqlClientPool: Pool, hasPermission) => { } const rowsPerms = await query(sqlClientPool, Sql.selectPermsForTask(task.id)); - await hasPermission('task', 'view', { - project: R.path(['0', 'pid'], rowsPerms) - }); + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { + await hasPermission('task', 'view', { + project: R.path(['0', 'pid'], rowsPerms) + }); + } return task; }; diff --git a/services/api/src/resources/task/resolvers.ts b/services/api/src/resources/task/resolvers.ts index 5edf4abbc5..4cfbd15f4a 100644 --- a/services/api/src/resources/task/resolvers.ts +++ b/services/api/src/resources/task/resolvers.ts @@ -109,7 +109,8 @@ export const getTasksByEnvironmentId: ResolverFn = async ( sqlClientPool ).getEnvironmentById(eid); - if (!adminScopes.projectViewAll) { + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { await hasPermission('task', 'view', { project: environment.project }); diff --git a/services/api/src/resources/task/task_definition_resolvers.ts b/services/api/src/resources/task/task_definition_resolvers.ts index 664fdda8e6..97f9a281b4 100644 --- a/services/api/src/resources/task/task_definition_resolvers.ts +++ b/services/api/src/resources/task/task_definition_resolvers.ts @@ -45,7 +45,7 @@ const PermissionsToRBAC = (permission: string) => { return `invoke:${permission.toLowerCase()}`; }; -export const allAdvancedTaskDefinitions = async (root, args, {sqlClientPool, hasPermission, models}) => { +export const allAdvancedTaskDefinitions = async (root, args, {sqlClientPool, hasPermission, models, adminScopes}) => { //is the user a system admin? try { await hasPermission('advanced_task','create:advanced'); @@ -58,7 +58,7 @@ export const allAdvancedTaskDefinitions = async (root, args, {sqlClientPool, has Sql.selectAdvancedTaskDefinitions() ); - const atf = advancedTaskToolbox.advancedTaskFunctions(sqlClientPool, models, hasPermission); + const atf = advancedTaskToolbox.advancedTaskFunctions(sqlClientPool, models, hasPermission, adminScopes); for(let i = 0; i < adTaskDefs.length; i++) { adTaskDefs[i].advancedTaskDefinitionArguments = await atf.advancedTaskDefinitionArguments(adTaskDefs[i].id); @@ -70,10 +70,10 @@ export const allAdvancedTaskDefinitions = async (root, args, {sqlClientPool, has export const advancedTaskDefinitionById = async ( root, { id }, - { sqlClientPool, hasPermission, models } + { sqlClientPool, hasPermission, models, adminScopes } ) => { - const atf = advancedTaskToolbox.advancedTaskFunctions(sqlClientPool, models, hasPermission); + const atf = advancedTaskToolbox.advancedTaskFunctions(sqlClientPool, models, hasPermission, adminScopes); await hasPermission('task', 'view', {}); const advancedTaskDef = await atf.advancedTaskDefinitionById( id @@ -90,7 +90,7 @@ export const advancedTaskDefinitionById = async ( export const getRegisteredTasksByEnvironmentId = async ( { id }, {}, - { sqlClientPool, hasPermission, models } + { sqlClientPool, hasPermission, models, adminScopes } ) => { let rows; @@ -98,7 +98,7 @@ export const getRegisteredTasksByEnvironmentId = async ( rows = await resolveTasksForEnvironment( {}, { environment: id }, - { sqlClientPool, hasPermission, models } + { sqlClientPool, hasPermission, models, adminScopes } ); rows = await Filters.filterAdminTasks(hasPermission, rows); @@ -110,14 +110,17 @@ export const getRegisteredTasksByEnvironmentId = async ( export const resolveTasksForEnvironment = async ( root, { environment }, - { sqlClientPool, hasPermission, models } + { sqlClientPool, hasPermission, models, adminScopes } ) => { const environmentDetails = await environmentHelpers( sqlClientPool ).getEnvironmentById(environment); - await hasPermission('task', 'view', { - project: environmentDetails.project - }); + + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { + await hasPermission('task', 'view', { + project: environmentDetails.project + }); + } let environmentRows = await query( sqlClientPool, @@ -162,7 +165,7 @@ export const resolveTasksForEnvironment = async ( //@ts-ignore rows = R.filter(e => currentUsersPermissionForProject.includes(e.permission), rows); - const atf = advancedTaskToolbox.advancedTaskFunctions(sqlClientPool, models, hasPermission); + const atf = advancedTaskToolbox.advancedTaskFunctions(sqlClientPool, models, hasPermission, adminScopes); let typeValidatorFactory = advancedTaskArgument.advancedTaskDefinitionTypeFactory(sqlClientPool, null, environment); // TODO: this needs to be somehow refactored into all lookups. @@ -212,15 +215,18 @@ const currentUsersAdvancedTaskRBACRolesForProject = async ( export const advancedTaskDefinitionArgumentById = async ( root, id, - { sqlClientPool, hasPermission } + { sqlClientPool, hasPermission, adminScopes } ) => { const rows = await query( sqlClientPool, Sql.selectAdvancedTaskDefinitionArgumentById(id) ); - await hasPermission('environment', 'view', { - project: id - }); + // if the user is not a platform owner or viewer, then perform normal permission check + if (!adminScopes.platformOwner && !adminScopes.platformViewer) { + await hasPermission('environment', 'view', { + project: id + }); + } return R.prop(0, rows); }; @@ -230,7 +236,7 @@ export const addAdvancedTaskDefinition = async ( { input }, - { sqlClientPool, hasPermission, models, userActivityLogger } + { sqlClientPool, hasPermission, models, userActivityLogger, adminScopes } ) => { const { @@ -254,7 +260,7 @@ export const addAdvancedTaskDefinition = async ( } = input; const atb = advancedTaskToolbox.advancedTaskFunctions( - sqlClientPool, models, hasPermission + sqlClientPool, models, hasPermission, adminScopes ); let projectObj = await getProjectByEnvironmentIdOrProjectId( @@ -422,14 +428,14 @@ export const updateAdvancedTaskDefinition = async ( } } }, - { sqlClientPool, hasPermission, models, userActivityLogger } + { sqlClientPool, hasPermission, models, userActivityLogger, adminScopes } ) => { if (isPatchEmpty({ patch })) { throw new Error('Input patch requires at least 1 attribute'); } const atb = advancedTaskToolbox.advancedTaskFunctions( - sqlClientPool, models, hasPermission + sqlClientPool, models, hasPermission, adminScopes ); let task = await atb.advancedTaskDefinitionById(id); @@ -502,7 +508,7 @@ export const updateAdvancedTaskDefinition = async ( } }); - const atf = advancedTaskToolbox.advancedTaskFunctions(sqlClientPool, models, hasPermission); + const atf = advancedTaskToolbox.advancedTaskFunctions(sqlClientPool, models, hasPermission, adminScopes); return await atf.advancedTaskDefinitionById(id); } catch (error) { throw error @@ -530,7 +536,7 @@ const getProjectByEnvironmentIdOrProjectId = async ( export const invokeRegisteredTask = async ( root, { advancedTaskDefinition, environment, argumentValues, sourceType }, - { sqlClientPool, hasPermission, models, keycloakGrant, legacyGrant } + { sqlClientPool, hasPermission, models, keycloakGrant, legacyGrant, adminScopes } ) => { await envValidators(sqlClientPool).environmentExists(environment); @@ -539,11 +545,12 @@ export const invokeRegisteredTask = async ( hasPermission, advancedTaskDefinition, environment, - models + models, + adminScopes ); const atb = advancedTaskToolbox.advancedTaskFunctions( - sqlClientPool, models, hasPermission + sqlClientPool, models, hasPermission, adminScopes ); //here we want to validate the incoming arguments @@ -664,14 +671,15 @@ const getNamedAdvancedTaskForEnvironment = async ( hasPermission, advancedTaskDefinition, environment, - models + models, + adminScopes ):Promise => { let rows; rows = await resolveTasksForEnvironment( {}, { environment }, - { sqlClientPool, hasPermission, models } + { sqlClientPool, hasPermission, models, adminScopes } ); rows = await Filters.filterAdminTasks(hasPermission, rows); @@ -691,11 +699,11 @@ const getNamedAdvancedTaskForEnvironment = async ( export const deleteAdvancedTaskDefinition = async ( root, { advancedTaskDefinition }, - { sqlClientPool, hasPermission, models } + { sqlClientPool, hasPermission, models, adminScopes } ) => { //load up advanced task definition ... const atb = advancedTaskToolbox.advancedTaskFunctions( - sqlClientPool, models, hasPermission + sqlClientPool, models, hasPermission, adminScopes ); const adTaskDef = await atb.advancedTaskDefinitionById(advancedTaskDefinition); diff --git a/services/keycloak/javascript/META-INF/keycloak-scripts.json b/services/keycloak/javascript/META-INF/keycloak-scripts.json index 300900ae2a..c082fa7a97 100644 --- a/services/keycloak/javascript/META-INF/keycloak-scripts.json +++ b/services/keycloak/javascript/META-INF/keycloak-scripts.json @@ -17,6 +17,11 @@ "description": "Checks the users role for the realm is Platform Owner or higher", "fileName": "policies/users-role-for-realm-is-platform-owner.js" }, + { + "name": "[Lagoon] Users role for realm is Platform Viewer", + "description": "Checks the users role for the realm is Platform Viewer or higher", + "fileName": "policies/users-role-for-realm-is-platform-viewer.js" + }, { "name": "[Lagoon] Users role for realm is Admin", "description": "Checks the users role for the realm is Admin", diff --git a/services/keycloak/javascript/policies/users-role-for-realm-is-platform-viewer.js b/services/keycloak/javascript/policies/users-role-for-realm-is-platform-viewer.js new file mode 100644 index 0000000000..55c7f147bd --- /dev/null +++ b/services/keycloak/javascript/policies/users-role-for-realm-is-platform-viewer.js @@ -0,0 +1,15 @@ +var realm = $evaluation.getRealm(); +var ctx = $evaluation.getContext(); +var ctxAttr = ctx.getAttributes(); + +if (!ctxAttr.exists('currentUser')) { + $evaluation.deny(); +} else { + var currentUser = ctxAttr.getValue('currentUser').asString(0); + + if (realm.isUserInRealmRole(currentUser, 'platform-viewer')) { + $evaluation.grant(); + } else { + $evaluation.deny(); + } +} \ No newline at end of file diff --git a/services/keycloak/lagoon-realm-base-import.json b/services/keycloak/lagoon-realm-base-import.json index 339ce49d06..5f588340d8 100644 --- a/services/keycloak/lagoon-realm-base-import.json +++ b/services/keycloak/lagoon-realm-base-import.json @@ -83,6 +83,12 @@ "clientRole": false, "attributes": {} }, + { + "name": "platform-viewer", + "composite": false, + "clientRole": false, + "attributes": {} + }, { "name": "reporter", "composite": true, @@ -1437,6 +1443,14 @@ "decisionStrategy": "UNANIMOUS", "config": {} }, + { + "name": "[Lagoon] Users role for realm is Platform Viewer", + "description": "Checks the users role for the realm is Platform Viewer or higher", + "type": "script-policies/users-role-for-realm-is-platform-viewer.js", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": {} + }, { "name": "[Lagoon] Users role for group is Developer", "description": "Checks the users role for a group is Developer or higher", @@ -1708,7 +1722,7 @@ "config": { "resources": "[\"ssh_key\"]", "scopes": "[\"view:user\"]", - "applyPolicies": "[\"[Lagoon] User has access to own data\",\"[Lagoon] Users role for realm is Platform Owner\"]" + "applyPolicies": "[\"[Lagoon] User has access to own data\",\"[Lagoon] Users role for realm is Platform Viewer\",\"[Lagoon] Users role for realm is Platform Owner\"]" } }, { @@ -1774,7 +1788,7 @@ "config": { "resources": "[\"organization\"]", "scopes": "[\"view\",\"viewProject\",\"viewGroup\",\"viewNotification\",\"viewUser\",\"viewUsers\"]", - "applyPolicies": "[\"[Lagoon] User is admin of organization\",\"[Lagoon] User is owner of organization\",\"[Lagoon] Users role for realm is Platform Owner\",\"[Lagoon] User is viewer of organization\"]" + "applyPolicies": "[\"[Lagoon] User is admin of organization\",\"[Lagoon] User is owner of organization\",\"[Lagoon] Users role for realm is Platform Viewer\",\"[Lagoon] Users role for realm is Platform Owner\",\"[Lagoon] User is viewer of organization\"]" } }, { @@ -1968,11 +1982,11 @@ "name": "View All Organizations", "type": "scope", "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", + "decisionStrategy": "AFFIRMATIVE", "config": { "resources": "[\"organization\"]", "scopes": "[\"viewAll\"]", - "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Owner\"]" + "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Viewer\",\"[Lagoon] Users role for realm is Platform Owner\"]" } }, { @@ -2034,11 +2048,11 @@ "name": "Get User By SSH Key", "type": "scope", "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", + "decisionStrategy": "AFFIRMATIVE", "config": { "resources": "[\"user\"]", "scopes": "[\"getBySshKey\"]", - "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Owner\"]" + "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Viewer\",\"[Lagoon] Users role for realm is Platform Owner\"]" } }, { @@ -2111,22 +2125,22 @@ "name": "View All Environments", "type": "scope", "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", + "decisionStrategy": "AFFIRMATIVE", "config": { "resources": "[\"environment\"]", "scopes": "[\"viewAll\"]", - "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Owner\"]" + "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Viewer\",\"[Lagoon] Users role for realm is Platform Owner\"]" } }, { "name": "View Environment Metrics", "type": "scope", "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", + "decisionStrategy": "AFFIRMATIVE", "config": { "resources": "[\"environment\"]", "scopes": "[\"storage\"]", - "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Owner\"]" + "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Viewer\",\"[Lagoon] Users role for realm is Platform Owner\"]" } }, { @@ -2210,22 +2224,22 @@ "name": "View All Notifications", "type": "scope", "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", + "decisionStrategy": "AFFIRMATIVE", "config": { "resources": "[\"notification\"]", "scopes": "[\"viewAll\"]", - "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Owner\"]" + "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Viewer\",\"[Lagoon] Users role for realm is Platform Owner\"]" } }, { "name": "View All Groups", "type": "scope", "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", + "decisionStrategy": "AFFIRMATIVE", "config": { "resources": "[\"group\"]", "scopes": "[\"viewAll\"]", - "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Owner\"]" + "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Viewer\",\"[Lagoon] Users role for realm is Platform Owner\"]" } }, { @@ -2265,11 +2279,11 @@ "name": "View All Users", "type": "scope", "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", + "decisionStrategy": "AFFIRMATIVE", "config": { "resources": "[\"user\"]", "scopes": "[\"viewAll\"]", - "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Owner\"]" + "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Viewer\",\"[Lagoon] Users role for realm is Platform Owner\"]" } }, { @@ -2342,11 +2356,11 @@ "name": "View All Openshifts", "type": "scope", "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", + "decisionStrategy": "AFFIRMATIVE", "config": { "resources": "[\"openshift\"]", "scopes": "[\"viewAll\"]", - "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Owner\"]" + "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Viewer\",\"[Lagoon] Users role for realm is Platform Owner\"]" } }, { @@ -2551,11 +2565,11 @@ "name": "View All Projects", "type": "scope", "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", + "decisionStrategy": "AFFIRMATIVE", "config": { "resources": "[\"project\"]", "scopes": "[\"viewAll\"]", - "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Owner\"]" + "applyPolicies": "[\"[Lagoon] Users role for realm is Platform Viewer\",\"[Lagoon] Users role for realm is Platform Owner\"]" } }, { diff --git a/services/keycloak/startup-scripts/00-configure-lagoon.sh b/services/keycloak/startup-scripts/00-configure-lagoon.sh index 104c262b45..0ccc69817a 100755 --- a/services/keycloak/startup-scripts/00-configure-lagoon.sh +++ b/services/keycloak/startup-scripts/00-configure-lagoon.sh @@ -423,6 +423,217 @@ function remove_deleteall_permissions_scopes { } +function add_update_platform_viewer_permissions { + # The changes here match the changes that are made in the realm import script + # fresh installs will not need to perform this migration as the changes will already be in the import + # this will only run on existing installations to get it into a state that matches the realm import + CLIENT_ID=$(/opt/keycloak/bin/kcadm.sh get -r lagoon clients?clientId=api --config $CONFIG_PATH | jq -r '.[0]["id"]') + organization_admin_permission=$(/opt/keycloak/bin/kcadm.sh get -r lagoon clients/$CLIENT_ID/authz/resource-server/permission?name=Get+SSH+Keys+for+User --config $CONFIG_PATH | jq -r '.[0]["id"]') + associated_policies=$(/opt/keycloak/bin/kcadm.sh get -r lagoon clients/$CLIENT_ID/authz/resource-server/policy/$organization_admin_permission/associatedPolicies --config $CONFIG_PATH | jq -c 'map({name})') + + # check the permission to see if the platform viewer role is already configured + if [[ "$associated_policies" =~ 'Users role for realm is Platform Viewer' ]]; then + echo "add_update_platform_viewer_permissions already configured" + return 0 + fi + + echo Creating platform viewer js mapper policy + local p_name1="Users role for realm is Platform Viewer" + local script_name1="[Lagoon] $p_name1" + local script_type1="script-policies/$(echo $p_name1 | sed -e 's/.*/\L&/' -e 's/ /-/g').js" + echo '{"name":"'$script_name1'","type":"'$script_type1'"}' | /opt/keycloak/bin/kcadm.sh create -r lagoon clients/$CLIENT_ID/authz/resource-server/policy/$(echo $script_type1 | sed -e 's/\//%2F/') --config $CONFIG_PATH -f - + + echo Create platform viewer role + /opt/keycloak/bin/kcadm.sh create roles --config $CONFIG_PATH -r ${KEYCLOAK_REALM:-master} -s name=platform-viewer + + echo Re-configuring ssh_key:view:user + #Delete existing permissions + get_user_sshkeys=$(/opt/keycloak/bin/kcadm.sh get -r lagoon clients/$CLIENT_ID/authz/resource-server/permission?name=Get+SSH+Keys+for+User --config $CONFIG_PATH | jq -r '.[0]["id"]') + /opt/keycloak/bin/kcadm.sh delete -r lagoon clients/$CLIENT_ID/authz/resource-server/permission/$get_user_sshkeys --config $CONFIG_PATH + + /opt/keycloak/bin/kcadm.sh create clients/$CLIENT_ID/authz/resource-server/permission/scope --config $CONFIG_PATH -r lagoon -f - <