Files
probo/contrib/claude/authorization.md
Sacha Al Himdani eecbe4c46c Rename vendors to third parties
Renames the user-facing 'vendor' concept to 'third party' across the
entire codebase. The shared common_third_parties reference table is
unchanged.

Migration. Renames the vendor_category enum, the vendors and
vendor_<entity> tables (contacts, services, compliance_reports,
business_associate_agreements, data_privacy_agreements,
risk_assessments) and their vendor_id columns, the asset_vendors /
data_vendors / processing_activity_vendors junction tables,
generated_documents.vendors_document_id, the webhook_event_type
'vendor:<verb>' values, and the snapshots_type 'VENDORS' value.

Backend. Renames coredata models and SQL queries, probo services,
GraphQL / MCP API surface, console / trust / webhook resolvers and
types, the CLI (prb vendor* -> prb third-party*; pkg/cmd/vendormgmt
-> pkg/cmd/thirdpartymgmt), the document generator, vetting agent
prompts, and the common-third-parties-import command.

Frontend, packages, n8n, e2e. Renames apps/console pages, components,
hooks, routes, dialogs, and tabs; the shared @probo/vendors package
(now @probo/third-parties); the @probo/ui Vendors atoms (now
ThirdParties, VendorLogo -> ThirdPartyLogo); the n8n community node
actions/vendor folder (now actions/thirdParty); and the e2e Go test
suite (console and MCP). Filesystem and URL paths use kebab-case
(third-parties), GraphQL fields and TypeScript identifiers use
camelCase (thirdParty / thirdParties), Go types use PascalCase
(ThirdParty), and human-facing text uses 'third party' with a space.

Co-authored-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-05-13 21:21:39 +02:00

7.3 KiB

Authorization — IAM & Policy

Policy-based authorization in pkg/iam/ using an evaluation model similar to AWS IAM. Explicit deny > explicit allow > implicit deny.

Policies are Go code, not database rows. All policy logic is assembled from Go structs at startup (pkg/probo/policies.go, pkg/iam/iam_policies.go). The database only stores the authz_role enum and membership rows — there is no policies or permissions table. Never create migrations for policy storage.

Core concepts

Policy — a named collection of statements:

policy.NewPolicy("thirdParty-crud", "ThirdParty CRUD",
	policy.Allow(ActionThirdPartyGet, ActionThirdPartyList).WithSID("read-thirdParties"),
	policy.Deny(ActionThirdPartyDelete).WithSID("deny-thirdParty-delete"),
).WithDescription("Standard third party access")

Statement — a single permission rule with effect (allow/deny), actions, optional resources, and optional conditions.

Action formatSERVICE:RESOURCE:OPERATION with wildcard support:

core:thirdParty:create      # specific action
core:thirdParty:*           # all third party actions
core:*                  # all core actions
*                       # everything

Policy evaluation

The evaluator processes all statements against a request:

  1. If any statement explicitly denies → DecisionDeny
  2. If any statement explicitly allows → DecisionAllow
  3. No match → DecisionNoMatch (implicit deny)

Authorizer flow

Authorizer is the main orchestrator in pkg/iam/authorizer.go:

err := iamService.Authorizer.Authorize(ctx, iam.AuthorizeParams{
	Principal:          identityID,    // who
	Resource:           thirdPartyID,      // what
	Action:             probo.ActionThirdPartyGet,  // which action
	ResourceAttributes: map[string]string{},    // optional extra attributes
})

The flow:

  1. Load organization membership for the resource's organization
  2. Load principal attributes (identity + membership role)
  3. Load resource attributes via AuthorizationAttributes() on the entity
  4. Build policies: identity-scoped + role-specific
  5. Evaluate all policies
  6. Return ErrInsufficientPermissions if no allow match

PolicySet

Policies are organized into identity-scoped (applied to all authenticated users) and role-based:

ps := iam.NewPolicySet().
	AddRolePolicy("OWNER", OwnerPolicy).
	AddRolePolicy("ADMIN", AdminPolicy).
	AddRolePolicy("VIEWER", ViewerPolicy).
	AddIdentityScopedPolicy(SelfManagePolicy)

Register during service initialization:

iamService.Authorizer.RegisterPolicySet(ProboPolicySet())

Conditions (attribute-based access control)

Conditions constrain when a statement applies. All conditions must be satisfied.

// Users can only access resources in their organization
organizationCondition := policy.Equals("principal.organization_id", "resource.organization_id")

policy.Allow(ActionThirdPartyGet).
	WithSID("view-thirdParty").
	When(organizationCondition)
Operator Purpose
policy.Equals(key, value) Key equals value
policy.NotEquals(key, value) Key does not equal value
policy.In(key, value) Key in list (supports comma-separated DB fields)
policy.NotIn(key, value) Key not in list

Key paths use principal.ATTR or resource.ATTR (e.g., principal.organization_id, resource.source).

AuthorizationAttributer interface

Resources that support authorization must implement this interface in pkg/coredata/:

func (v *ThirdParty) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) {
	q := `SELECT organization_id FROM thirdParties WHERE id = $1 LIMIT 1;`
	var organizationID gid.GID
	if err := conn.QueryRow(ctx, q, v.ID).Scan(&organizationID); err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, ErrResourceNotFound
		}
		return nil, fmt.Errorf("cannot query third party authorization attributes: %w", err)
	}
	return map[string]string{"organization_id": organizationID.String()}, nil
}

The returned map provides attributes for condition evaluation (e.g., resource.organization_id).

Error types

var (
	ErrInsufficientPermissions // access denied
	ErrAssumptionRequired      // session assumption needed
	ErrUnsupportedPrincipalType // principal is not an Identity
)

Integration in resolvers

GraphQL resolvers use AuthorizeFunc from pkg/server/api/authz/:

if err := authorize(ctx, thirdPartyID, probo.ActionThirdPartyGet); err != nil {
	return nil, err
}

MCP resolvers use MustAuthorize which panics (caught by middleware):

r.MustAuthorize(ctx, input.ID, probo.ActionThirdPartyGet)

File locations

What File
Product action constants (core:*) pkg/probo/actions.go
IAM action constants (iam:*) pkg/iam/iam_actions.go
Product role policies (ProboPolicySet) pkg/probo/policies.go
IAM role policies (IAMPolicySet) pkg/iam/iam_policies.go
Authorizer + AuthorizationAttributer pkg/iam/authorizer.go
PolicySet registration pkg/iam/policy_set.go
GraphQL authz helper pkg/server/api/authz/authorization.go
MCP authz + recovery pkg/server/api/mcp/v1/resolver.go, mcputils/recovery.go

Action constants

IAM actions live in pkg/iam/iam_actions.go, probo actions in pkg/probo/actions.go. Follow the naming pattern:

const (
	ActionThirdPartyGet    = "core:thirdParty:get"
	ActionThirdPartyList   = "core:thirdParty:list"
	ActionThirdPartyCreate = "core:thirdParty:create"
	ActionThirdPartyUpdate = "core:thirdParty:update"
	ActionThirdPartyDelete = "core:thirdParty:delete"
)

Built-in role policies

Role Access level
OWNER Full access to all features including org management
ADMIN Full access to core features, restricted org management
VIEWER Read-only access to most entities
AUDITOR Read-only, excludes internal/employee content
EMPLOYEE Can sign documents and view internal content

New entity IAM wiring

When adding a new entity that needs authorization:

  1. Action constants — add core:<entity>:<verb> constants in pkg/probo/actions.go (get, list, create, update, delete)
  2. Role policies — wire actions into the appropriate role policies in pkg/probo/policies.go (OwnerPolicy, AdminPolicy, ViewerPolicy, etc.) with organization_id condition
  3. AuthorizationAttributes — implement on the coredata entity struct, returning at minimum {"organization_id": ...} (use the denormalized OrganizationID field — see coredata doc)
  4. Entity type registry — register in pkg/coredata/entity_type_reg.go and NewEntityFromID so the authorizer can construct the entity from its GID
  5. Resolver calls — add r.authorize(ctx, id, probo.ActionEntityGet) in GraphQL resolvers and r.MustAuthorize(ctx, id, probo.ActionEntityGet) in MCP resolvers

Key patterns

  • Always use organization_id condition — most policies scope access to the principal's organization
  • SID every statement.WithSID("description") for debugging
  • Explicit denies for restrictions — even if allow would match, deny takes precedence
  • Identity-scoped for self-management — cross-org permissions like managing own profile
  • Role-based for org features — CRUD operations on domain entities