identity

package
v0.2.0 Latest Latest
Warning

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

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

Documentation

Overview

Package identity defines the user profile shared by authentication, the identity API (backyard.id), presence and audit logging.

Index

Constants

View Source
const (
	DefaultEmailClaim  = "email"
	DefaultNameClaim   = "name"
	DefaultTitleClaim  = "title"
	DefaultTeamClaim   = "department"
	DefaultSlackClaim  = "slack"
	DefaultAvatarClaim = "picture"
	DefaultGroupsClaim = "groups"
)

Default claim names, used for fields left empty in a config.ClaimsMapping.

View Source
const (
	// SystemSite is the reserved docstore namespace for system data.
	SystemSite = "_system"
	// UsersCollection holds one document per user, keyed by lowercased email.
	UsersCollection = "_users"
)

Location of the user cache in the document store (SPEC §10).

View Source
const (
	// TouchInterval is the minimum time between two cache writes for the same
	// user on one instance.
	TouchInterval = 10 * time.Minute
)

Variables

This section is empty.

Functions

func EmailDomain

func EmailDomain(email string) string

EmailDomain returns the lowercased domain part of an e-mail address, or "".

func NormalizeEmail

func NormalizeEmail(s string) string

NormalizeEmail trims and lowercases s and returns it when it is a plain e-mail address (local@domain, no display name, no spaces); otherwise it returns "".

func ParseDirectoryCSV

func ParseDirectoryCSV(r io.Reader) (map[string]*User, error)

ParseDirectoryCSV parses a CSV people directory (see NewDirectory) into a map keyed by lowercased email. Rows without a valid email are skipped; a later row for the same email replaces an earlier one.

func ParseDirectoryJSON

func ParseDirectoryJSON(r io.Reader) (map[string]*User, error)

ParseDirectoryJSON parses a JSON people directory: an array of user objects or an object with a "users" array (see NewDirectory).

func ParseList

func ParseList(v any) []string

ParseList converts a claim value into a list of strings: []string, []any of scalars, or a comma-separated string. Empty and (case-insensitively) duplicate entries are dropped; the order of first occurrence is preserved.

func WithUser

func WithUser(ctx context.Context, u *User) context.Context

WithUser returns a context carrying u.

Types

type Directory

type Directory interface {
	// Lookup returns the profile for an email, or (nil, nil) if unknown.
	Lookup(ctx context.Context, email string) (*User, error)
}

Directory looks up people profiles (static file, user cache, …).

func NewDirectory

func NewDirectory(cfg config.DirectoryConfig) (Directory, error)

NewDirectory loads the configured static directory (nil, nil when type is "").

Type "csv" expects a header row containing an "email" column; recognised columns are name, title, team (alias department), slack, avatar_url (aliases avatar, picture) and groups (';'-separated). Any other non-empty column is copied into User.Extra. Type "json" expects an array of user objects (or {"users": [...]}) with the same field names; groups may be an array or a ';'/','-separated string, and an "extra" object as well as any unknown keys end up in User.Extra.

Lookups are case-insensitive on the email. When cfg.Reload is positive the file is re-checked (modification time and size) at most once per interval, on lookup, and re-read in the background when it changed; a failed reload keeps the previous contents.

type Summary

type Summary struct {
	ID        string `json:"id"`
	Name      string `json:"name"`
	Email     string `json:"email,omitempty"`
	AvatarURL string `json:"avatar_url,omitempty"`
}

Summary is the compact form embedded in realtime presence and messages.

type User

type User struct {
	// ID is stable: lowercased email when known, else "sub:<issuer-sub>",
	// "token:<name>" for static tokens, or "anon:<16 hex>" when anonymous.
	ID        string         `json:"id"`
	Email     string         `json:"email,omitempty"`
	Name      string         `json:"name"`
	Title     string         `json:"title,omitempty"`
	Team      string         `json:"team,omitempty"`
	Slack     string         `json:"slack,omitempty"`
	AvatarURL string         `json:"avatar_url,omitempty"`
	Groups    []string       `json:"groups,omitempty"`
	Anonymous bool           `json:"anonymous"`
	Admin     bool           `json:"admin"`
	Extra     map[string]any `json:"extra,omitempty"`
}

User is the authenticated (or anonymous) principal of a request. Its JSON shape is part of the public client API (see docs/spec/SPEC.md §4).

func FromContext

func FromContext(ctx context.Context) *User

FromContext returns the user stored by WithUser, or nil.

func MapClaims

func MapClaims(claims map[string]any, mapping config.ClaimsMapping, extra []string) *User

MapClaims builds a User from IdP claims according to mapping. The id is the lowercased email when present, else "sub:"+sub. Admin is NOT set here.

Claim names may address nested objects with dots ("custom.team") when no top-level claim of that exact name exists. The groups claim may be a JSON array or a comma-separated string. The name falls back to given_name + family_name, then to the local part of the email, then to preferred_username and finally to sub. Claims listed in extra are copied verbatim into User.Extra. When neither a valid email nor a sub is present the returned user has an empty ID, which callers must reject.

When the email is read from the standard "email" claim and the claims carry "email_verified": false, the email is ignored (the user is identified by sub): an unverified address must not satisfy allowed_domains or match an admin entry.

func (*User) Actor

func (u *User) Actor() string

Actor returns the identifier recorded in created_by/updated_by fields.

func (*User) Clone

func (u *User) Clone() *User

Clone returns a deep copy of u (nil for nil). Groups and Extra are copied so the clone can be modified without affecting u.

func (*User) Enrich

func (u *User) Enrich(p *User)

Enrich overlays a profile p (typically a directory entry for the same email) onto u: non-empty Name, Title, Team, Slack and AvatarURL of p replace u's values, groups are merged (union, case-insensitive, u's order first) and Extra keys of p override u's. ID, Email, Anonymous and Admin are never changed. A nil u or p is a no-op.

A name of p that is merely the local part of p's email (the fallback used for directory entries without a name) does not replace a name u already has, so a sparse directory never downgrades "Alice Smith" to "alice".

func (*User) InGroup

func (u *User) InGroup(g string) bool

InGroup reports whether the user belongs to group g (case-insensitive).

func (*User) Summary

func (u *User) Summary() Summary

Summary returns the compact representation of u.

type UserCache

type UserCache struct {
	// contains filtered or unexported fields
}

UserCache stores profiles of users who have used Backyard in the docstore (site "_system", collection "_users", doc id = lowercased email) so that GET /users/{email} can resolve colleagues. Writes are throttled per user.

The cache never stores anonymous users, static-token principals or users without an email. Admin flags are not persisted (they are derived from the configuration at request time). A UserCache is safe for concurrent use.

func NewUserCache

func NewUserCache(store docstore.Store, fallback Directory) *UserCache

NewUserCache returns a cache backed by store. fallback (optional) is consulted first (e.g. the static directory).

func (*UserCache) Lookup

func (c *UserCache) Lookup(ctx context.Context, email string) (*User, error)

Lookup implements Directory.

The fallback directory is consulted first; the cached profile (if any) is then enriched with the directory entry, so directory data wins over data last seen from the IdP. It returns (nil, nil) for unknown emails.

func (*UserCache) Touch

func (c *UserCache) Touch(ctx context.Context, u *User)

Touch records u (no-op for anonymous users or users without email; throttled).

The write happens synchronously (bounded by a short timeout and detached from ctx cancellation) at most once per TouchInterval per user and instance; failures are logged and retried after a minute.

Directories

Path Synopsis
Package identitytest holds integration tests that run the identity user cache against the real docstore backends (memory, SQLite and, when the BACKYARD_TEST_* services are available, PostgreSQL and DynamoDB Local).
Package identitytest holds integration tests that run the identity user cache against the real docstore backends (memory, SQLite and, when the BACKYARD_TEST_* services are available, PostgreSQL and DynamoDB Local).

Jump to

Keyboard shortcuts

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