directory

package
v0.7.13 Latest Latest
Warning

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

Go to latest
Published: Sep 28, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package directory turns what an admin TYPES into what Wardyn STORES.

Every "who" field in the product — a governance assignment's subject, the SSO People step's mapping value — is a claim value, and the claim value is rarely the thing a human knows. An Entra group's `groups` claim carries the group's OBJECT GUID, not its name; an App Role arrives as its manifest `value`, not its display name. Hand-typing those is a silent-mismatch landmine: a plausible-looking wrong string binds nothing and fails open to whatever the unassigned default is.

So: Entry.ClaimValue IS the contract. It is the exact string the claim will carry — email for a user, object GUID for a group, manifest value for an App Role — and it is what a UI inserts on selection. Entry.DisplayName is what a UI RENDERS. The two are deliberately different fields because for groups they are deliberately different strings.

Provider-abstracted on purpose (PF-29): one interface, Entra ships first, Okta/Google are later connectors behind the same contract. The whole capability is opt-in — unconfigured, there is no Directory at all and every field stays free text, exactly as it behaves today.

Index

Constants

View Source
const (
	// MaxResults is ONE GLOBAL cap across every kind in a single search — not a
	// per-kind quota. In KindAny the three per-kind result sets are concatenated
	// in anyKindOrder and the first MaxResults survive, so the outcome is
	// deterministic and explainable ("App Roles first, then groups, then users,
	// twenty in total") instead of an interleaving whose shape depends on how
	// many hits each kind happened to return.
	//
	// Every search re-trims to this cap on the way out, single-kind ones
	// included: $top is a request to the upstream, not a guarantee from it.
	MaxResults = 20

	// MinQueryLen is the shortest query a connector will send upstream. Below it
	// a search returns empty WITHOUT a round trip: one or two characters match
	// most of a directory, so the request costs a Graph call to produce noise.
	// The HTTP layer enforces the same floor as a request contract; this is the
	// connector-side backstop so the package is safe called directly.
	MinQueryLen = 2
)

Variables

View Source
var ErrUnconfigured = errors.New("directory: no provider configured")

ErrUnconfigured means no directory provider is configured — the credentials were absent at construction, or there is no Directory at all. The HTTP layer answers it with a distinct 503 code, which is the UI's ABSENT-MODE signal: the combobox degrades to a plain free-text input with no error surface.

It is deliberately a sentinel and not a ProviderError: nothing upstream failed, the feature is simply off.

Functions

This section is empty.

Types

type Directory

type Directory interface {
	// Search returns at most MaxResults entries matching q, in the fixed
	// anyKindOrder when kind is KindAny. A query shorter than MinQueryLen
	// returns (nil, nil) — empty is not an error.
	Search(ctx context.Context, q string, kind Kind) ([]Entry, error)
}

Directory searches a configured identity provider. Implementations are expected to be safe for concurrent use.

func NewEntra

func NewEntra(cfg EntraConfig) (Directory, error)

NewEntra builds the Microsoft Graph connector. It returns ErrUnconfigured when any credential is absent — that is the "feature is off" answer, not a failure, and the caller wires no Directory at all in that case.

It performs no network I/O: a tenant that is reachable at boot but not at first search would fail either way, so there is nothing to gain by refusing startup over it (unlike the credential SHAPE check, which boot does make).

type EntraConfig

type EntraConfig struct {
	TenantID     string
	ClientID     string
	ClientSecret string
}

EntraConfig carries the client-credentials app registration. This package reads NO environment: the boot slice resolves the credentials (default = the OIDC app registration, override = a dedicated least-privilege app) and passes them in, so the fail-closed boot refusal for a PUBLIC OIDC client — which has no secret and therefore cannot do client credentials at all — lives at boot where it can actually refuse startup, not here where it could only 503.

type Entry

type Entry struct {
	// DisplayName is the human-readable label — a user's display name, a
	// group's name, an App Role's display name. Never store this.
	DisplayName string `json:"display_name"`
	// ClaimValue is the exact string the claim carries and the field must hold:
	// email (user), object GUID (group), manifest value (App Role). THIS is
	// what a UI inserts when a row is picked.
	ClaimValue string `json:"claim_value"`
	// Kind is which of the three sources produced this entry.
	Kind Kind `json:"kind"`
	// Detail is a secondary disambiguator for the row ("group · 8f3c1a2b",
	// a user's other address). Presentation only — never the stored value.
	Detail string `json:"detail,omitempty"`
}

Entry is one suggestion. DisplayName is rendered; ClaimValue is stored.

type Kind

type Kind string

Kind selects which class of directory object a search covers. KindAny is IN the v1 contract, not a convenience: the People-step Value field is one kind-LESS input that accepts an App Role, a group, or an email, so a one-kind-per-request interface would force a rework at the swap.

const (
	KindUser    Kind = "user"
	KindGroup   Kind = "group"
	KindAppRole Kind = "approle"
	KindAny     Kind = "any"
)

func (Kind) Valid

func (k Kind) Valid() bool

Valid reports whether k is one of the four contract kinds.

type ProviderError

type ProviderError struct {
	Provider string // "entra"
	Op       string
	Status   int
	Err      error
}

ProviderError wraps a failure talking to the upstream directory. Op names the call ("token", "users", "groups", "approles") and Status carries the upstream HTTP status when there was one (0 otherwise), so the HTTP layer can decide what to tell the admin without re-parsing an error string.

func (*ProviderError) Error

func (e *ProviderError) Error() string

func (*ProviderError) Unwrap

func (e *ProviderError) Unwrap() error

Jump to

Keyboard shortcuts

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