diff --git a/local-dev/api-data-watcher-pusher/api-data/02-populate-api-data-lagoon-demo-org.gql b/local-dev/api-data-watcher-pusher/api-data/02-populate-api-data-lagoon-demo-org.gql index da220e2a33..a6f0b6b9e0 100644 --- a/local-dev/api-data-watcher-pusher/api-data/02-populate-api-data-lagoon-demo-org.gql +++ b/local-dev/api-data-watcher-pusher/api-data/02-populate-api-data-lagoon-demo-org.gql @@ -89,15 +89,15 @@ mutation PopulateApi { name } - UIOrganizationAddViewer: addUserToOrganization(input: {user: {email: "orgviewer@example.com"}, organization: 1}) { + UIOrganizationAddViewer: addAdminToOrganization(input: {user: {email: "orgviewer@example.com"}, organization: {id: 1}, role: VIEWER}) { id } - UIOrganizationAddAdmin: addUserToOrganization(input: {user: {email: "orgadmin@example.com"}, organization: 1, admin: true}) { + UIOrganizationAddAdmin: addAdminToOrganization(input: {user: {email: "orgadmin@example.com"}, organization: {id: 1}, role: ADMIN}) { id } - UIOrganizationAddOwner: addUserToOrganization(input: {user: {email: "orgowner@example.com"}, organization: 1, owner: true}) { + UIOrganizationAddOwner: addAdminToOrganization(input: {user: {email: "orgowner@example.com"}, organization: {id: 1}, role: OWNER}) { id } diff --git a/local-dev/k3d-seed-data/00-populate-kubernetes.gql b/local-dev/k3d-seed-data/00-populate-kubernetes.gql index 7e2e94ab19..6eeeff0e47 100644 --- a/local-dev/k3d-seed-data/00-populate-kubernetes.gql +++ b/local-dev/k3d-seed-data/00-populate-kubernetes.gql @@ -261,15 +261,15 @@ mutation PopulateApi { name } - UIOrganizationAddViewer: addUserToOrganization(input: {user: {email: "orgviewer@example.com"}, organization: 1}) { + UIOrganizationAddViewer: addAdminToOrganization(input: {user: {email: "orgviewer@example.com"}, organization: {id: 1}, role: VIEWER}) { id } - UIOrganizationAddAdmin: addUserToOrganization(input: {user: {email: "orgadmin@example.com"}, organization: 1, admin: true}) { + UIOrganizationAddAdmin: addAdminToOrganization(input: {user: {email: "orgadmin@example.com"}, organization: {id: 1}, role: ADMIN}) { id } - UIOrganizationAddOwner: addUserToOrganization(input: {user: {email: "orgowner@example.com"}, organization: 1, owner: true}) { + UIOrganizationAddOwner: addAdminToOrganization(input: {user: {email: "orgowner@example.com"}, organization: {id: 1}, role: OWNER}) { id } diff --git a/services/api/src/models/user.ts b/services/api/src/models/user.ts index 4f0e98524f..74f30d9d0a 100644 --- a/services/api/src/models/user.ts +++ b/services/api/src/models/user.ts @@ -24,6 +24,7 @@ export interface User { attributes?: IUserAttributes; owner?: boolean; admin?: boolean; + organizationRole?: string; } interface UserEdit { @@ -163,7 +164,7 @@ export const User = (clients: { (keycloakUser: UserRepresentation): User => // @ts-ignore R.pipe( - R.pick(['id', 'email', 'username', 'firstName', 'lastName', 'attributes', 'admin', 'owner']), + R.pick(['id', 'email', 'username', 'firstName', 'lastName', 'attributes', 'admin', 'owner', 'organizationRole']), // @ts-ignore R.set(commentLens, R.view(attrCommentLens, keycloakUser)) )(keycloakUser) @@ -320,9 +321,14 @@ export const User = (clients: { let filteredViewers = filterUsersByAttribute(keycloakUsers, viewerFilter); for (const f1 in filteredOwners) { filteredOwners[f1].owner = true + filteredOwners[f1].organizationRole = "OWNER" } for (const f1 in filteredAdmins) { filteredAdmins[f1].admin = true + filteredAdmins[f1].organizationRole = "ADMIN" + } + for (const f1 in filteredViewers) { + filteredViewers[f1].organizationRole = "VIEWER" } const orgUsers = [...filteredOwners, ...filteredAdmins, ...filteredViewers] diff --git a/services/api/src/resolvers.js b/services/api/src/resolvers.js index b1674d59a7..f27b71fb0a 100644 --- a/services/api/src/resolvers.js +++ b/services/api/src/resolvers.js @@ -206,6 +206,8 @@ const { updateUser, addUserToOrganization, removeUserFromOrganization, + addAdminToOrganization, + removeAdminFromOrganization, resetUserPassword, deleteUser, getAllUsers, @@ -637,6 +639,8 @@ const resolvers = { updateUser, addUserToOrganization, removeUserFromOrganization, + addAdminToOrganization, + removeAdminFromOrganization, resetUserPassword, deleteUser, addDeployment, diff --git a/services/api/src/resources/organization/helpers.ts b/services/api/src/resources/organization/helpers.ts index 33200337a1..8b208ad361 100644 --- a/services/api/src/resources/organization/helpers.ts +++ b/services/api/src/resources/organization/helpers.ts @@ -1,6 +1,7 @@ import * as R from 'ramda'; import { Pool } from 'mariadb'; import { query } from '../../util/db'; +import { asyncPipe } from '@lagoon/commons/dist/util/func'; import { Sql } from './sql'; export const Helpers = (sqlClientPool: Pool) => { @@ -65,5 +66,36 @@ export const Helpers = (sqlClientPool: Pool) => { getNotificationsForOrganizationId, getNotificationsByTypeForOrganizationId, getEnvironmentsByOrganizationId, + getOrganizationByOrganizationInput: async (organizationInput, scope: string, resource: string) => { + const notEmpty = R.complement(R.anyPass([R.isNil, R.isEmpty])); + const hasId = R.both(R.has('id'), R.propSatisfies(notEmpty, 'id')); + const hasName = R.both(R.has('name'), R.propSatisfies(notEmpty, 'name')); + + const orgFromId = asyncPipe(R.prop('id'), getOrganizationById, organization => { + if (!organization) { + throw new Error(`Unauthorized: You don't have permission to "${scope}" on "${resource}"`); + } + return organization; + }); + + const orgFromName = asyncPipe(R.prop('name'), async name => { + const rows = await query(sqlClientPool, Sql.selectOrganizationByName(name)); + const organization = R.prop(0, rows); + if (!organization) { + throw new Error(`Unauthorized: You don't have permission to "${scope}" on "${resource}"`); + } + return organization; + }); + return R.cond([ + [hasId, orgFromId], + [hasName, orgFromName], + [ + R.T, + () => { + throw new Error('Must provide organization "id" or "name"'); + } + ] + ])(organizationInput); + }, } }; diff --git a/services/api/src/resources/user/resolvers.ts b/services/api/src/resources/user/resolvers.ts index 3719e12edd..9f205dd2d8 100644 --- a/services/api/src/resources/user/resolvers.ts +++ b/services/api/src/resources/user/resolvers.ts @@ -218,7 +218,7 @@ export const deleteUser: ResolverFn = async ( return 'success'; }; -// addUserToOrganization adds a user as an organization owner +// @DEPRECATED use addAdminToOrganization - addUserToOrganization adds a user as an organization owner export const addUserToOrganization: ResolverFn = async ( _root, { input: { user: userInput, organization: organization, admin: admin, owner: owner } }, @@ -227,7 +227,15 @@ export const addUserToOrganization: ResolverFn = async ( const organizationData = await organizationHelpers(sqlClientPool).getOrganizationById(organization); if (organizationData === undefined) { - throw new Error(`Organization does not exist`) + let scope = "addViewer" + if (owner) { + scope = "addOwner" + } else { + if (admin) { + scope = "addOwner" + } + } + throw new Error(`Unauthorized: You don't have permission to "${scope}" on "organization"`) } const user = await models.UserModel.loadUserByIdOrUsername({ @@ -278,7 +286,7 @@ export const addUserToOrganization: ResolverFn = async ( }; -// removeUserFromOrganization a user as an organization owner +// @DEPRECATED use removeAdminFromOrganization - removeUserFromOrganization a user as an organization owner export const removeUserFromOrganization: ResolverFn = async ( _root, { input: { user: userInput, organization: organization } }, @@ -287,7 +295,7 @@ export const removeUserFromOrganization: ResolverFn = async ( const organizationData = await organizationHelpers(sqlClientPool).getOrganizationById(organization); if (organizationData === undefined) { - throw new Error(`Organization does not exist`) + throw new Error(`Unauthorized: You don't have permission to "addOwner" on "organization"`) } const user = await models.UserModel.loadUserByIdOrUsername({ @@ -318,3 +326,117 @@ export const removeUserFromOrganization: ResolverFn = async ( return organizationData; }; + +// addAdminToOrganization adds a user as an organization administrator +export const addAdminToOrganization: ResolverFn = async ( + _root, + { input: { user: userInput, organization: organization, role } }, + { sqlClientPool, models, hasPermission, userActivityLogger }, +) => { + let updateUser = { + id: "", + admin: false, + owner: false, + organization: 0 + } + let scope = "addOwner" + switch (role) { + case "ADMIN": + scope = "addOwner" + updateUser.admin = true + break; + case "OWNER": + scope = "addOwner" + updateUser.owner = true + break; + case "VIEWER": //fallthrough default + default: + scope = "addViewer" + updateUser.admin = false + updateUser.owner = false + break; + } + const organizationData = await organizationHelpers(sqlClientPool).getOrganizationByOrganizationInput( + organization, + scope, + "organization" + ); + if (organizationData === undefined) { + throw new Error(`Unauthorized: You don't have permission to "${scope}" on "organization"`) + } + + const user = await models.UserModel.loadUserByIdOrUsername({ + id: R.prop('id', userInput), + email: R.prop('email', userInput), + }); + + updateUser.id = user.id + updateUser.organization = organizationData.id + + await hasPermission('organization', scope, { + organization: organizationData.id + }); + + await models.UserModel.updateUser(updateUser); + + userActivityLogger(`User added an administrator to organization '${organizationData.name}'`, { + project: '', + event: 'api:addAdminToOrganization', + payload: { + user: { + id: user.id, + email: user.email, + organization: organizationData.id, + role: role, + }, + } + }); + + return organizationData; +}; + +// removeAdminFromOrganization an administrator from and organization +export const removeAdminFromOrganization: ResolverFn = async ( + _root, + { input: { user: userInput, organization } }, + { sqlClientPool, models, hasPermission, userActivityLogger }, +) => { + + const scope = 'addOwner' + const organizationData = await organizationHelpers(sqlClientPool).getOrganizationByOrganizationInput( + organization, + scope, + "organization" + ); + if (organizationData === undefined) { + throw new Error(`Unauthorized: You don't have permission to scope on "organization"`) + } + + const user = await models.UserModel.loadUserByIdOrUsername({ + id: R.prop('id', userInput), + email: R.prop('email', userInput), + }); + + await hasPermission('organization', scope, { + organization: organizationData.id + }); + + await models.UserModel.updateUser({ + id: user.id, + organization: organizationData.id, + remove: true, + }); + + userActivityLogger(`User removed an administrator from organization '${organizationData.name}'`, { + project: '', + event: 'api:removeAdminFromOrganization', + payload: { + user: { + id: user.id, + organization: organizationData.id, + }, + } + }); + + return organizationData; +}; diff --git a/services/api/src/typeDefs.js b/services/api/src/typeDefs.js index 54677c2305..3c4a61eb4c 100644 --- a/services/api/src/typeDefs.js +++ b/services/api/src/typeDefs.js @@ -110,6 +110,12 @@ const typeDefs = gql` OWNER } + enum OrganizationRole { + VIEWER + ADMIN + OWNER + } + enum ProblemSeverityRating { NONE UNKNOWN @@ -1069,10 +1075,11 @@ const typeDefs = gql` email: String firstName: String lastName: String - admin: Boolean - owner: Boolean + admin: Boolean @deprecated(reason: "use organizationRole") + owner: Boolean @deprecated(reason: "use organizationRole") comment: String groupRoles: [GroupRoleInterface] + organizationRole: OrganizationRole } type Organization { @@ -1462,6 +1469,12 @@ const typeDefs = gql` environment: EnvironmentInput } + # Must provide id OR name + input OrganizationInput { + id: Int + name: String + } + input AddSshKeyInput { id: Int name: String! @@ -1930,6 +1943,17 @@ const typeDefs = gql` owner: Boolean } + input AddAdminToOrganizationInput { + user: UserInput! + organization: OrganizationInput! + role: OrganizationRole! + } + + input RemoveAdminFromOrganizationInput { + user: UserInput! + organization: OrganizationInput! + } + input ResetUserPasswordInput { user: UserInput! } @@ -2395,12 +2419,14 @@ const typeDefs = gql` Add a user to an organization as a viewer or owner of the organization. This allows the user to view or manage the organizations groups, projects, and notifications """ - addUserToOrganization(input: addUserToOrganizationInput!): Organization + addAdminToOrganization(input: AddAdminToOrganizationInput!): Organization + addUserToOrganization(input: addUserToOrganizationInput!): Organization @deprecated(reason: "Use addAdminToOrganization instead") """ Remove a viewer or owner from an organization. This removes the users ability to view or manage the organizations groups, projects, and notifications """ - removeUserFromOrganization(input: addUserToOrganizationInput!): Organization + removeAdminFromOrganization(input: RemoveAdminFromOrganizationInput!): Organization + removeUserFromOrganization(input: addUserToOrganizationInput!): Organization @deprecated(reason: "Use removeAdminFromOrganization instead") resetUserPassword(input: ResetUserPasswordInput!): String deleteUser(input: DeleteUserInput!): String addDeployment(input: AddDeploymentInput!): Deployment