Skip to content

feat: provision, activate, and reclaim namespace member accounts#6626

Merged
gustavosbarreto merged 1 commit into
masterfrom
feature/member-provisioning
Jul 9, 2026
Merged

feat: provision, activate, and reclaim namespace member accounts#6626
gustavosbarreto merged 1 commit into
masterfrom
feature/member-provisioning

Conversation

@gustavosbarreto

Copy link
Copy Markdown
Member

Adding a member by an email that has no account used to fail with "user
must already exist", forcing that person to self-register before they
could be added. This adds the missing lane: the member is provisioned on
the spot.

Adding such a member now creates a not-confirmed account and its
namespace membership in a single transaction. The invitee finishes the
account through a one-time activation link (a set-password token in
Redis, 24h TTL, consumed on use), so an admin never sees or handles a
password.

On self-hosted Enterprise a namespace admin who is not a system admin can
provision too, but the account is created awaiting approval and stays
inert until a system admin approves it. Only a system admin can mint the
activation link for an unapproved account; once approved, a namespace
admin who manages the member can mint it as well. Community keeps the
"must already exist" behavior unless the capability is enabled.

Removing a member's last namespace membership on a bound single-namespace
instance now reclaims the orphaned account, since there it can neither
self-register nor own a namespace.

The approval side (the Enterprise capability toggle and the system-admin
approve endpoint) lands in shellhub-io/cloud.

@gustavosbarreto gustavosbarreto requested review from a team as code owners July 9, 2026 19:25
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Claude finished @gustavosbarreto's task in 15m 31s —— View job


Code Review Complete

Reviewed 40 files across code quality, security, testing, language patterns, and architecture. 1 inline issue posted (1 critical).

The bulk of the PR is well-built: the single-transaction provisioning, the one-time/24h Redis activation token, the nginx public/private split (/activate is correctly public while /activation stays behind auth), the deleteOrphanedMemberAccount guards, and the cross-repo wiring with cloud/ (OnMemberAdd, EnableNonAdminProvisioning, migration 010, mock/interface parity) all check out.

Additional notes (not tied to a single line)

  • AwaitingApproval can get stuck true after activation. When an instance admin mints for an unapproved account, CreateUserActivationToken skips the AwaitingApproval gate and ActivateUser never clears the flag — so an account can become fully Confirmed/usable while awaiting_approval stays true, leaving it permanently listed in the console's "Account Requests" queue (useAdminAccountRequests filters on awaiting_approval == true). Consider clearing the flag when an admin mints/activates directly, or requiring approval-before-mint. Closely tied to the inline critical fix in api/services/user.go.
  • Provisioning behavior is effectively unasserted in tests. In api/services/member_test.go, every provisioning case stubs WithTransaction with .Return(nil), and that mock does not execute the callback — so UserCreate is never invoked and the AwaitingApproval: !user.Admin, Status: NotConfirmed, empty-password provisioning are asserted nowhere. Inverting !user.Admin (which would defeat the enterprise approval gate) passes the whole suite. Using RunAndReturn/Run on WithTransaction to run the closure and assert the UserCreate args would close this.
  • deleteOrphanedMemberAccount's remaining > 0 guard is untested — there's no case with InstanceTenantID set and the removed user still in another namespace, which is the guard preventing wrongful deletion of an account that outlives the membership.

To request another review round, comment /review.

Comment thread api/services/user.go
Adding a member by an email with no account now provisions a
not-confirmed account and its membership in one step, instead of
failing with "user must already exist". The invitee completes the
account through a one-time activation link (a set-password token in
Redis, 24h TTL, single use), so an admin never handles a password.

On self-hosted Enterprise a namespace admin who is not a system admin
can provision too, but the account is created awaiting approval and
stays inert until a system admin approves it. Only an admin can mint
the activation link for an unapproved account; once approved, a
namespace admin who manages the member can mint it too. Community
keeps the "must already exist" behavior unless the capability is on.

Removing the last membership of a bound single-namespace instance now
deletes the orphaned member account.
@gustavosbarreto

Copy link
Copy Markdown
Member Author

/review

@gustavosbarreto gustavosbarreto force-pushed the feature/member-provisioning branch from 83dfb56 to 0fd5b79 Compare July 9, 2026 19:52
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Claude finished @gustavosbarreto's task in 8m 53s —— View job


Code Review Complete

Reviewed 42 files across code quality, security, testing, language patterns, and architecture — no new issues found.

The one material issue in this PR — the account-takeover on api/services/user.go:142 (activation/minting not restricted to not-confirmed accounts, no role-hierarchy check) — was already posted inline in the previous review round and remains the thing to fix. Nothing new surfaced this pass.

Notes from this round (context, not new findings):

  • deleteOrphanedMemberAccount (api/services/member.go:345) hard-deleting any orphaned account (including confirmed, pre-existing ones) on a bound single-namespace instance is intended — it's documented at member.go:333-344 and matches the PR description's reclaim behavior. Not a defect.
  • The prior round's "additional notes" about provisioning being unasserted are not accurate: member_test.go wires WithTransaction with a Run(...) that executes the transaction closure, so UserCreate fires and the Status: NotConfirmed / AwaitingApproval: !user.Admin / empty-password shape is enforced via MatchedBy. The remaining > 0 guard in deleteOrphanedMemberAccount is also covered. Test coverage of the new gates is solid.

To request another review round, comment /review.

@gustavosbarreto

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Code Review Complete

The automated review ran but did not post an updated summary — this usually means no new issues were found since the previous review. If you've pushed changes and want a fresh pass, comment /review.

View job

@gustavosbarreto gustavosbarreto merged commit d45aa18 into master Jul 9, 2026
17 checks passed
@gustavosbarreto gustavosbarreto deleted the feature/member-provisioning branch July 9, 2026 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant