contract

package
v1.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 8, 2026 License: Apache-2.0 Imports: 34 Imported by: 0

Documentation

Overview

authz.go: the authorization decision in front of every dashboard intent.

The REST API refuses a caller with no identity, then asks the engine whether that caller holds the permission the route maps to (see api/auth.go, defaultAuthorize). The dashboard contract path is a second door onto the same mutations, and it had neither step: an empty `requires` predicate allows everyone in Forge's contract transport, including a request with no user at all, and the dashboard's own auth middleware never blocks. So this file puts the REST model on the contract path.

Every intent in manifest.yaml declares `requires: { warden: warden.engine }`. Forge's transport runs that delegate after the (empty) boolean predicate and before the dispatcher, and treats an error or a non-allow decision as a 403. The delegate below refuses anything it cannot positively identify and authorize.

Package contract wires warden into the Forge dashboard's contract path. It registers the `warden` contributor with the dashboard's contract registry and answers the intents the React plugin reads.

Warden continues to expose its templ pages through DashboardContributor while this package grows; the templ dashboard is retired once every surface has an equivalent here. See warden/MIGRATION.md for the accounting.

handlers_assignments.go: the assignment surface.

Assignments are create-and-delete only, because assignment.Store exposes no update. So there is no patch input here and no edit form on the page: changing who holds a role means deleting one binding and making another.

The one thing this file cares about more than the rest is expiry. The engine's resolution path filters expired assignments, so an expired row grants nothing. The LISTING path does not filter them, so they are visible. A page that showed them without saying so would be reporting access that does not exist, which is why every projected row carries Expired.

handlers_checklogs.go: the check log list and one check's detail.

There is no purge intent here. Retention runs through maintenance.run, which purges only the caller's tenant's entries older than the configured retention window (PurgeCheckLogsForTenant), never an arbitrary range.

handlers_config.go: the read-only view of the engine's configuration.

Warden's Config comes from Forge config, not from a store, so there is no write path and no settings intent. The templ dashboard's settings panel had no inputs either: it showed some of these values as text and badges.

handlers_graphs.go: the two graph views, the schema graph and the rooted relation expansion.

The schema graph is drawn from resource types alone: every relation a type declares names the subject types it accepts, and each one is an edge. It says what the schema allows, not what is stored.

The expansion is the opposite. It runs the ReBAC walker's own traversal from one object and relation with no target (Engine.ExpandRelation), so it shows what is stored and reachable from there, under the engine's graph budget. It reads tuples only, so it needs no resource type grant.

handlers_maintenance.go: the two operational commands.

These are the only writes warden's config surface has. maintenance.run purges the caller's tenant's expired assignments and, when CheckLogRetention is positive, that tenant's check log entries past it. Cache invalidation is the manual version of what a write would do automatically.

handlers_namespaces.go: the derived namespace list.

There is no namespace entity in warden. No table, no CRUD, no create. A namespace exists only as a string on rows, so this scans the entity tables for distinct values. Check log rows count too, because a leaf namespace where checks run may hold no role, grant or assignment of its own, but only when the path is valid: Check never validates one. The tenant root ("") is always present, because it is a real place where things live and the filter needs it as an option distinct from "all namespaces".

handlers_overview.go: the entity counters and the recent-checks list.

handlers_permissions.go: the permission surface.

One thing about permissions matters more than the rest: the RBAC evaluator matches a request against Resource + ":" + Action, and never against Name. So Name is a label, and a permission whose name disagrees with its resource and action is invisible to every check that looks for it by name while appearing correct in every list. This file refuses to write that state.

handlers_playground.go: the policy playground's explain intent.

playground.batchCheck runs several checks as dry runs at one namespace and reports each outcome.

playground.explain runs one authorization check as a dry run and reports what each model did. The check it builds writes no check log row, fires no hooks and reads no cache, so it is a query: a viewer who may run checks may run it. Authorizing the call is a dry run too, as for every query.

handlers_policies.go: the policy surface, reads and writes.

A policy row carries two kinds of fact. The stored fields say what was written. analysePolicy says what the policy will do, and the two disagree more often than they should: an allow whose condition throws looks exactly like a working allow, and a deny with a broken condition applies to every check. So the state, failsClosed, neverApplies and matchesEverything flags on every row come from the analysis, computed at read time, never stored and never guessed by a page.

The writes hold two rules the REST API does not. Every write is validated by collectPolicyIssues, so the dashboard cannot store a condition that cannot behave the way it reads. And a create is always stored inactive: a new policy with no matchers matches every check in its namespace and below, so storing it active would make an empty allow grant everything. Activation is an explicit policies.setActive.

handlers_relations.go: the relation surface.

A tuple is object#relation@subject, Zanzibar style: document:readme#viewer@user:bob.

Two things about tuples that differ from everything else in this contract.

They are create-and-delete only. relation.Store exposes no update, so changing a tuple means deleting one and writing another. There is no patch input here and no edit form on the page.

Their namespace cascades at check time but not in the list. Like roles and policies, a tuple stored at a namespace is in scope for a check in that namespace and in every namespace below it: the engine passes AncestorNamespaces(checkNamespace) to every tuple lookup a check makes, in the direct check, the expression evaluator and the graph walker alike (see evaluateReBAC in engine.go and TestReBAC_NamespaceCascade). The list filter, by contrast, is an exact match on purpose, so it shows what is stored in one namespace. A namespace's listing therefore does NOT show a parent's tuples that also apply there, and a page must say so rather than let the listing read as everything in effect.

handlers_resourcetypes.go: the ReBAC schema surface.

A resource type declares its relations (viewer, editor, parent) and the permissions derived from them (read = viewer or editor or parent->read).

Those expressions are a real language with a real parser, dsl.CompileExpr, which returns diagnostics carrying Pos{Line, Col}. Nothing calls it on a write. api/resourcetype_handler.go copies Expression straight through, and the store stores whatever it is handed. An invalid expression therefore saves without complaint and fails at CHECK time, where evaluateReBAC logs a warning and treats it as no match. The permission silently never grants, and nothing an operator can see says why.

So this file validates on write, twice. First the expression must parse. Second, every relation it names must be one the type declares, which the parser cannot know and which is the other way to write an expression that can never match.

handlers_roles.go: the role surface.

Roles carry more than the templ dashboard ever showed: a namespace, the system and default flags, a member cap, and parent-slug inheritance. Slugs are unique per (tenant, namespace), which is why the namespace is on every read and every write.

handlers_schema.go: the schema as Warden source. schema.export writes a tenant's state as canonical source, schema.plan reports what applying an edited copy would do without doing it, and schema.apply does it, but only as planned: it re-plans and refuses unless the digest the operator saw still matches.

Both wrap the dsl package. The dashboard never reads a file, so anything the file loader would supply is refused instead: imports, because there is nothing to resolve them against, and a `tenant` that is not the caller's, because the dashboard applies to the caller's tenant only and a source that says otherwise is either a mistake or an attempt to aim at another tenant. Variable placeholders (`${NAME}`) are refused by the lexer itself: it has no `$` token, so the parser reports the character where it stands.

The two intents are gated on read of warden:role, which intentPolicies enforces, and they return warden:permission, warden:policy, warden:resourcetype and warden:relation rows as source. The handler asks for those reads itself, as subjects.detail does, so a caller who may read roles cannot read the rest through this door.

handlers_subjects.go: what one subject can do at a namespace, and why.

immutable.go: the system-entity guard.

READ THIS BEFORE REMOVING IT AS REDUNDANT. It is not redundant. No store checks IsSystem on update or delete, and the engine does not check it. The rule lives in role.CheckWritable and permission.CheckWritable, which this file and the REST API (api/role_handler.go, api/permission_handler.go) both call, so the two refuse the same writes in the same words. Without these two functions this dashboard will let an operator rename a system role, change its parent, or delete it, and the store will do it.

The guard belongs here rather than in each handler so that the plans after this one cannot add a role or permission write that forgets it.

schema.plan and schema.apply write through dsl.Apply, which cannot call these two. They set dsl.ApplyOptions.ProtectSystem instead, which refuses the same writes with the same sentence, as a diagnostic at the declaration. That sentence names a role by its slug, which is what the source declares, where this guard and REST name it by its name.

members.go: the member-cap guards.

READ THIS BEFORE REMOVING THEM AS REDUNDANT. No store enforces Role.MaxMembers: none counts a role's members before inserting an assignment, and none compares a new cap with the members a role already has. warden defines ErrMaxMembersExceeded, and nothing returns it.

The cap is enforced above the store, in package assignment, and every warden write path calls it. Assigning a subject goes through assignment.CheckMemberCap here (guardMemberCap) and in the REST assignment create. Lowering a cap goes through assignment.CheckCapLowering here (guardCapLowering), in the REST role update and in a DSL apply. The one deliberate exception is BootstrapAdmin (extension/bootstrap.go), the break-glass path that assigns the bootstrap admin role whatever its cap says, so a capped bootstrap role can end up over its cap. Code that writes through the store directly checks nothing.

mutate.go: what every dashboard write does after the store call.

The REST handlers in api/ emit a typed hook and an audit event for each mutation, with the caller as the actor. A dashboard write used to skip all of it: the store changed, and nothing else heard about it. The consequences were an audit plugin that never saw dashboard changes, a cache that kept serving an ALLOW for a grant that was gone until its TTL ran out, and CreatedBy/UpdatedBy left empty.

This helper duplicates the small emit pattern from api/role_handler.go and api/permission_handler.go instead of sharing it. Sharing would need a third package both api/ and extension/contract import, for about ten lines, and the two call sites differ in what they have in hand (a forge context there, a Principal here).

paging.go: the one paging shape every list intent in this contract uses.

Warden's stores all expose ListX(filter) plus CountX(filter) with Limit and Offset, and none of them offers a cursor. So this contract pages by offset everywhere, and there is no second convention to translate between.

policy_analysis.go: what a policy will actually do, decided before any check runs.

Every rule here mirrors warden's evaluator (evaluator.go) and is tested against it in policy_analysis_test.go by running each case through warden.NewConditionEvaluator. If the engine changes, that test fails, rather than a page quietly describing behaviour the engine no longer has. One rule mirrors the engine instead: requiredFields is the set of fields engine.go's prepareCheck refuses when empty, so it follows Check, not the evaluator.

Why this exists: nothing validates a policy on write (policy.Validate has no non-test caller, and the REST create stores whatever it is sent), and several shapes store fine while doing something other than what they read as. `context.ip not_in "10.0.0.0/8"` with a string instead of a list is always true, so on a deny it denies everyone. `gt` against a non-number is always false. `action.verb` never resolves, so `neq` on it is always true.

policy_validate.go: refuse, on write, the policy shapes that store fine but cannot behave the way they read.

Nothing below the contract does this: policy.Validate has no non-test caller. And policy.ValidateCondition on its own is not enough, because it accepts fields the evaluator never resolves (bare "action", "action.x") and says nothing about the always-true and always-false shapes classifyCondition finds.

Index

Constants

View Source
const (
	StateActive    = "active"
	StateInactive  = "inactive"
	StateScheduled = "scheduled"
	StateExpired   = "expired"
	StateNever     = "never"
)

Policy states. "never" is a window whose end precedes its start: under EffectiveAt it cannot be in effect at any instant.

Variables

This section is empty.

Functions

func Register

Register loads the embedded manifest, validates it, registers the `warden` contributor with reg, and binds the handlers against deps.

Types

type AckResponse

type AckResponse struct {
	ID string `json:"id,omitempty"`
}

AckResponse is what a command returns when the only thing worth reporting is which row it touched. ID is empty for a delete.

type AssignmentCreateInput

type AssignmentCreateInput struct {
	RoleID        string `json:"roleId"`
	SubjectKind   string `json:"subjectKind"`
	SubjectID     string `json:"subjectId"`
	NamespacePath string `json:"namespacePath,omitempty"`
	ResourceType  string `json:"resourceType,omitempty"`
	ResourceID    string `json:"resourceId,omitempty"`
	ExpiresAt     string `json:"expiresAt,omitempty"`
}

AssignmentCreateInput binds a subject to a role.

ExpiresAt is an RFC3339 string rather than a time, because that is what the wire carries and parsing it here lets a malformed value be a BAD_REQUEST naming the field instead of a JSON decode failure naming nothing.

type AssignmentDeleteInput

type AssignmentDeleteInput struct {
	ID string `json:"id"`
}

AssignmentDeleteInput names the binding to remove.

type AssignmentSummary

type AssignmentSummary struct {
	ID            string `json:"id"`
	NamespacePath string `json:"namespacePath"`
	RoleID        string `json:"roleId"`
	RoleSlug      string `json:"roleSlug"`
	RoleName      string `json:"roleName"`
	SubjectKind   string `json:"subjectKind"`
	SubjectID     string `json:"subjectId"`
	ResourceType  string `json:"resourceType,omitempty"`
	ResourceID    string `json:"resourceId,omitempty"`
	ExpiresAt     string `json:"expiresAt,omitempty"`
	Expired       bool   `json:"expired"`
	GrantedBy     string `json:"grantedBy,omitempty"`
	CreatedAt     string `json:"createdAt"`
}

AssignmentSummary is one row of the assignments list.

RoleSlug is denormalised deliberately. A row that named only a role id would be unreadable, and resolving it per row in the page would be a request per row.

Expired is computed rather than stored, because the store has no such column: it is ExpiresAt compared against now, using the same test the engine applies when it resolves roles.

type AssignmentsListInput

type AssignmentsListInput struct {
	PageRequest
	NamespacePath *string `json:"namespacePath,omitempty"`
	RoleID        string  `json:"roleId,omitempty"`
	SubjectKind   string  `json:"subjectKind,omitempty"`
	SubjectID     string  `json:"subjectId,omitempty"`
	ResourceType  string  `json:"resourceType,omitempty"`
	ResourceID    string  `json:"resourceId,omitempty"`
}

AssignmentsListInput filters the assignments list.

type AssignmentsListResponse

type AssignmentsListResponse struct {
	PageMeta
	Items []AssignmentSummary `json:"items"`
}

AssignmentsListResponse is the paged reply.

type CacheInvalidateInput

type CacheInvalidateInput struct {
	SubjectKind string `json:"subjectKind,omitempty"`
	SubjectID   string `json:"subjectId,omitempty"`
}

CacheInvalidateInput selects what to flush. Both fields empty flushes the whole tenant; both set flushes one subject. One without the other is an error rather than a silent widening to the tenant.

type CacheInvalidateResult

type CacheInvalidateResult struct {
	Scope string `json:"scope"` // "tenant" or "subject"
}

CacheInvalidateResult reports which scope was flushed, so the page can say what it did rather than "done".

type CheckLogDetail

type CheckLogDetail struct {
	CheckLogSummary
	AppID       string          `json:"appId,omitempty"`
	MatchedBy   []CheckLogMatch `json:"matchedBy"`
	Obligations []string        `json:"obligations"`
	RequestIP   string          `json:"requestIp,omitempty"`
	RequestID   string          `json:"requestId,omitempty"`
	TraceID     string          `json:"traceId,omitempty"`
}

CheckLogDetail is the checkLogs.detail reply: the summary plus what an auditor needs to reconstruct the decision without replaying the check.

type CheckLogDetailInput

type CheckLogDetailInput struct {
	ID string `json:"id"`
}

CheckLogDetailInput is the checkLogs.detail request.

type CheckLogLossView

type CheckLogLossView struct {
	QueueFull   uint64 `json:"queueFull"`
	WriteFailed uint64 `json:"writeFailed"`
	Since       string `json:"since"`
}

CheckLogLossView is the engine's count of decided checks that left no row. It is per server process and covers every tenant that process serves, because the writer's queue is shared.

type CheckLogMatch

type CheckLogMatch struct {
	Source string `json:"source"`
	RuleID string `json:"ruleId,omitempty"`
	Detail string `json:"detail,omitempty"`
}

CheckLogMatch is one rule that contributed to a decision.

type CheckLogSummary

type CheckLogSummary struct {
	ID            string `json:"id"`
	NamespacePath string `json:"namespacePath"`
	SubjectKind   string `json:"subjectKind"`
	SubjectID     string `json:"subjectId"`
	Action        string `json:"action"`
	ResourceType  string `json:"resourceType"`
	ResourceID    string `json:"resourceId"`
	Decision      string `json:"decision"`
	Reason        string `json:"reason,omitempty"`
	EvalTimeNs    int64  `json:"evalTimeNs"`
	Cached        bool   `json:"cached"`
	Error         string `json:"error,omitempty"`
	CreatedAt     string `json:"createdAt"`
}

CheckLogSummary is one row of the recent-checks list and of the check log page. Cached and Error are carried because they are this page's scan signal: allow versus deny volume depends on the deployment's posture, so neither can be the thing colour keys off.

type CheckLogsListInput

type CheckLogsListInput struct {
	PageRequest
	// NamespacePath is a pointer because the root namespace is the empty
	// string: nil means every namespace, "" means the root only.
	NamespacePath *string `json:"namespacePath,omitempty"`
	SubjectKind   string  `json:"subjectKind,omitempty"`
	SubjectID     string  `json:"subjectId,omitempty"`
	Action        string  `json:"action,omitempty"`
	ResourceType  string  `json:"resourceType,omitempty"`
	ResourceID    string  `json:"resourceId,omitempty"`
	Decision      string  `json:"decision,omitempty"`
	Cached        *bool   `json:"cached,omitempty"`
	// After and Before are RFC 3339 instants, both inclusive. Empty means
	// unbounded.
	After  string `json:"after,omitempty"`
	Before string `json:"before,omitempty"`
}

CheckLogsListInput is the checkLogs.list request. Every filter is optional and they combine with AND.

type CheckLogsListResponse

type CheckLogsListResponse struct {
	PageMeta
	Items []CheckLogSummary `json:"items"`
	// NotRecorded is absent when check logging is off.
	NotRecorded *CheckLogLossView `json:"notRecorded,omitempty"`
}

CheckLogsListResponse is the checkLogs.list reply.

type ConditionIssue

type ConditionIssue struct {
	Index   int    `json:"index"`
	Message string `json:"message"`
}

ConditionIssue marks one condition row.

type ConditionProblem

type ConditionProblem string

ConditionProblem is a condition's outcome when it does not depend on the check being made.

const (
	ProblemNone        ConditionProblem = ""
	ProblemThrows      ConditionProblem = "throws"      // evaluation errors: a deny fails closed, an allow is skipped
	ProblemAlwaysFalse ConditionProblem = "alwaysFalse" // the policy can never apply
	ProblemAlwaysTrue  ConditionProblem = "alwaysTrue"  // the condition restricts nothing
)

type ConditionReason

type ConditionReason string

ConditionReason says why a condition's outcome is fixed.

const (
	ReasonNone              ConditionReason = ""
	ReasonUnknownOperator   ConditionReason = "unknownOperator"
	ReasonInvalidRegex      ConditionReason = "invalidRegex"
	ReasonUnresolvableField ConditionReason = "unresolvableField"
	ReasonNotAList          ConditionReason = "notAList"
	ReasonEmptyList         ConditionReason = "emptyList"
	ReasonNotANumber        ConditionReason = "notANumber"
	ReasonNoValidCIDR       ConditionReason = "noValidCIDR"
	ReasonNotATime          ConditionReason = "notATime"
	ReasonAlwaysPresent     ConditionReason = "alwaysPresent"
	ReasonMatchesAnything   ConditionReason = "matchesAnything"
)

type ConfigDetail

type ConfigDetail struct {
	MaxGraphDepth     int  `json:"maxGraphDepth"`
	MaxGraphVisited   int  `json:"maxGraphVisited"`
	MaxGraphFanout    int  `json:"maxGraphFanout"`
	MaxBatchChecks    int  `json:"maxBatchChecks"`
	CacheTTLSeconds   int  `json:"cacheTtlSeconds"`
	CacheMaxSize      int  `json:"cacheMaxSize"`
	RBACEnabled       bool `json:"rbacEnabled"`
	ABACEnabled       bool `json:"abacEnabled"`
	ReBACEnabled      bool `json:"rebacEnabled"`
	CheckLogEnabled   bool `json:"checkLogEnabled"`
	RequireTenant     bool `json:"requireTenant"`
	EvaluateAllModels bool `json:"evaluateAllModels"`
	CheckLogQueueSize int  `json:"checkLogQueueSize"`
	// CheckLogRetentionSeconds and MaintenanceIntervalSeconds carry the
	// durations in whole seconds, rounded up, so a positive duration never
	// reads as 0, and 0 means off: the engine purges no check log entries
	// (or runs no maintenance loop). The engine reads a 0 or negative
	// duration as off, and both are sent as 0, so the wire never carries a
	// negative number. CheckLogRetentionHours and MaintenanceIntervalMin
	// stay for clients older than the seconds fields. They are rounded down
	// and also 0 when off, so there a retention under an hour reads 0, the
	// same as off.
	CheckLogRetentionSeconds   int64 `json:"checkLogRetentionSeconds"`
	MaintenanceIntervalSeconds int64 `json:"maintenanceIntervalSeconds"`
	CheckLogRetentionHours     int64 `json:"checkLogRetentionHours"`
	MaintenanceIntervalMin     int64 `json:"maintenanceIntervalMinutes"`
	// Plugins is the name of every plugin in the engine's registry when the
	// request was served, sorted. It can include plugins warden registers on
	// its own, such as the cache invalidators and the audit log sink, beside
	// any a caller passed in. Never nil: an engine with no registry sends [].
	Plugins []string `json:"plugins"`
}

ConfigDetail is the config.detail response.

Durations are flattened to whole units because the wire carries JSON and a Go duration serialises as a nanosecond integer nobody can read. The page formats from these.

type Deps

type Deps struct {
	// Engine is the live warden engine. Required.
	Engine *warden.Engine

	// DefaultTenantID is the tenant every dashboard request is scoped to
	// when the principal carries no tenant claim. Required for any
	// deployment that wants the dashboard to answer at all today, because
	// nothing populates Principal.Claims yet.
	//
	// Leave it empty in a multi-tenant deployment. Every read then refuses
	// with PERMISSION_DENIED, which is correct: a dashboard that cannot
	// tell which tenant it is looking at must not guess, and the empty
	// string would match every tenant's rows rather than none.
	DefaultTenantID string
}

Deps bundles what the contract handlers need at registration time.

type ExpiringInput

type ExpiringInput struct {
	WithinHours int `json:"withinHours,omitempty"`
	Limit       int `json:"limit,omitempty"`
}

ExpiringInput asks what lapses within a horizon.

type ExpiringResponse

type ExpiringResponse struct {
	Items []AssignmentSummary `json:"items"`
}

ExpiringResponse is the reply. Not paged: this is a "what needs attention" feed with a caller-set limit, not a browsable collection, so it carries no PageMeta. Compare overview.recentChecks in the spine plan.

type ExpressionDiagnostic

type ExpressionDiagnostic struct {
	Permission string `json:"permission"`
	Line       int    `json:"line"`
	Col        int    `json:"col"`
	Message    string `json:"message"`
}

ExpressionDiagnostic is one problem found in a permission expression. Line and Col are 1-based and relative to the expression text, which is what lets an editor mark the exact column.

type MaintenanceResult

type MaintenanceResult struct {
	AssignmentsPurged int64 `json:"assignmentsPurged"`
	CheckLogsPurged   int64 `json:"checkLogsPurged"`
}

MaintenanceResult is the maintenance.run reply.

Both counters are always present, including at zero, because zero is a real outcome: the run succeeded and there was nothing to purge. A page that could not distinguish that from a failure would have to guess.

type NamespacesResponse

type NamespacesResponse struct {
	Namespaces []string `json:"namespaces"`
}

NamespacesResponse is the namespaces.list reply. Paths are sorted, with the tenant root first because the empty string sorts first anyway and the filter renders it at the top.

type OverviewStats

type OverviewStats struct {
	Roles         int64 `json:"roles"`
	Permissions   int64 `json:"permissions"`
	Assignments   int64 `json:"assignments"`
	Relations     int64 `json:"relations"`
	Policies      int64 `json:"policies"`
	ResourceTypes int64 `json:"resourceTypes"`
}

OverviewStats is the overview.stats response: one counter per entity kind in a single payload, so the six stat tiles cost one request.

type PageMeta

type PageMeta struct {
	Total  int64 `json:"total"`
	Limit  int   `json:"limit"`
	Offset int   `json:"offset"`
}

PageMeta is embedded in every list intent's response, beside its items.

Total is the count matching the filter rather than the number of rows returned, which is what a pager needs to know how many pages exist. Limit and Offset are echoed back as clamped, so a client that asked for a million rows can see what it actually got.

type PageRequest

type PageRequest struct {
	Limit  int `json:"limit,omitempty"`
	Offset int `json:"offset,omitempty"`
}

PageRequest is embedded in every list intent's input.

func (PageRequest) Clamp

func (p PageRequest) Clamp() (limit, offset int)

Clamp returns a limit and offset safe to hand to a store filter. An unset or non-positive limit becomes the default, an oversized one is capped, and a negative offset becomes zero rather than reaching SQL as OFFSET -1.

type PermissionCreateInput

type PermissionCreateInput struct {
	Name          string `json:"name,omitempty"`
	Resource      string `json:"resource"`
	Action        string `json:"action"`
	NamespacePath string `json:"namespacePath,omitempty"`
	Description   string `json:"description,omitempty"`
}

PermissionCreateInput creates a permission. Name may be omitted, in which case it is derived as resource:action.

type PermissionDefDTO

type PermissionDefDTO struct {
	Name       string `json:"name"`
	Expression string `json:"expression"`
}

PermissionDefDTO mirrors resourcetype.PermissionDef on the wire.

type PermissionDeleteInput

type PermissionDeleteInput struct {
	ID string `json:"id"`
}

PermissionDeleteInput names the permission to remove.

type PermissionDetail

type PermissionDetail struct {
	PermissionSummary
	GrantedBy []RoleSummary `json:"grantedBy"`
}

PermissionDetail is one permission plus the roles that grant it, which is the question an operator opens this page to answer.

type PermissionDetailInput

type PermissionDetailInput struct {
	ID string `json:"id"`
}

PermissionDetailInput names one permission.

type PermissionRef

type PermissionRef struct {
	Name          string `json:"name"`
	NamespacePath string `json:"namespacePath,omitempty"`
}

PermissionRef names one permission by its natural key.

The junction is keyed by (namespacePath, name), not by id, because that is what the DSL declares and what survives a re-apply. An empty NamespacePath means the tenant root, which is a real namespace rather than an absent value.

type PermissionSummary

type PermissionSummary struct {
	ID            string `json:"id"`
	NamespacePath string `json:"namespacePath"`
	Name          string `json:"name"`
	Resource      string `json:"resource"`
	Action        string `json:"action"`
	Description   string `json:"description,omitempty"`
	IsSystem      bool   `json:"isSystem"`
	CreatedAt     string `json:"createdAt"`
	UpdatedAt     string `json:"updatedAt"`
}

PermissionSummary is one row of the permissions list, and one grant on a role's detail page. The same shape serves both so the role page can link each grant to its own permission row.

type PermissionUpdateInput

type PermissionUpdateInput struct {
	ID          string  `json:"id"`
	Description *string `json:"description,omitempty"`
}

PermissionUpdateInput patches a permission. Resource and action are absent deliberately: changing either would change what the permission means without changing which roles grant it, silently altering every check that resolves through it. Delete and recreate instead.

type PermissionsListInput

type PermissionsListInput struct {
	PageRequest
	NamespacePath *string `json:"namespacePath,omitempty"`
	Resource      string  `json:"resource,omitempty"`
	Action        string  `json:"action,omitempty"`
	Search        string  `json:"search,omitempty"`
	IsSystem      *bool   `json:"isSystem,omitempty"`
}

PermissionsListInput filters the permissions list.

type PermissionsListResponse

type PermissionsListResponse struct {
	PageMeta
	Items []PermissionSummary `json:"items"`
}

PermissionsListResponse is the paged reply.

type PlaygroundBatchInput

type PlaygroundBatchInput struct {
	// NamespacePath applies to every item. "" is the root.
	NamespacePath string                `json:"namespacePath"`
	Items         []PlaygroundBatchItem `json:"items"`
}

PlaygroundBatchInput is the playground.batchCheck request.

There is no tenant field, on purpose. The tenant is the caller's, resolved by tenantFrom.

type PlaygroundBatchItem

type PlaygroundBatchItem struct {
	SubjectKind  string `json:"subjectKind"`
	SubjectID    string `json:"subjectId"`
	Action       string `json:"action"`
	ResourceType string `json:"resourceType"`
	ResourceID   string `json:"resourceId,omitempty"`
}

PlaygroundBatchItem is one check of a batch.

type PlaygroundBatchResponse

type PlaygroundBatchResponse struct {
	// Results is in item order, one per item.
	Results []PlaygroundBatchResult `json:"results"`
}

PlaygroundBatchResponse is the playground.batchCheck reply.

type PlaygroundBatchResult

type PlaygroundBatchResult struct {
	// Decision is "error" when the item's check failed, with Error set.
	Decision string `json:"decision"`
	Allowed  bool   `json:"allowed"`
	Reason   string `json:"reason,omitempty"`
	Error    string `json:"error,omitempty"`
}

PlaygroundBatchResult is the outcome of one item.

type PlaygroundExplainInput

type PlaygroundExplainInput struct {
	SubjectKind  string `json:"subjectKind"`
	SubjectID    string `json:"subjectId"`
	Action       string `json:"action"`
	ResourceType string `json:"resourceType"`
	ResourceID   string `json:"resourceId,omitempty"`
	// NamespacePath is where the check runs. "" is the tenant root. Always
	// explicit: a playground check is at a namespace, never "any".
	NamespacePath      string         `json:"namespacePath"`
	Context            map[string]any `json:"context,omitempty"`
	SubjectAttributes  map[string]any `json:"subjectAttributes,omitempty"`
	ResourceAttributes map[string]any `json:"resourceAttributes,omitempty"`
}

PlaygroundExplainInput is the playground.explain request.

There is no tenant field, on purpose. The tenant is the caller's, resolved by tenantFrom, so a request can never ask about another tenant's data.

type PlaygroundExplainResponse

type PlaygroundExplainResponse struct {
	// Decision is "error" when a model failed, with Error set; otherwise
	// the merged decision.
	Decision    string          `json:"decision"`
	Allowed     bool            `json:"allowed"`
	Reason      string          `json:"reason,omitempty"`
	Error       string          `json:"error,omitempty"`
	MatchedBy   []CheckLogMatch `json:"matchedBy"`
	Obligations []string        `json:"obligations"`
	EvalTimeNs  int64           `json:"evalTimeNs"`
	// Lanes is always three, in pipeline order: rbac, rebac, abac.
	Lanes []PlaygroundLane `json:"lanes"`
}

PlaygroundExplainResponse is the playground.explain reply.

type PlaygroundLane

type PlaygroundLane struct {
	// Model is "rbac", "rebac" or "abac".
	Model string `json:"model"`
	// State is one of warden.LaneState's values.
	State           string          `json:"state"`
	Decision        string          `json:"decision,omitempty"`
	Reason          string          `json:"reason,omitempty"`
	MatchedBy       []CheckLogMatch `json:"matchedBy"`
	WalkTruncated   bool            `json:"walkTruncated,omitempty"`
	ExpressionError string          `json:"expressionError,omitempty"`
	Error           string          `json:"error,omitempty"`
}

PlaygroundLane is one authorization model's part in an explained check.

type PoliciesListInput

type PoliciesListInput struct {
	PageRequest
	NamespacePath *string `json:"namespacePath,omitempty"`
	Effect        string  `json:"effect,omitempty"`
	IsActive      *bool   `json:"isActive,omitempty"`
	Search        string  `json:"search,omitempty"`
}

PoliciesListInput filters the policy list. NamespacePath is a pointer for the same reason it is on every other list: nil means every namespace and "" means the root namespace only.

type PoliciesListResponse

type PoliciesListResponse struct {
	PageMeta
	Items []PolicySummary `json:"items"`
}

PoliciesListResponse is the paged reply.

type PolicyCondition

type PolicyCondition struct {
	ID       string `json:"id,omitempty"`
	Field    string `json:"field"`
	Operator string `json:"operator"`
	Value    any    `json:"value,omitempty"`
}

PolicyCondition is one condition on the wire. Value's JSON type follows the operator: a list for in, not_in and ip_in_cidr (ip_in_cidr also accepts one string), a number for gt, lt, gte and lte, an RFC3339 string for time_after and time_before, absent for exists and not_exists, and a string otherwise.

type PolicyConditionView

type PolicyConditionView struct {
	PolicyCondition
	Problem string `json:"problem,omitempty"`
	Reason  string `json:"reason,omitempty"`
}

PolicyConditionView is a stored condition plus what it will do.

type PolicyCreateInput

type PolicyCreateInput struct {
	PolicyDraft
	NamespacePath string `json:"namespacePath,omitempty"`
}

PolicyCreateInput creates a policy. It is always stored INACTIVE: a new policy with no matchers matches every check in its namespace and below, so storing it active would make an empty allow grant everything, or an empty deny lock everyone out, the moment it is saved. Activation is an explicit policies.setActive. The REST create honours isActive; this contract deliberately does not.

type PolicyDeleteInput

type PolicyDeleteInput struct {
	ID string `json:"id"`
}

PolicyDeleteInput names the policy to remove.

type PolicyDetail

type PolicyDetail struct {
	PolicySummary
	Subjects              []PolicySubject       `json:"subjects"`
	Actions               []string              `json:"actions"`
	Resources             []string              `json:"resources"`
	Conditions            []PolicyConditionView `json:"conditions"`
	Obligations           []string              `json:"obligations"`
	NotBefore             string                `json:"notBefore,omitempty"`
	NotAfter              string                `json:"notAfter,omitempty"`
	SubjectsUnrestricted  bool                  `json:"subjectsUnrestricted"`
	ActionsUnrestricted   bool                  `json:"actionsUnrestricted"`
	ResourcesUnrestricted bool                  `json:"resourcesUnrestricted"`
	HasRoleMatcher        bool                  `json:"hasRoleMatcher"`
	// DecidingCondition is the index of the condition that makes the
	// policy fail closed or never apply. Absent when neither.
	DecidingCondition *int   `json:"decidingCondition,omitempty"`
	CreatedBy         string `json:"createdBy,omitempty"`
	UpdatedBy         string `json:"updatedBy,omitempty"`
	CreatedAt         string `json:"createdAt"`
}

PolicyDetail is one policy, everything the rule block renders.

type PolicyDetailInput

type PolicyDetailInput struct {
	ID string `json:"id"`
}

PolicyDetailInput names the policy to read, by id.

type PolicyDraft

type PolicyDraft struct {
	Name        string            `json:"name"`
	Description string            `json:"description,omitempty"`
	Effect      string            `json:"effect"`
	Priority    int               `json:"priority"`
	NotBefore   string            `json:"notBefore,omitempty"`
	NotAfter    string            `json:"notAfter,omitempty"`
	Subjects    []PolicySubject   `json:"subjects"`
	Actions     []string          `json:"actions"`
	Resources   []string          `json:"resources"`
	Conditions  []PolicyCondition `json:"conditions"`
	Obligations []string          `json:"obligations"`
}

PolicyDraft is the editable body of a policy.

type PolicyIssues

type PolicyIssues struct {
	Fields     map[string]string `json:"fields"`
	Conditions []ConditionIssue  `json:"conditions"`
}

PolicyIssues is every reason a draft cannot be saved. Fields is keyed by wire name: name, effect, window, subjects, actions, resources, obligations. Conditions carries one entry per bad row, so a page can mark the exact row.

type PolicySetActiveInput

type PolicySetActiveInput struct {
	ID     string `json:"id"`
	Active bool   `json:"active"`
}

PolicySetActiveInput turns a policy on or off and changes nothing else.

type PolicySubject

type PolicySubject struct {
	Kind string `json:"kind,omitempty"`
	ID   string `json:"id,omitempty"`
	Role string `json:"role,omitempty"`
}

PolicySubject mirrors policy.SubjectMatch. Its three fields are AND-ed.

type PolicySummary

type PolicySummary struct {
	ID                string `json:"id"`
	NamespacePath     string `json:"namespacePath"`
	Name              string `json:"name"`
	Description       string `json:"description,omitempty"`
	Effect            string `json:"effect"`
	Priority          int    `json:"priority"`
	IsActive          bool   `json:"isActive"`
	State             string `json:"state"`
	FailsClosed       bool   `json:"failsClosed"`
	NeverApplies      bool   `json:"neverApplies"`
	MatchesEverything bool   `json:"matchesEverything"`
	Version           int    `json:"version"`
	UpdatedAt         string `json:"updatedAt"`
}

PolicySummary is one row of the policies list.

State, FailsClosed, NeverApplies and MatchesEverything are computed by analysePolicy, because the stored fields do not say them and a page that guessed would guess wrong: an allow whose condition throws looks exactly like a working allow.

type PolicyUpdateInput

type PolicyUpdateInput struct {
	ID              string             `json:"id"`
	ExpectedVersion *int               `json:"expectedVersion,omitempty"`
	Name            *string            `json:"name,omitempty"`
	Description     *string            `json:"description,omitempty"`
	Effect          *string            `json:"effect,omitempty"`
	Priority        *int               `json:"priority,omitempty"`
	NotBefore       *string            `json:"notBefore,omitempty"`
	NotAfter        *string            `json:"notAfter,omitempty"`
	Subjects        *[]PolicySubject   `json:"subjects,omitempty"`
	Actions         *[]string          `json:"actions,omitempty"`
	Resources       *[]string          `json:"resources,omitempty"`
	Conditions      *[]PolicyCondition `json:"conditions,omitempty"`
	Obligations     *[]string          `json:"obligations,omitempty"`
}

PolicyUpdateInput patches a policy. Namespace is not patchable. An empty string on NotBefore or NotAfter clears that bound.

ExpectedVersion is the version the caller loaded. When it is present and older than the stored version, the update is refused as stale and nothing is written, so an editor cannot save over a change it never saw. A version above the stored one, or a negative one, was never stored, so it is refused as bad input. Absent, the update is still guarded against a write that lands between this handler's own read and write, but not against anything older.

type PolicyValidateResponse

type PolicyValidateResponse struct {
	Valid bool `json:"valid"`
	// MatchesEverything is the same analysis the policy list applies, run over
	// the submitted draft: true when, saved as it stands, the policy would
	// apply to every check. It lets an editor confirm before an active policy
	// is saved into that shape, without repeating the analysis in the page.
	MatchesEverything bool `json:"matchesEverything"`
	PolicyIssues
}

PolicyValidateResponse is policies.validate's reply.

type RecentChecksInput

type RecentChecksInput struct {
	Limit int `json:"limit,omitempty"`
}

RecentChecksInput caps the list. Zero or negative means the default of 10.

type RecentChecksResponse

type RecentChecksResponse struct {
	Checks []CheckLogSummary `json:"checks"`
}

RecentChecksResponse is the overview.recentChecks reply.

type RelationCreateInput

type RelationCreateInput struct {
	NamespacePath   string `json:"namespacePath,omitempty"`
	ObjectType      string `json:"objectType"`
	ObjectID        string `json:"objectId"`
	Relation        string `json:"relation"`
	SubjectType     string `json:"subjectType"`
	SubjectID       string `json:"subjectId"`
	SubjectRelation string `json:"subjectRelation,omitempty"`
}

RelationCreateInput writes one tuple. Every part of the triple is required: a tuple missing any of them matches nothing and is reachable by no filter, so it is silent junk rather than a partial grant.

type RelationDefDTO

type RelationDefDTO struct {
	Name            string   `json:"name"`
	AllowedSubjects []string `json:"allowedSubjects"`
}

RelationDefDTO mirrors resourcetype.RelationDef on the wire.

type RelationDeleteInput

type RelationDeleteInput struct {
	ID string `json:"id"`
}

RelationDeleteInput names the tuple to remove, by id.

The store also offers DeleteRelationTuple, which takes the whole natural key. By-id is used here because the list hands the page an id, and an id cannot be half-right the way an eight-part key can.

type RelationExpandEdge

type RelationExpandEdge struct {
	From          string `json:"from"`
	To            string `json:"to"`
	NamespacePath string `json:"namespacePath"`
}

RelationExpandEdge is one tuple, between node keys.

type RelationExpandInput

type RelationExpandInput struct {
	ObjectType    string `json:"objectType"`
	ObjectID      string `json:"objectId"`
	Relation      string `json:"relation"`
	NamespacePath string `json:"namespacePath"` // "" is the root
	// PathToType and PathToID, when both set, ask for the walk's path to
	// that subject.
	PathToType string `json:"pathToType,omitempty"`
	PathToID   string `json:"pathToId,omitempty"`
}

RelationExpandInput names the object and relation to expand.

type RelationExpandNode

type RelationExpandNode struct {
	Key      string `json:"key"` // "type:id#relation" or "type:id"
	Type     string `json:"type"`
	ID       string `json:"id"`
	Relation string `json:"relation,omitempty"`
	Depth    int    `json:"depth"`
	// Walked is true when every one of the node's tuples is drawn: the walk
	// took the node off its queue and went through all of its tuples, and
	// none of its edges was left out by the node cap. It is false for a node
	// reached but never walked (a frontier node after a stop, the node that
	// tripped the fanout limit, every single subject) and for a walked node
	// with an edge to a node the cap dropped. Any edges drawn from a node
	// with walked false are not all of its tuples.
	Walked bool `json:"walked"`
	// Capped is true when the node cap removed any of this node's outgoing
	// edges, because the node at the other end was left out, and false
	// otherwise. It tells the two reasons for walked false apart: a node
	// that was never walked has capped false, and a node the walk did go
	// through, whose edges the cap then cut, has capped true. Together with
	// the node's drawn edges it says how much of the node is shown.
	Capped bool `json:"capped"`
}

RelationExpandNode is one node the walk reached.

type RelationExpandResponse

type RelationExpandResponse struct {
	Nodes []RelationExpandNode `json:"nodes"`
	Edges []RelationExpandEdge `json:"edges"`
	Stop  string               `json:"stop"` // complete | depth | visited | fanout
	// Limit is the limit that stopped the walk, as the walk ran with it: the
	// engine's own walker's when ExactWalk is true, Config's otherwise. 0
	// when the walk completed.
	Limit int `json:"limit"`
	// ExactWalk is false when the engine's graph walker is not the built-in
	// one: Check walks with that walker, so this expansion shows what the
	// built-in walk would find, which may differ.
	ExactWalk bool `json:"exactWalk"`
	// TruncatedNodes is how many nodes were left out past the node cap, 0
	// when none. Edges that touch a left-out node are left out too, and a
	// kept node that lost an edge that way reports walked false.
	TruncatedNodes int `json:"truncatedNodes"`
	// Path is the node keys of the walk's path to PathTo, root first, or
	// empty when not asked for or not reached.
	Path []string `json:"path"`
}

RelationExpandResponse is the expansion.

type RelationSummary

type RelationSummary struct {
	ID              string `json:"id"`
	NamespacePath   string `json:"namespacePath"`
	ObjectType      string `json:"objectType"`
	ObjectID        string `json:"objectId"`
	Relation        string `json:"relation"`
	SubjectType     string `json:"subjectType"`
	SubjectID       string `json:"subjectId"`
	SubjectRelation string `json:"subjectRelation,omitempty"`
	CreatedBy       string `json:"createdBy,omitempty"`
	CreatedAt       string `json:"createdAt"`
	// Undeclared is set when the resource type governing the tuple's object
	// type, from the tuple's own namespace, does not declare it: the reason
	// (resourcetype.UndeclaredTupleError.Reason) a write of this tuple would
	// be refused now. A tuple stored before the write check existed, by a
	// writer that skips it, or before its resource type changed can carry
	// one. It is absent when the tuple conforms, when no resource type
	// governs its object type, and on every row when the response says
	// MarksWithheld.
	//
	// It judges the tuple by the write check's rule and no other. A check
	// reads stored tuples as they are, without consulting the resource
	// type's relations or allowed subjects: the direct check and the graph
	// walker read no resource type, and the expression evaluator reads only
	// its permissions. So a marked tuple still counts.
	Undeclared string `json:"undeclared,omitempty"`
}

RelationSummary is one tuple.

type RelationsListInput

type RelationsListInput struct {
	PageRequest
	NamespacePath   *string `json:"namespacePath,omitempty"`
	ObjectType      string  `json:"objectType,omitempty"`
	ObjectID        string  `json:"objectId,omitempty"`
	Relation        string  `json:"relation,omitempty"`
	SubjectType     string  `json:"subjectType,omitempty"`
	SubjectID       string  `json:"subjectId,omitempty"`
	SubjectRelation string  `json:"subjectRelation,omitempty"`
}

RelationsListInput filters the tuple list. Every field the store's filter offers is here, because tracing a tuple means narrowing on whichever end you happen to know.

type RelationsListResponse

type RelationsListResponse struct {
	PageMeta
	Items []RelationSummary `json:"items"`
	// MarksWithheld is true when the caller may not read resource types
	// (read on warden:resourcetype), so no row was checked against its
	// resource type and none carries Undeclared. A mark's reason names the
	// type's relations or allowed subjects, which resourceTypes.* keeps
	// behind that grant, and this list must not be a way around it. An
	// unmarked row then says nothing about whether the tuple conforms.
	MarksWithheld bool `json:"marksWithheld"`
}

RelationsListResponse is the paged reply.

type ResourceTypeCreateInput

type ResourceTypeCreateInput struct {
	Name          string             `json:"name"`
	NamespacePath string             `json:"namespacePath,omitempty"`
	Description   string             `json:"description,omitempty"`
	Relations     []RelationDefDTO   `json:"relations,omitempty"`
	Permissions   []PermissionDefDTO `json:"permissions,omitempty"`
}

ResourceTypeCreateInput creates a resource type.

type ResourceTypeDeleteInput

type ResourceTypeDeleteInput struct {
	ID string `json:"id"`
}

ResourceTypeDeleteInput names the resource type to remove.

type ResourceTypeDetail

type ResourceTypeDetail struct {
	ResourceTypeSummary
	Relations   []RelationDefDTO   `json:"relations"`
	Permissions []PermissionDefDTO `json:"permissions"`
	CreatedBy   string             `json:"createdBy,omitempty"`
	UpdatedBy   string             `json:"updatedBy,omitempty"`
}

ResourceTypeDetail is one type with its full definitions. Both slices are always non-nil so the JSON carries [] rather than null: a page doing data.relations.length on null throws.

type ResourceTypeDetailInput

type ResourceTypeDetailInput struct {
	ID string `json:"id"`
}

ResourceTypeDetailInput names one resource type.

type ResourceTypeGraphEdge

type ResourceTypeGraphEdge struct {
	From string `json:"from"` // resource type name
	// FromID is the owning type's id. Two types can share a name in
	// different namespaces, and From alone cannot tell them apart.
	FromID   string `json:"fromId"`
	Relation string `json:"relation"` // the RelationDef's name
	To       string `json:"to"`       // allowed subject type
	// ToRelation is the subject set's relation for "type#rel", else "".
	ToRelation string `json:"toRelation,omitempty"`
	// Declared is false when To names no resource type in the graph (a
	// subject kind like "user", or a type outside the namespace filter).
	Declared bool `json:"declared"`
	// ToIDs is the id of every returned type whose name is To, in node
	// order. It is empty (never null) when the edge is undeclared.
	ToIDs []string `json:"toIds"`
}

ResourceTypeGraphEdge is one allowed subject of one relation.

type ResourceTypeGraphInput

type ResourceTypeGraphInput struct {
	NamespacePath *string `json:"namespacePath,omitempty"`
}

ResourceTypeGraphInput filters the schema graph.

type ResourceTypeGraphNode

type ResourceTypeGraphNode struct {
	ID            string             `json:"id"`
	NamespacePath string             `json:"namespacePath"`
	Name          string             `json:"name"`
	Relations     []RelationDefDTO   `json:"relations"`
	Permissions   []PermissionDefDTO `json:"permissions"`
}

ResourceTypeGraphNode is one resource type with its definitions.

type ResourceTypeGraphResponse

type ResourceTypeGraphResponse struct {
	Nodes     []ResourceTypeGraphNode `json:"nodes"`
	Edges     []ResourceTypeGraphEdge `json:"edges"`
	Truncated bool                    `json:"truncated"` // more than 500 types
}

ResourceTypeGraphResponse is the schema graph.

type ResourceTypeSummary

type ResourceTypeSummary struct {
	ID              string `json:"id"`
	NamespacePath   string `json:"namespacePath"`
	Name            string `json:"name"`
	Description     string `json:"description,omitempty"`
	RelationCount   int    `json:"relationCount"`
	PermissionCount int    `json:"permissionCount"`
	CreatedAt       string `json:"createdAt"`
	UpdatedAt       string `json:"updatedAt"`
}

ResourceTypeSummary is one row of the resource types list. It carries counts rather than the definitions themselves: a row showing every relation and expression of every type is a wall of text, and the detail page is where the definitions live.

type ResourceTypeUpdateInput

type ResourceTypeUpdateInput struct {
	ID          string              `json:"id"`
	Description *string             `json:"description,omitempty"`
	Relations   *[]RelationDefDTO   `json:"relations,omitempty"`
	Permissions *[]PermissionDefDTO `json:"permissions,omitempty"`
}

ResourceTypeUpdateInput patches a resource type.

Every field but the id is a pointer. nil means "leave it alone"; a pointer to an empty slice means "remove them all". Name and namespace are absent on purpose: tuples name a type by its name, so renaming one strands every tuple that used the old name.

type ResourceTypesListInput

type ResourceTypesListInput struct {
	PageRequest
	NamespacePath *string `json:"namespacePath,omitempty"`
	Search        string  `json:"search,omitempty"`
}

ResourceTypesListInput filters the resource types list.

type ResourceTypesListResponse

type ResourceTypesListResponse struct {
	PageMeta
	Items []ResourceTypeSummary `json:"items"`
}

ResourceTypesListResponse is the paged reply.

type RoleCreateInput

type RoleCreateInput struct {
	Name          string `json:"name"`
	Slug          string `json:"slug"`
	NamespacePath string `json:"namespacePath,omitempty"`
	Description   string `json:"description,omitempty"`
	ParentSlug    string `json:"parentSlug,omitempty"`
	MaxMembers    int    `json:"maxMembers,omitempty"`
	IsDefault     bool   `json:"isDefault,omitempty"`
}

RoleCreateInput creates a role. Non-pointer fields because a create has no "leave this alone": every field is either given or defaulted.

type RoleDeleteInput

type RoleDeleteInput struct {
	ID string `json:"id"`
}

RoleDeleteInput names the role to remove.

type RoleDetail

type RoleDetail struct {
	RoleSummary
	Permissions []PermissionSummary `json:"permissions"`
	Children    []RoleSummary       `json:"children"`
	CreatedBy   string              `json:"createdBy,omitempty"`
	UpdatedBy   string              `json:"updatedBy,omitempty"`
}

RoleDetail is one role with everything its page shows.

type RoleDetailInput

type RoleDetailInput struct {
	ID string `json:"id"`
}

RoleDetailInput names one role.

type RolePermissionInput

type RolePermissionInput struct {
	RoleID                  string `json:"roleId"`
	PermissionName          string `json:"permissionName"`
	PermissionNamespacePath string `json:"permissionNamespacePath,omitempty"`
}

RolePermissionInput attaches or detaches one grant.

type RoleSetPermissionsInput

type RoleSetPermissionsInput struct {
	RoleID      string          `json:"roleId"`
	Permissions []PermissionRef `json:"permissions"`
}

RoleSetPermissionsInput replaces a role's whole grant set.

An empty Permissions slice is a real instruction: it revokes everything. That is why the field is not a pointer; there is no "leave the set alone" case for an intent whose only job is to replace it.

type RoleSummary

type RoleSummary struct {
	ID            string `json:"id"`
	NamespacePath string `json:"namespacePath"`
	Name          string `json:"name"`
	Slug          string `json:"slug"`
	Description   string `json:"description,omitempty"`
	ParentSlug    string `json:"parentSlug,omitempty"`
	IsSystem      bool   `json:"isSystem"`
	IsDefault     bool   `json:"isDefault"`
	MaxMembers    int    `json:"maxMembers,omitempty"`
	CreatedAt     string `json:"createdAt"`
	UpdatedAt     string `json:"updatedAt"`
}

RoleSummary is one row of the roles list.

type RoleUpdateInput

type RoleUpdateInput struct {
	ID          string  `json:"id"`
	Name        *string `json:"name,omitempty"`
	Description *string `json:"description,omitempty"`
	ParentSlug  *string `json:"parentSlug,omitempty"`
	MaxMembers  *int    `json:"maxMembers,omitempty"`
	IsDefault   *bool   `json:"isDefault,omitempty"`
}

RoleUpdateInput patches a role.

Every optional field is a pointer, and the distinction is load-bearing: nil means "leave this alone", and a pointer to the zero value means "set it to empty". A non-pointer field cannot express the difference, and the UI would silently erase values the operator never touched.

type RolesListInput

type RolesListInput struct {
	PageRequest
	NamespacePath *string `json:"namespacePath,omitempty"`
	Search        string  `json:"search,omitempty"`
	IsSystem      *bool   `json:"isSystem,omitempty"`
	IsDefault     *bool   `json:"isDefault,omitempty"`
}

RolesListInput filters the roles list.

NamespacePath is a pointer because nil and the empty string are different queries: nil means every namespace, "" means the tenant root.

type RolesListResponse

type RolesListResponse struct {
	PageMeta
	Items []RoleSummary `json:"items"`
}

RolesListResponse is the paged reply.

type SchemaApplyInput

type SchemaApplyInput struct {
	Source string `json:"source"`
	Prune  bool   `json:"prune"`
	// Digest is the plan digest the operator saw.
	Digest string `json:"digest"`
}

SchemaApplyInput is the schema.apply request.

type SchemaApplyResponse

type SchemaApplyResponse struct {
	Created []string `json:"created"`
	Updated []string `json:"updated"`
	Deleted []string `json:"deleted"`
	NoOps   int      `json:"noOps"`
	// Diverged is true when what was written differs from the plan the
	// digest vouched for. The digest is checked before the write and the
	// write is a second read of the store, so a change in between can make
	// the two differ. The lists are what was actually written.
	Diverged bool `json:"diverged"`
}

SchemaApplyResponse is the schema.apply reply: what was written.

type SchemaDiagnostic

type SchemaDiagnostic struct {
	Line    int    `json:"line"` // 1-based
	Col     int    `json:"col"`  // 1-based byte column, as dsl.Pos
	Message string `json:"message"`
}

SchemaDiagnostic is one problem with submitted source.

type SchemaExportInput

type SchemaExportInput struct {
	// NamespacePrefix limits the export to that namespace and below. "" is
	// every namespace.
	NamespacePrefix string `json:"namespacePrefix,omitempty"`
}

SchemaExportInput is the schema.export request.

type SchemaExportResponse

type SchemaExportResponse struct {
	Source string `json:"source"`
}

SchemaExportResponse is the schema.export reply.

type SchemaPlanInput

type SchemaPlanInput struct {
	Source string `json:"source"`
	Prune  bool   `json:"prune"`
}

SchemaPlanInput is the schema.plan request.

type SchemaPlanResponse

type SchemaPlanResponse struct {
	// Valid is false when Diagnostics is non-empty; then the lists are empty
	// and Digest is "".
	Valid       bool               `json:"valid"`
	Diagnostics []SchemaDiagnostic `json:"diagnostics"`
	Created     []string           `json:"created"`
	Updated     []string           `json:"updated"`
	Deleted     []string           `json:"deleted"`
	NoOps       int                `json:"noOps"`
	// Digest identifies this exact diff and prune flag; schema.apply
	// requires it.
	Digest string `json:"digest"`
}

SchemaPlanResponse is the schema.plan reply.

type SubjectAssignment

type SubjectAssignment struct {
	ID            string `json:"id"`
	NamespacePath string `json:"namespacePath"`
	RoleID        string `json:"roleId"`
	RoleSlug      string `json:"roleSlug"`
	ResourceType  string `json:"resourceType,omitempty"`
	ResourceID    string `json:"resourceId,omitempty"`
	ExpiresAt     string `json:"expiresAt,omitempty"`
	Expired       bool   `json:"expired"`
	// ExpiringSoon is true for a live assignment whose ExpiresAt falls within
	// the horizon assignments.expiring uses. That feed also lists lapsed
	// rows; this flag does not, because Expired already says so.
	ExpiringSoon bool `json:"expiringSoon"`
}

SubjectAssignment is one assignment row of the subject.

type SubjectDetailInput

type SubjectDetailInput struct {
	SubjectKind   string `json:"subjectKind"`
	SubjectID     string `json:"subjectId"`
	NamespacePath string `json:"namespacePath"` // "" is the root; always explicit
}

SubjectDetailInput names one subject at one namespace.

SubjectKind is not validated: the check log and the stores accept any string, and a subject of a kind no assignment names is a legitimate question with an empty answer.

type SubjectDetailResponse

type SubjectDetailResponse struct {
	Roles                []SubjectRole       `json:"roles"`
	Assignments          []SubjectAssignment `json:"assignments"`
	AssignmentsTruncated bool                `json:"assignmentsTruncated"`
	Relations            []SubjectRelation   `json:"relations"`
	RelationsTruncated   bool                `json:"relationsTruncated"`
	Policies             []SubjectPolicy     `json:"policies"`
	// Withheld names the sections left empty because the caller lacks their
	// read grant. Always an array, empty when nothing is withheld.
	Withheld []string `json:"withheld"`
}

SubjectDetailResponse is the subjects.detail reply.

The intent's own grant (read on warden:assignment) covers Assignments. The other sections need their own read grant, checked in the handler: Roles needs warden:role, Relations warden:relation and Policies warden:policy. A section the caller may not read is an empty array, its truncated flag is false, and its name ("roles", "relations" or "policies") is in Withheld. The check log is not here: it needs read_audit, and the page reads it through checkLogs.list.

type SubjectPermission

type SubjectPermission struct {
	Name     string `json:"name"` // resource:action as stored, wildcards included
	Resource string `json:"resource"`
	Action   string `json:"action"`
}

SubjectPermission is one grant of a role.

type SubjectPolicy

type SubjectPolicy struct {
	ID            string `json:"id"`
	Name          string `json:"name"`
	Effect        string `json:"effect"`
	Priority      int    `json:"priority"`
	NamespacePath string `json:"namespacePath"`
	// SelectedBy says how the policy's subject matchers select this subject:
	// "everyone" (no subject matchers, or an empty matcher), "kind", "id" or
	// "role:<slug>" (the first agreeing matcher's most specific field).
	SelectedBy string `json:"selectedBy"`
}

SubjectPolicy is one policy in effect that selects the subject.

type SubjectRelation

type SubjectRelation struct {
	ID            string `json:"id"`
	NamespacePath string `json:"namespacePath"`
	ObjectType    string `json:"objectType"`
	ObjectID      string `json:"objectId"`
	Relation      string `json:"relation"`
	// SubjectRelation is the tuple's subject relation, set when the tuple
	// grants a userset (group:eng#member) rather than the subject itself.
	SubjectRelation string `json:"subjectRelation,omitempty"`
}

SubjectRelation is one tuple naming the subject.

type SubjectRole

type SubjectRole struct {
	ID            string `json:"id"`
	Slug          string `json:"slug"`
	Name          string `json:"name"`
	NamespacePath string `json:"namespacePath"`
	// Via is "assigned" or "inherited".
	Via string `json:"via"`
	// InheritedBy lists the slugs of the resolved roles whose ParentSlug
	// names this role in its namespace. Empty for an assigned role that no
	// other resolved role inherits.
	InheritedBy []string            `json:"inheritedBy"`
	Permissions []SubjectPermission `json:"permissions"`
}

SubjectRole is one resolved role.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL