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
2 changes: 1 addition & 1 deletion packages/auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type * from './team/context.js'
export type * from './device/types.js'
export type * from './invitation/types.js'
export type * from './lockbox/types.js'
export type * from './role/types.js'
export * from './role/types.js'
export type * from './server/types.js'
export type * from './team/types.js'

Expand Down
12 changes: 11 additions & 1 deletion packages/auth/src/role/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
export type PermissionsMap = Record<string, boolean>
export enum Permission {
MODIFIABLE_MEMBERSHIP = 'modifiable-membership',
}

export interface ModifiableMembershipPermissionConfig {
memberIds: string[]
}

export type PermissionsMap = {
[Permission.MODIFIABLE_MEMBERSHIP]?: true | ModifiableMembershipPermissionConfig
}

export type Role = {
roleName: string
Expand Down
9 changes: 6 additions & 3 deletions packages/auth/src/team/Team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import * as invitations from 'invitation/index.js'
import { type ProofOfInvitation } from 'invitation/index.js'
import { normalize } from 'invitation/normalize.js'
import * as lockbox from 'lockbox/index.js'
import { AddRoleInput, ADMIN, type Role } from 'role/index.js'
import { AddRoleInput, ADMIN, Permission, type Role } from 'role/index.js'
import { castServer } from 'server/castServer.js'
import { type Host, type Server } from 'server/types.js'
import { type LocalUserContext } from 'team/context.js'
Expand Down Expand Up @@ -352,8 +352,9 @@ export class Team extends EventEmitter<TeamEvents> {
}

// Post the role to the graph
const eventType = role.permissions && role.permissions[Permission.MODIFIABLE_MEMBERSHIP] === true ? 'ADD_ROLE' : 'ADD_STATIC_ROLE'
this.dispatch({
type: 'ADD_ROLE',
type: eventType,
payload: { ...(role as Role), lockboxes: lockboxes },
})

Expand All @@ -374,8 +375,10 @@ export class Team extends EventEmitter<TeamEvents> {

/** Dispatch the add role action */
private _dispatchAddMemberRole(userId: string, roleName: string, lockboxes: lockbox.Lockbox[]) {
const role = select.role(this.state, roleName)
const eventType = role.permissions && role.permissions[Permission.MODIFIABLE_MEMBERSHIP] === true ? 'ADD_MEMBER_ROLE' : 'ADD_MEMBER_STATIC_ROLE'
this.dispatch({
type: 'ADD_MEMBER_ROLE',
type: eventType,
payload: { userId, roleName, lockboxes },
})
}
Expand Down
4 changes: 3 additions & 1 deletion packages/auth/src/team/isAdminOnlyAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ export const isAdminOnlyAction = (action: TeamLinkBody) => {
'CHANGE_SERVER_KEYS',
'ADMIT_MEMBER',
'ADMIT_DEVICE',
'ADD_MEMBER_ROLE',
'ADD_ROLE',
'ADD_MEMBER_ROLE',
'ADD_STATIC_ROLE',
'ADD_MEMBER_STATIC_ROLE',
'SET_METADATA',
]

Expand Down
10 changes: 8 additions & 2 deletions packages/auth/src/team/membershipResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type TeamLink,
type TeamGraph,
type AdmitMemberAction,
type AddMemberStaticRoleAction,
} from 'team/types.js'
import { arraysAreEqual } from 'util/arraysAreEqual.js'

Expand Down Expand Up @@ -123,7 +124,7 @@ const leastSenior = (chain: TeamGraph, userNames: string[]) =>
userNames.sort(bySeniority(chain)).pop()!

const isAddAction = (link: TeamLink): link is AddActionLink =>
['ADD_MEMBER', 'ADD_MEMBER_ROLE', 'ADMIT_MEMBER'].includes(link.body.type)
['ADD_MEMBER', 'ADD_MEMBER_ROLE', 'ADD_MEMBER_STATIC_ROLE', 'ADMIT_MEMBER', ].includes(link.body.type)

const isRemovalAction = (link: TeamLink): boolean => link.body.type === 'REMOVE_MEMBER'

Expand Down Expand Up @@ -168,6 +169,11 @@ const addedUserId = (link: AddActionLink): string => {
return addAction.payload.userId
}

case 'ADD_MEMBER_STATIC_ROLE': {
const addAction = link.body
return addAction.payload.userId
}

case 'ADMIT_MEMBER': {
const addAction = link.body
return addAction.payload.memberKeys.name
Expand All @@ -185,4 +191,4 @@ const usesInvitation = (invitation: Invitation) => (l: TeamLink) =>
l.body.payload.id === invitation.id

type RemoveActionLink = Link<RemoveMemberAction | RemoveMemberRoleAction, TeamContext>
type AddActionLink = Link<AddMemberAction | AddMemberRoleAction | AdmitMemberAction, TeamContext>
type AddActionLink = Link<AddMemberAction | AddMemberRoleAction | AddMemberStaticRoleAction | AdmitMemberAction, TeamContext>
16 changes: 15 additions & 1 deletion packages/auth/src/team/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ROOT, type Reducer } from '@localfirst/crdx'
import { ADMIN } from 'role/index.js'
import { ADMIN, Permission, type Role } from 'role/index.js'
import { clone, composeTransforms } from 'util/index.js'
import { invalidLinkReducer } from './invalidLinkReducer.js'
import { setHead } from './setHead.js'
Expand Down Expand Up @@ -128,6 +128,20 @@ const getTransforms = (action: TeamAction): Transform[] => {
]
}

case 'ADD_STATIC_ROLE': {
const newRole = action.payload
return [
addRole(newRole), // Add this role to the team
]
}

case 'ADD_MEMBER_STATIC_ROLE': {
const { userId, roleName } = action.payload
return [
...addMemberRoles(userId, [roleName]), // Add this role to the member's list of roles
]
}

case 'ADD_DEVICE': {
const { device } = action.payload
return [
Expand Down
18 changes: 17 additions & 1 deletion packages/auth/src/team/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type { Client, LocalContext } from 'team/context.js'
import type { Device } from 'device/index.js'
import type { Invitation, InvitationState } from 'invitation/types.js'
import type { Lockbox } from 'lockbox/index.js'
import type { PermissionsMap, Role } from 'role/index.js'
import type { PermissionsMap, Role, Permission } from 'role/index.js'
import type { Host, Server } from 'server/index.js'
import type { ValidationResult } from 'util/index.js'
import { Logger, SharedLogger } from '@localfirst/shared'
Expand Down Expand Up @@ -120,6 +120,11 @@ export type AddRoleAction = {
payload: BasePayload & Role
}

export type AddStaticRoleAction = {
type: 'ADD_STATIC_ROLE'
payload: BasePayload & Role
}

export type RemoveRoleAction = {
type: 'REMOVE_ROLE'
payload: BasePayload & {
Expand All @@ -136,6 +141,15 @@ export type AddMemberRoleAction = {
}
}

export type AddMemberStaticRoleAction = {
type: 'ADD_MEMBER_STATIC_ROLE'
payload: BasePayload & {
userId: string
roleName: string
permissions?: PermissionsMap
}
}

export type RemoveMemberRoleAction = {
type: 'REMOVE_MEMBER_ROLE'
payload: BasePayload & {
Expand Down Expand Up @@ -269,6 +283,8 @@ export type TeamAction =
| RemoveDeviceAction
| RemoveRoleAction
| RemoveMemberRoleAction
| AddStaticRoleAction
| AddMemberStaticRoleAction
| InviteMemberAction
| InviteDeviceAction
| RevokeInvitationAction
Expand Down
40 changes: 40 additions & 0 deletions packages/auth/src/team/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type TeamStateValidatorSet,
type ValidationArgs,
} from './types.js'
import { Permission } from 'role/types.js'

export const validate: TeamStateValidator = (previousState: TeamState, link: TeamLink, extendableLogger?: Logger) => {
const logger = extendableLogger != null ? extendableLogger.extend('validate') : new Logger({ moduleName: 'auth:validate' })
Expand Down Expand Up @@ -134,6 +135,45 @@ const validators: TeamStateValidatorSet = {
}
return VALID
},

/** Check that members not listed in the permissions map for a role aren't added */
cantModifyStaticRolesWithAddMemberRole(previousState: TeamState, link: TeamLink, extendableLogger: Logger) {
const logger = extendableLogger.extend('cantModifyStaticRolesWithAddMemberRole')
if (link.body.type === 'ADD_MEMBER_ROLE') {
const { userId: assigningUserId } = link.body
const { userId, roleName } = link.body.payload
const role = select.role(previousState, roleName)
if (role.permissions == null) {
return VALID
}
if (role.permissions[Permission.MODIFIABLE_MEMBERSHIP] == null || role.permissions[Permission.MODIFIABLE_MEMBERSHIP] === true) {
return VALID
}
return fail(`User ${assigningUserId} attempted to assign role ${roleName} to member ${userId} using ADD_MEMBER_ROLE but the role is static`, previousState, link, logger)
}
return VALID
},

/** Check that members not listed in the permissions map for a role aren't added */
cantAddNewMembersToStaticRole(previousState: TeamState, link: TeamLink, extendableLogger: Logger) {
const logger = extendableLogger.extend('cantAddNewMembersToStaticRole')
if (link.body.type === 'ADD_MEMBER_STATIC_ROLE') {
const { userId: assigningUserId } = link.body
const { userId, roleName } = link.body.payload
const role = select.role(previousState, roleName)
if (role.permissions == null) {
return fail(`User ${assigningUserId} attempted to assign role ${roleName} to member ${userId} using ADD_MEMBER_STATIC_ROLE but the role has no permissions`, previousState, link, logger)
}
if (role.permissions[Permission.MODIFIABLE_MEMBERSHIP] == null || role.permissions[Permission.MODIFIABLE_MEMBERSHIP] === true) {
return fail(`User ${assigningUserId} attempted to assign role ${roleName} to member ${userId} using ADD_MEMBER_STATIC_ROLE but the role is not static`, previousState, link, logger)
}
if (role.permissions[Permission.MODIFIABLE_MEMBERSHIP].memberIds.includes(userId)) {
return VALID
}
return fail(`User ${assigningUserId} attempted to assign role ${roleName} to member ${userId} not specified in role's permissions`, previousState, link, logger)
}
return VALID
},
}

const fail = (message: string, previousState: TeamState, link: TeamLink, extendableLogger: Logger) => {
Expand Down
4 changes: 3 additions & 1 deletion packages/auth/src/util/actionFingerprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ export const actionFingerprint = (link: TeamLink) => {
return action.payload.userId
}

case 'ADD_ROLE': {
case 'ADD_ROLE':
case 'ADD_STATIC_ROLE': {
return action.payload.roleName
}

case 'ADD_MEMBER_ROLE':
case 'ADD_MEMBER_STATIC_ROLE':
case 'REMOVE_MEMBER_ROLE': {
return `${action.payload.roleName}:${action.payload.userId}`
}
Expand Down