Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion packages/auth/src/team/Team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
/**
Expand Down Expand Up @@ -364,14 +366,23 @@ export class Team extends EventEmitter<TeamEvents> {

/** 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',
payload: { roleName },
})
}

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({
Expand Down Expand Up @@ -414,6 +425,49 @@ export class Team extends EventEmitter<TeamEvents> {
})
}

/** 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 */
Expand Down
6 changes: 5 additions & 1 deletion packages/auth/src/team/isAdminOnlyAction.ts
Original file line number Diff line number Diff line change
@@ -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<TeamAction['type']> = [
'INVITE_DEVICE',
Expand All @@ -14,5 +18,5 @@ export const isAdminOnlyAction = (action: TeamLinkBody) => {
'SET_METADATA',
]

return !nonAdminActions.includes(action.type)
return !nonAdminActions.includes(actionType)
}
23 changes: 12 additions & 11 deletions packages/auth/src/team/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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
Expand Down