"Archive" was misleading for users: the action sets a profile to
DEACTIVATED while keeping the person in the organization. Rename it to
"deactivate" across the API, CLI, MCP, n8n, and console UI.
Consolidate the two overlapping operations into a single deactivateUser
backed by the fuller, guarded logic (SCIM guard, last-active-owner
guard, invitation expiry, signature cancellation, membership update,
webhook) and authorized via iam:membership-profile:deactivate. Remove
the archiveUser surface and the thin state-only deactivate path.
Signed-off-by: Émile Ré <emile@probo.com>
Replace the binary profile ACTIVE/INACTIVE model with PENDING, ACTIVE,
and DEACTIVATED so invited-but-not-yet-activated members remain
assignable to assets, data, and risks instead of being treated like
deactivated users.
Add activated_at/deactivated_at timestamps and Mark* lifecycle helpers,
and update every transition (create, invite/re-invite, activation,
archive, SCIM, SAML, sessions, compliance-portal grant) to the new
states. Expose a multi-state states[] filter across coredata, GraphQL,
MCP, and the console owner pickers, which now request ACTIVE and
PENDING members.
A migration renames the membership_state enum, classifies existing
inactive profiles as PENDING from recent invitation activity, and
backfills the new timestamp columns.
Signed-off-by: Émile Ré <emile@probo.com>
The source headers, LICENSE files, and license metadata had drifted
apart. Align the entire project to MIT:
- Convert every source-file header to the MIT text across all comment
styles (Go, TS, TSX, JS, MJS, SQL, CSS, GraphQL, shell), including
SPDX-License-Identifier tags
- Set the root and cookie-banner LICENSE files to the MIT text with a
"MIT License" title line
- Switch the package.json license fields, Docker image label, and
cookie-banner README to MIT
- Update docs and the genmodels header generator accordingly
- Normalize copyright lines to a single format
(Copyright (c) <year(s)> Probo Inc <hello@probo.com>.): unify the
hello@getprobo.com and hello@probo.inc emails to hello@probo.com and
the comma-separated years to a hyphenated range
Genuine third-party references are intentionally left untouched: the
Lucide icon attributions (Lucide is ISC) and the trivy dependency
license allowlist.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
Replace the deny-based restriction on granting OWNER with role-scoped
allow policies so authorization fails closed: admins may create and
update memberships only when the assigned role is not OWNER, and the
absence of a target role no longer implies permission.
To keep console UI gating accurate without loosening the base grants,
the permission field gains an optional typed options argument
(PermissionOptionsInput) that forwards target_role into the dry-run
authorization. Only the two role-related console calls (create user,
update membership) pass it; the OWNER option stays hidden for admins via
the existing assignable-roles helper.
Add a non-regression test that an admin cannot promote a member to OWNER
while still being able to change members between non-owner roles.
An organization ADMIN could hard-remove members, including OWNERs, because
removeUser (connect and MCP) only checked the weaker iam:membership-profile:delete
gate. Authorize the owner-only iam:membership:delete instead, and expose the
source attribute on MembershipProfile so the owner grant's non-SCIM condition
can match.
Consolidate ownership-grant authorization into policy for both createUser and
updateMembership: each resolver passes the requested role as a target_role
attribute and ADMIN is denied granting ownership via deny-create-owner /
deny-promote-owner. target_role is distinct from resource.role, which is the
target's current role and guards editing existing owners. With no callers left,
the iam:membership-role:set-owner action (grant and OAuth2 scope) is removed.
Also pass the authorized scope through to the RemoveUser/CreateUser services,
gate the console Remove action on iam:membership:delete, and add regression
tests plus a changelog entry.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
An organization ADMIN could mint an OWNER membership via createUser, which
only gated iam:membership-profile:create and bypassed the owner-only
iam:membership-role:set-owner check that updateMembership already enforces.
Gate the requested role in both createUser entry points (connect resolver
and the MCP CreateUserTool) with an additional set-owner authorization when
the role is OWNER, mirroring updateMembership. Add a regression test that
locks the ADMIN/OWNER privilege boundary the fix relies on.
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
Deleting a profile still referenced elsewhere (for example as an
asset owner) surfaced an internal error. PostgreSQL reports ON DELETE
RESTRICT blocks as SQLSTATE 23001, not 23503; map both in profile
delete and propagate ErrProfileInUse through removeUser as CONFLICT.
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
Separate the owner membership variable from the range loop in the e2e regression to match the repository's whitespace lint rules.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Use the Connect schema's profile connection in the e2e regression so the test can find the sole owner's membership. Adjust whitespace around the resolver error path to satisfy Go lint.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Return a conflict when membership role updates would demote the final active owner in an organization.
Add an end-to-end regression that verifies the mutation fails and leaves the owner role intact.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
When UpdateUser sets a contract end date that is already in the past,
the user can no longer fulfill outstanding signature requests. Delete
their still-pending requests as part of the same update so they stop
appearing as awaiting signatures.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Disabling a profile now removes pending document signature requests tied to that signatory. This keeps inactive accounts from retaining signature work across manual, archived, and SCIM deactivation flows.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Restore RemoveUser as a hard delete operation and surface dependency\nconflicts with a dedicated IAM error.\n\nAdd a new ArchiveUser flow that deactivates profiles while keeping the\nmember in the organization, then expose both actions across Connect, MCP,\nCLI, n8n, console UI, and e2e coverage.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Switch remove-user behavior for manually managed profiles from hard\ndelete to archival by deactivating the profile. This matches the\nrequested SCIM-like lifecycle while avoiding dependency errors for\nlinked records such as signatures and assets.\n\nThe remove flow now updates profile state to INACTIVE, updates\nmembership timestamps, and emits a user-updated webhook event instead of\ndelete events. E2E coverage now asserts that remove keeps the profile\nand marks it inactive.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
The CountActiveOwnerByOrganizationID query joins iam_membership_profiles with
iam_memberships, but used an unqualified tenant_id = @tenant_id in the WHERE
clause. Since both tables have a tenant_id column, PostgreSQL raised an
"ambiguous column" error when deleting an owner. Fixed by prefixing with the
table alias (p.tenant_id) to match the pattern used in CountByOrganizationID.
Added TestUser_RemoveOwner e2e test to verify one owner can remove another.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>