Skip to content

Commit

Permalink
Merge pull request #3763 from uselagoon/add-orguser-roles
Browse files Browse the repository at this point in the history
feat: add new add/remove admin to organization, deprecate old add/remove
  • Loading branch information
tobybellwood authored Sep 9, 2024
2 parents 9a89260 + 8340461 commit 0c3d72c
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
6 changes: 3 additions & 3 deletions local-dev/k3d-seed-data/00-populate-kubernetes.gql
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
8 changes: 7 additions & 1 deletion services/api/src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface User {
attributes?: IUserAttributes;
owner?: boolean;
admin?: boolean;
organizationRole?: string;
}

interface UserEdit {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]

Expand Down
4 changes: 4 additions & 0 deletions services/api/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ const {
updateUser,
addUserToOrganization,
removeUserFromOrganization,
addAdminToOrganization,
removeAdminFromOrganization,
resetUserPassword,
deleteUser,
getAllUsers,
Expand Down Expand Up @@ -637,6 +639,8 @@ const resolvers = {
updateUser,
addUserToOrganization,
removeUserFromOrganization,
addAdminToOrganization,
removeAdminFromOrganization,
resetUserPassword,
deleteUser,
addDeployment,
Expand Down
32 changes: 32 additions & 0 deletions services/api/src/resources/organization/helpers.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -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);
},
}
};
130 changes: 126 additions & 4 deletions services/api/src/resources/user/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } },
Expand All @@ -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({
Expand Down Expand Up @@ -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 } },
Expand All @@ -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({
Expand Down Expand Up @@ -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;
};
34 changes: 30 additions & 4 deletions services/api/src/typeDefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ const typeDefs = gql`
OWNER
}
enum OrganizationRole {
VIEWER
ADMIN
OWNER
}
enum ProblemSeverityRating {
NONE
UNKNOWN
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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!
Expand Down Expand Up @@ -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!
}
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 0c3d72c

Please sign in to comment.