Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
c59c55a
Add access tabs showing all users and groups
charliepark Mar 5, 2026
2df92c6
Add role and member count to users and groups tabs
charliepark Mar 5, 2026
74708fc
add docs link, better microcopy
charliepark Mar 5, 2026
f0dfa91
Roles tab is redundant, with Users / Groups present
charliepark Mar 5, 2026
f8785c8
Add tooltip to show source of role when it comes from group
charliepark Mar 5, 2026
1a6461c
Update tests
charliepark Mar 6, 2026
2b61971
Updated styling on sidebars
charliepark Mar 6, 2026
b959e59
Use consistent subtitle style for group sidebars
charliepark Mar 6, 2026
ff357a6
Merge branch 'main' into full_user_group_lists
charliepark Mar 7, 2026
7f05526
Add time_created column
charliepark Mar 7, 2026
7d966e2
stub out user sidebars for more info, tabs
charliepark Mar 7, 2026
c18f2d7
Merge main and resolve conflict
charliepark Mar 11, 2026
c83f6ea
Add additional data to User and Group sidebars
charliepark Mar 11, 2026
d40e4c0
Updates to sidebar and roles table
charliepark Mar 12, 2026
35c79bd
Refactor / consolidate
charliepark Mar 12, 2026
7de7785
More refactoring, plus remeda
charliepark Mar 12, 2026
f0cadbf
Add button to copy IDs of Users, Groups, though will try to set up li…
charliepark Mar 12, 2026
997b892
Merge branch 'main' into full_user_group_lists
charliepark Mar 16, 2026
f663d9d
Add Users & Groups nav
charliepark Mar 16, 2026
e852345
Refactor
charliepark Mar 16, 2026
8ca63fd
Fix broken access form
charliepark Mar 16, 2026
6423d4b
merge main and resolve conflicts
charliepark Mar 18, 2026
e6304ed
merge main and resolve conflicts (again)
charliepark Mar 18, 2026
7ccc0fc
Remove Users & Groups from Project page / nav
charliepark Mar 19, 2026
9b0710d
Don't show all users on access pages; only assigned, inherited from s…
charliepark Mar 19, 2026
a14f499
Merge branch 'main' into full_user_group_lists
charliepark Mar 19, 2026
ae4f2c3
Merge branch 'main' into full_user_group_lists
charliepark Mar 23, 2026
5fa4800
Merge main and resolve conflicts
charliepark Apr 24, 2026
e16bf02
Merge branch 'main' into full_user_group_lists
charliepark Apr 30, 2026
7b881e7
Patch up several small issues
charliepark Apr 30, 2026
bdd4a75
Add e2e tests
charliepark Apr 30, 2026
0db4102
Consolidate Users and Groups into Silo Access page
charliepark May 4, 2026
7196089
Align Project Access page design with Silo Access; put Groups as firs…
charliepark May 5, 2026
fecb287
Merge main and resolve conflicts
charliepark May 5, 2026
a7815db
Merge branch 'main' into full_user_group_lists_consolidated
charliepark Jun 26, 2026
20066c2
Fetch all-ish and sort client side
charliepark Jun 30, 2026
bfb58d2
test revisions
charliepark Jun 30, 2026
c3a742f
Merge branch 'main' into full_user_group_lists_consolidated
charliepark Jul 2, 2026
1ad9c31
Refactor API calls; update tests; remove dead code
charliepark Jul 3, 2026
f46e988
More refactoring and shared code extracting
charliepark Jul 3, 2026
2fefc69
Users and Groups have own page on silo view
charliepark Jul 9, 2026
9561020
sort users and groups by display name in dropdown
charliepark Jul 10, 2026
b9dad64
Add sidebars to silo access page
charliepark Jul 10, 2026
54ada75
Fix a few issues with memoization, inherited roles, etc.
charliepark Jul 11, 2026
6122195
Merge branch 'main' into full_user_group_lists_consolidated
charliepark Jul 14, 2026
5cf57ed
Pull in logic and copy from PR 3263
charliepark Jul 14, 2026
97ede69
Add to Fleet roles page and refactor out duplicated code
charliepark Jul 15, 2026
3fd38e2
add note to AGENTS.md about top-level routes so we don't forget about it
david-crespo Jul 22, 2026
02f17df
Un-abstract users/groups tabs to silo-only (#3300)
david-crespo Jul 22, 2026
e52a6e1
delete unused effectiveScopedRole
david-crespo Jul 23, 2026
33f3cd9
AccessRolesTable: replace local roleIn with rolesByIdFromPolicy
david-crespo Jul 23, 2026
09fbc17
remove role disable tooltip copy tweak
david-crespo Jul 23, 2026
289c20d
Add modal to show all roles on hover on Users and Groups page
charliepark Jul 23, 2026
374ae04
drop ScopedPolicy[] in favor of explicit siloPolicy/projectPolicy args
david-crespo Jul 23, 2026
81ef513
simplify role actions
david-crespo Jul 23, 2026
58601cb
gray out disabled remove-role actions
david-crespo Jul 23, 2026
cc4f35f
Revert to remove +N affordance on Silo Access; use group icon
charliepark Jul 23, 2026
08a8adf
replace membership query version key with combine
david-crespo Jul 23, 2026
d0f11d2
merge main and resolve conflicts
charliepark Jul 31, 2026
6588e7f
Merge main and resolve conflicts
charliepark Sep 24, 2026
9ec0751
Update action copy when no direct assigned role
charliepark Sep 30, 2026
0a37460
Merge branch 'main' into full_user_group_lists_consolidated
charliepark Oct 8, 2026
784cd44
Update ListboxField to ComboboxField on access forms
charliepark Oct 8, 2026
f520597
npm run fmt
charliepark Oct 8, 2026
8ce8d97
fix tests
charliepark Oct 8, 2026
371728b
Merge branch 'main' into full_user_group_lists_consolidated
charliepark Oct 9, 2026
bdd6388
Merge branch 'main' into full_user_group_lists_consolidated
charliepark Oct 9, 2026
03a7b66
Use proper linkable pages for users and groups
charliepark Oct 10, 2026
0b3b6e6
bump OMICRON_VERSION to get new Nexus routes
charliepark Oct 10, 2026
8f56f3e
fix test
charliepark Oct 10, 2026
34adf85
Update permissions to better handle accounts with fleet roles
charliepark Oct 11, 2026
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
- Breadcrumbs come from route `handle.crumb`; use `makeCrumb`/`titleCrumb` and provide a `path` when the parent route redirects (`app/hooks/use-crumbs.ts`). Use `titleCrumb` for side modal forms that should appear in page title but not nav breadcrumbs (check `Crumb.titleOnly` flag).
- When adding tabs or redirects, wire the canonical link in the path builder (e.g., point to the default tab) and update the sidebar/quick actions as needed.
- For tabs synced with the URL, use `QueryParamTabs` (`app/components/QueryParamTabs.tsx`).
- Adding a new _top-level_ path segment (e.g., `/users`) requires a matching console page endpoint in Nexus, or direct navigation and refresh at that URL will 404. Nexus can't use a catchall route (it would overlap API routes), so each top-level console prefix is registered as an `unpublished` endpoint serving the console index: definitions in [`nexus/external-api/src/lib.rs`](https://github.com/oxidecomputer/omicron/blob/4b0ce4b/nexus/external-api/src/lib.rs#L8921-L8928), handlers in [`nexus/src/external_api/http_entrypoints.rs`](https://github.com/oxidecomputer/omicron/blob/4b0ce4b/nexus/src/external_api/http_entrypoints.rs#L8765-L8769). Routes nested under existing prefixes (`/projects/*`, `/system/*`, `/settings/*`, `/lookup/*`) need no Nexus change.

# Forms

Expand Down
2 changes: 1 addition & 1 deletion OMICRON_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6d241a7d21d11cb7979122bde9a906d8d4837d09
26f5e91524fe0ca6c61a8c90f2321d59f323f60e
2 changes: 1 addition & 1 deletion app/api/__generated__/OMICRON_VERSION

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions app/api/__generated__/nexus-console.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion app/api/__tests__/safety.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ it('mock-api is only referenced in test files', () => {
"test/e2e/ip-pool-silo-config.e2e.ts",
"test/e2e/profile.e2e.ts",
"test/e2e/project-access.e2e.ts",
"test/e2e/silo-access.e2e.ts",
"tsconfig.json",
]
`)
Expand Down
128 changes: 59 additions & 69 deletions app/api/roles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import {
deleteRole,
getEffectiveRole,
roleOrder,
rolesByIdFromPolicy,
updateRole,
userRoleFromPolicies,
userScopedRoleEntries,
type Policy,
} from './roles'

Expand Down Expand Up @@ -75,87 +76,76 @@ describe('deleteRole', () => {
})
})

const user1 = {
id: 'user1',
}

const groups = [{ id: 'group1' }, { id: 'group2' }]
test('byGroupThenName sorts as expected', () => {
const a = { identityType: 'silo_group' as const, name: 'a' }
const b = { identityType: 'silo_group' as const, name: 'b' }
const c = { identityType: 'silo_user' as const, name: 'c' }
const d = { identityType: 'silo_user' as const, name: 'd' }
const e = { identityType: 'silo_user' as const, name: 'e' }

describe('getEffectiveRole', () => {
it('returns null when there are no policies', () => {
expect(userRoleFromPolicies(user1, groups, [])).toBe(null)
})
expect([c, e, b, d, a].sort(byGroupThenName)).toEqual([a, b, c, d, e])
})

it('returns null when there are no roles', () => {
expect(userRoleFromPolicies(user1, groups, [{ roleAssignments: [] }])).toBe(null)
describe('rolesByIdFromPolicy', () => {
it('maps each identity to its role', () => {
expect(rolesByIdFromPolicy(abcAdminPolicy)).toEqual(new Map([['abc', 'admin']]))
})

it('returns role if user matches directly', () => {
expect(
userRoleFromPolicies(user1, groups, [
{
roleAssignments: [
{ identityId: 'user1', identityType: 'silo_user', roleName: 'admin' },
],
},
])
).toEqual('admin')
it('keeps the strongest role when an identity has multiple assignments', () => {
const policy: Policy = { roleAssignments: [abcViewer, abcAdmin] }
expect(rolesByIdFromPolicy(policy)).toEqual(new Map([['abc', 'admin']]))
const reversed: Policy = { roleAssignments: [abcAdmin, abcViewer] }
expect(rolesByIdFromPolicy(reversed)).toEqual(new Map([['abc', 'admin']]))
})
})

it('returns strongest role if both group and user match', () => {
expect(
userRoleFromPolicies(user1, groups, [
{
roleAssignments: [
{ identityId: 'user1', identityType: 'silo_user', roleName: 'viewer' },
{ identityId: 'group1', identityType: 'silo_group', roleName: 'collaborator' },
],
},
])
).toEqual('collaborator')
describe('userScopedRoleEntries', () => {
it('collapses multiple assignments for the same identity to the strongest role', () => {
// API permits multiple assignments for one identity in a single policy
const policy: Policy = {
roleAssignments: [
{ identityId: 'u', identityType: 'silo_user', roleName: 'viewer' },
{ identityId: 'u', identityType: 'silo_user', roleName: 'admin' },
],
}
expect(userScopedRoleEntries('u', [], policy)).toEqual([
{ roleName: 'admin', scope: 'silo', source: { type: 'direct' } },
])
})

it('ignores groups and users that do not match', () => {
expect(
userRoleFromPolicies(user1, groups, [
{
roleAssignments: [
{ identityId: 'other', identityType: 'silo_user', roleName: 'viewer' },
{ identityId: 'group3', identityType: 'silo_group', roleName: 'viewer' },
],
},
])
).toEqual(null)
it('emits one entry per direct assignment and per group, tagged by scope', () => {
const group = { id: 'g', displayName: 'g' }
const silo: Policy = {
roleAssignments: [
{ identityId: 'g', identityType: 'silo_group', roleName: 'viewer' },
],
}
const project: Policy = {
roleAssignments: [{ identityId: 'u', identityType: 'silo_user', roleName: 'admin' }],
}
expect(userScopedRoleEntries('u', [group], silo, project)).toEqual([
{ roleName: 'viewer', scope: 'silo', source: { type: 'group', group } },
{ roleName: 'admin', scope: 'project', source: { type: 'direct' } },
])
})

it('resolves multiple policies', () => {
expect(
userRoleFromPolicies(user1, groups, [
{
roleAssignments: [
{ identityId: 'user1', identityType: 'silo_user', roleName: 'viewer' },
],
},
{
roleAssignments: [
{ identityId: 'group1', identityType: 'silo_group', roleName: 'admin' },
],
},
])
).toEqual('admin')
it('keeps a separate entry per group even when the role is identical', () => {
const groupA = { id: 'a', displayName: 'a' }
const groupB = { id: 'b', displayName: 'b' }
const silo: Policy = {
roleAssignments: [
{ identityId: 'a', identityType: 'silo_group', roleName: 'collaborator' },
{ identityId: 'b', identityType: 'silo_group', roleName: 'collaborator' },
],
}
// same role via two groups must not collapse — each source is shown separately
expect(userScopedRoleEntries('u', [groupA, groupB], silo)).toEqual([
{ roleName: 'collaborator', scope: 'silo', source: { type: 'group', group: groupA } },
{ roleName: 'collaborator', scope: 'silo', source: { type: 'group', group: groupB } },
])
})
})

test('byGroupThenName sorts as expected', () => {
const a = { identityType: 'silo_group' as const, name: 'a' }
const b = { identityType: 'silo_group' as const, name: 'b' }
const c = { identityType: 'silo_user' as const, name: 'c' }
const d = { identityType: 'silo_user' as const, name: 'd' }
const e = { identityType: 'silo_user' as const, name: 'e' }

expect([c, e, b, d, a].sort(byGroupThenName)).toEqual([a, b, c, d, e])
})

test('allRoles', () => {
expect(allRoles).toEqual(['admin', 'collaborator', 'limited_collaborator', 'viewer'])
})
Loading
Loading