diff --git a/packages/auth/src/team/Team.ts b/packages/auth/src/team/Team.ts index 2c555fd2..207d5bbf 100644 --- a/packages/auth/src/team/Team.ts +++ b/packages/auth/src/team/Team.ts @@ -53,6 +53,8 @@ import type { TeamState, } from './types.js' import { isNewTeam } from './types.js' +import { canUserAddMemberToRole } from './validate.js' +import { isAdminOnlyActionType } from './isAdminOnlyAction.js' const { DEVICE, USER } = KeyType /** @@ -364,7 +366,7 @@ export class Team extends EventEmitter { /** Remove a role from the team */ public removeRole = (roleName: string) => { - assert(roleName !== ADMIN, 'Cannot remove admin role') + this._isRoleRemovable(roleName, true) this.dispatch({ type: 'REMOVE_ROLE', @@ -372,6 +374,15 @@ export class Team extends EventEmitter { }) } + private _isRoleRemovable = (roleName: string, assertOnFalse: boolean): boolean => { + const anyRoleButAdmin = roleName !== ADMIN + if (assertOnFalse) { + assert(anyRoleButAdmin, 'Cannot remove admin role') + } + + return anyRoleButAdmin + } + /** Dispatch the add role action */ private _dispatchAddMemberRole(userId: string, roleName: string, lockboxes: lockbox.Lockbox[]) { this.dispatch({ @@ -414,6 +425,49 @@ export class Team extends EventEmitter { }) } + /** Check if member is priveleged enough to perform a specific action */ + private _memberHasPrivelegeToPerformAction(memberId: string, actionType: TeamAction['type']): boolean { + if (!isAdminOnlyActionType(actionType)) { + return true + } + return this.memberIsAdmin(memberId) + } + + /** Check if member has permissions to add members to a role */ + public memberCanAddMembersToRole(roleName: string, memberId: string): boolean { + if (!this._memberHasPrivelegeToPerformAction(memberId, 'ADD_MEMBER_ROLE')) { + return false + } + if (!this.memberHasRole(memberId, roleName)) { + return false + } + return canUserAddMemberToRole(roleName, memberId, this.state) + } + + /** Check if member has permissions to reevoke membership from a role */ + public memberCanRemoveMembersFromRole(roleName: string, memberId: string): boolean { + if (!this._memberHasPrivelegeToPerformAction(memberId, 'REMOVE_MEMBER_ROLE')) { + return false + } + return this.memberHasRole(memberId, roleName) + } + + /** Check if member has permissions to create roles */ + public memberCanCreateRole(memberId: string): boolean { + return this._memberHasPrivelegeToPerformAction(memberId, 'ADD_ROLE') + } + + /** Check if member has permissions to delete a specific role */ + public memberCanDeleteRole(roleName: string, memberId: string): boolean { + if (!this._memberHasPrivelegeToPerformAction(memberId, 'REMOVE_ROLE')) { + return false + } + if (!this.memberHasRole(memberId, roleName)) { + return false + } + return this._isRoleRemovable(roleName, false) + } + /** ************** DEVICES */ /** Returns true if the given member has a device by the given name */ diff --git a/packages/auth/src/team/isAdminOnlyAction.ts b/packages/auth/src/team/isAdminOnlyAction.ts index 03ffa774..97813f5d 100644 --- a/packages/auth/src/team/isAdminOnlyAction.ts +++ b/packages/auth/src/team/isAdminOnlyAction.ts @@ -1,6 +1,10 @@ import { type TeamAction, type TeamLinkBody } from './types.js' export const isAdminOnlyAction = (action: TeamLinkBody) => { + return isAdminOnlyActionType(action.type) +} + +export const isAdminOnlyActionType = (actionType: TeamAction['type']): boolean => { // Any team member can do these things const nonAdminActions: Array = [ 'INVITE_DEVICE', @@ -14,5 +18,5 @@ export const isAdminOnlyAction = (action: TeamLinkBody) => { 'SET_METADATA', ] - return !nonAdminActions.includes(action.type) + return !nonAdminActions.includes(actionType) } diff --git a/packages/auth/src/team/validate.ts b/packages/auth/src/team/validate.ts index a0b3050a..a3a6773f 100644 --- a/packages/auth/src/team/validate.ts +++ b/packages/auth/src/team/validate.ts @@ -25,6 +25,17 @@ export const validate: TeamStateValidator = (previousState: TeamState, link: Tea return VALID } +export const canUserAddMemberToRole = (roleName: string, assigningUserId: string, previousState: TeamState): boolean => { + const metadata = select.getMetadata(previousState) + if (metadata.selfAssignableRoles.includes(roleName)) { + return true + } + if (select.memberIsAdmin(previousState, assigningUserId)) { + return true + } + return false + } + const validators: TeamStateValidatorSet = { rootDeviceBelongsToRootUser(previousState: TeamState, link: TeamLink, extendableLogger: Logger) { const logger = extendableLogger.extend('rootDeviceBelongsToRootUser') @@ -118,17 +129,7 @@ const validators: TeamStateValidatorSet = { if (link.body.type === 'ADD_MEMBER_ROLE') { const { userId: assigningUserId } = link.body const { roleName } = link.body.payload - const metadata = select.getMetadata(previousState) - if (metadata.selfAssignableRoles.includes(roleName)) { - return VALID - } - const role = select.role(previousState, roleName) - if (role.createdBy === assigningUserId) { - return VALID - } - if (select.memberIsAdmin(previousState, assigningUserId)) { - return VALID - } + if (canUserAddMemberToRole(roleName, assigningUserId, previousState)) return VALID return fail(`User ${assigningUserId} attempted to assign role ${roleName} illegally`, previousState, link, logger) } return VALID