-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathroles.ts
More file actions
188 lines (169 loc) · 6.39 KB
/
roles.ts
File metadata and controls
188 lines (169 loc) · 6.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright Oxide Computer Company
*/
/*
* Utilities around resource roles and policies. This logic belongs in the data
* layer and not in app/ because we are experimenting with it to decide whether
* it belongs in the API proper.
*/
import { useMemo } from 'react'
import * as R from 'remeda'
import type { FleetRole, IdentityType, ProjectRole, SiloRole } from './__generated__/Api'
import { api, q, usePrefetchedQuery } from './client'
/**
* Union of all the specific roles, which used to all be the same until we added
* limited collaborator to silo.
*/
export type RoleKey = FleetRole | SiloRole | ProjectRole
/** Turn a role order record into a sorted array of strings. */
// used for displaying lists of roles, like in a <select>
const flatRoles = (roleOrder: Record<RoleKey, number>): RoleKey[] =>
R.sortBy(Object.keys(roleOrder) as RoleKey[], (role) => roleOrder[role])
// This is a record only to ensure that all RoleKey are covered. weird order
// on purpose so allRoles test can confirm sorting works
export const roleOrder: Record<RoleKey, number> = {
collaborator: 1,
admin: 0,
viewer: 3,
limited_collaborator: 2,
}
/** `roleOrder` record converted to a sorted array of roles. */
export const allRoles = flatRoles(roleOrder)
// Fleet roles don't include limited_collaborator
export const fleetRoles = allRoles.filter(
(r): r is FleetRole => r !== 'limited_collaborator'
)
/** Given a list of roles, get the most permissive one */
export const getEffectiveRole = <Role extends RoleKey>(roles: Role[]): Role | undefined =>
R.firstBy(roles, (role) => roleOrder[role])
////////////////////////////
// Policy helpers
////////////////////////////
type RoleAssignment<Role extends RoleKey = RoleKey> = {
identityId: string
identityType: IdentityType
roleName: Role
}
export type Policy<Role extends RoleKey = RoleKey> = {
roleAssignments: RoleAssignment<Role>[]
}
/**
* Returns a new updated policy. Does not modify the passed-in policy.
*/
export function updateRole<Role extends RoleKey>(
newAssignment: RoleAssignment<Role>,
policy: Policy<Role>
): Policy<Role> {
const roleAssignments = policy.roleAssignments.filter(
(ra) => ra.identityId !== newAssignment.identityId
)
roleAssignments.push(newAssignment)
return { roleAssignments }
}
/**
* Delete any role assignments for user or group ID. Returns a new updated
* policy. Does not modify the passed-in policy.
*/
export function deleteRole<Role extends RoleKey>(
identityId: string,
policy: Policy<Role>
): Policy<Role> {
const roleAssignments = policy.roleAssignments.filter(
(ra) => ra.identityId !== identityId
)
return { roleAssignments }
}
type UserAccessRow<Role extends RoleKey = RoleKey> = {
id: string
identityType: IdentityType
name: string
roleName: Role
roleSource: string
}
/**
* Role assignments come from the API in (user, role) pairs without display
* names and without info about which resource the role came from. This tags
* each row with that info. It has to be a hook because it depends on the result
* of an API request for the list of users. It's a bit awkward, but the logic is
* identical between projects and orgs so it is worth sharing.
*/
export function useUserRows<Role extends RoleKey = RoleKey>(
roleAssignments: RoleAssignment<Role>[],
roleSource: string
): UserAccessRow<Role>[] {
// HACK: because the policy has no names, we are fetching ~all the users,
// putting them in a dictionary, and adding the names to the rows
const { data: users } = usePrefetchedQuery(q(api.userList, {}))
const { data: groups } = usePrefetchedQuery(q(api.groupList, {}))
return useMemo(() => {
const userItems = users?.items || []
const groupItems = groups?.items || []
const usersDict = Object.fromEntries(userItems.concat(groupItems).map((u) => [u.id, u]))
return roleAssignments.map((ra) => ({
id: ra.identityId,
identityType: ra.identityType,
// A user might not appear here if they are not in the current user's
// silo. This could happen in a fleet policy, which might have users from
// different silos. Hence the ID fallback. The code that displays this
// detects when we've fallen back and includes an explanatory tooltip.
name: usersDict[ra.identityId]?.displayName || ra.identityId,
roleName: ra.roleName,
roleSource,
}))
}, [roleAssignments, roleSource, users, groups])
}
type SortableUserRow = { identityType: IdentityType; name: string }
/**
* Comparator for array sort. Group groups and users, then sort by name within
* groups and within users.
*/
export function byGroupThenName(a: SortableUserRow, b: SortableUserRow) {
const aGroup = Number(a.identityType === 'silo_group')
const bGroup = Number(b.identityType === 'silo_group')
return bGroup - aGroup || a.name.localeCompare(b.name)
}
export type Actor = {
identityType: IdentityType
displayName: string
id: string
}
/**
* Fetch lists of users and groups, filtering out the ones that are already in
* the given policy.
*/
export function useActorsNotInPolicy<Role extends RoleKey = RoleKey>(
policy: Policy<Role>
): Actor[] {
const { data: users } = usePrefetchedQuery(q(api.userList, {}))
const { data: groups } = usePrefetchedQuery(q(api.groupList, {}))
return useMemo(() => {
// IDs are UUIDs, so no need to include identity type in set value to disambiguate
const actorsInPolicy = new Set(policy?.roleAssignments.map((ra) => ra.identityId) || [])
const allGroups = groups.items.map((g) => ({
...g,
identityType: 'silo_group' as IdentityType,
}))
const allUsers = users.items.map((u) => ({
...u,
identityType: 'silo_user' as IdentityType,
}))
// groups go before users
return allGroups.concat(allUsers).filter((u) => !actorsInPolicy.has(u.id)) || []
}, [users, groups, policy])
}
export function userRoleFromPolicies(
user: { id: string },
groups: { id: string }[],
policies: Policy[]
): RoleKey | null {
const myIds = new Set([user.id, ...groups.map((g) => g.id)])
const myRoles = policies
.flatMap((p) => p.roleAssignments) // concat all the role assignments together
.filter((ra) => myIds.has(ra.identityId))
.map((ra) => ra.roleName)
return getEffectiveRole(myRoles) || null
}