adcore

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 22, 2026 License: MIT Imports: 11 Imported by: 0

README

go-adcore

The Active Directory vocabulary shared by every backend: the models a directory read returns, the specs a write takes, identities, queries, the normalized error Kind, and the invariant machinery every backend composes rather than reimplements.

It performs no I/O, imports nothing outside the standard library, and knows nothing about PowerShell, LDAP or Terraform.

Why it exists

go-adpwsh drives Active Directory through PowerShell. A second backend drives it over LDAP. Both must return the same models, take the same specs, and classify the same condition as the same Kind — otherwise a Terraform provider switching between them is switching between two subtly different directories.

That agreement is this module. go-adpwsh re-exports all of it as type aliases, so adpwsh.OU and adcore.OU are one type, not two convertible ones.

What is here

File Responsibility
models.go OU, Group, User, GMSA, Computer, Member, OptTime, pointer helpers
identity.go the sealed Identity interface and its four constructors
secret.go Secret, RevealSecret
errors.go Kind, Error, the sentinels, the MS-ERREF code table
spec.go the *Spec types, their validators, PresenceCheck, WithIdentity
search.go Query, SearchScope
acl.go ACE/ACL types, CanonicalACEKey
delegation.go Delegation — pure expansion of a curated task into ACEs
dn.go, filter.go RFC 4514 DNs and RFC 4515 filter escaping
directory.go the Directory struct of interfaces
locks.go, retry.go KeyedMutex, RetryConfig, Backoff
schema/ the schema catalog and its reader
adcoretest/ RunDirectorySuite — the behavioural conformance suite

Three rules this module keeps

No third-party dependencies. Standard library only, enforced by TestNoThirdPartyDependencies. This is the shared vocabulary for two backends and a Terraform provider; a dependency here is a dependency everywhere.

Identity stays sealed across a module boundary. The interface's methods are unexported, so the only values satisfying it come from ByGUID, ByDN, BySID and BySAM. Backends read the parts through the IdentityArg and IdentityForm functions — exporting the methods instead would let any package hand a backend an arbitrary string as an identity, which is what keeps a caller's value from becoming PowerShell script text or an unescaped LDAP filter term.

A Secret never reaches a log line. It renders REDACTED under every fmt verb — via fmt.Formatter, not merely Stringer, because %d on a struct walks the fields — and its MarshalJSON always fails. The plaintext is reachable only through RevealSecret, a function rather than a method so that every site extracting a password is greppable by name.

Conformance

The guarantees a consumer relies on — read-back after write, delete verification, a pinned domain controller, serialized writes per identity, a search that errors rather than truncating, a rename that never replaces the object — were once enforced structurally, by a single implementation no backend could opt out of. With backends in separate modules that is no longer possible, so they are behavioural assertions:

func TestMyBackendConformance(t *testing.T) {
	adcoretest.RunDirectorySuite(t, func(t *testing.T) adcore.Directory {
		return newMyBackend(t).Directory()
	})
}

A backend that does not run RunDirectorySuite is not a conforming implementation.

Attribution

dn.go and filter.go are a reimplementation of the slice of RFC 4514 and RFC 4515 this module needs, rather than a dependency on go-ldap — which would pull Kerberos, NTLM, SSPI and BER into a module that performs no I/O, and which the no-third-party-imports rule forbids outright. The test vectors are ported from that project (MIT licence), so the reimplementation stays pinned to a maintained parser's behaviour.

Licence

MIT. See LICENSE.

Documentation

Overview

Package adcore is the Active Directory vocabulary shared by every backend: the models a directory read returns, the specs a write takes, identities, queries, the normalized error Kind, and the invariant machinery (per-identity write locking, retry, delete verification) that every backend composes rather than reimplements.

It performs no I/O, imports nothing outside the standard library, and knows nothing about PowerShell, LDAP or Terraform.

Index

Constants

View Source
const DefaultSizeLimit = 1000

DefaultSizeLimit caps a search unless the caller sets its own limit. Large enough for a realistic subtree, small enough that a domain-wide accident errors rather than dragging.

Variables

View Source
var (
	ErrNotFound         error = kindSentinel{KindNotFound}
	ErrAlreadyExists    error = kindSentinel{KindAlreadyExists}
	ErrDenied           error = kindSentinel{KindDenied}
	ErrConstraint       error = kindSentinel{KindConstraint}
	ErrPassword         error = kindSentinel{KindPassword}
	ErrReferral         error = kindSentinel{KindReferral}
	ErrTransient        error = kindSentinel{KindTransient}
	ErrTransport        error = kindSentinel{KindTransport}
	ErrInvalidAttribute error = kindSentinel{KindInvalidAttribute}
	ErrSchema           error = kindSentinel{KindSchema}
	ErrReplication      error = kindSentinel{KindReplication}
	ErrTooManyResults   error = kindSentinel{KindTooManyResults}
	ErrUnsupported      error = kindSentinel{KindUnsupported}
)

Kind sentinels for errors.Is.

Functions

func And

func And(terms ...string) string

And composes a conjunction. Zero terms is the empty filter (the caller decides the default); one term is returned unwrapped; two or more are joined under a single "&".

func Backoff

func Backoff(ctx context.Context, cfg RetryConfig, attempt int) error

Backoff sleeps for the attempt's exponential delay, jittered, and honours cancellation. attempt is 1-based.

A cancelled context yields KindTransport, never KindTransient: the caller gave up, and a Kind that invites a re-issue would turn that into one.

func Bool

func Bool(b bool) *bool

Bool is the pointer helper for optional booleans.

func CanonicalACEKey

func CanonicalACEKey(a ACE) string

CanonicalACEKey is the semantic identity of an ACE: case-insensitive, and order-insensitive over Rights, so two representations of the same grant match. It is what drift detection and revoke-by-identity compare on. Fields join on "\x1f" (US) and rights join on "\x1e" (RS) — control characters that can never appear in a SID, GUID, AD rights name, or enum value — so no field's content can ever be mistaken for a separator.

func ContainerOf

func ContainerOf(dn string) (string, error)

ContainerOf returns the parent DN. It is derived rather than read back, because no directory read returns the parent as its own attribute.

func Equal

func Equal(attr, value string) string

Equal builds an equality assertion "(attr=<escaped value>)". The attribute name is a caller-controlled schema identifier and is not escaped; only the value is.

func EqualFoldDN

func EqualFoldDN(a, b string) (bool, error)

EqualFoldDN is the string convenience used wherever a spec value meets a value AD echoed back. It is not named EqualFold because at a call site that reads as a string comparison, and comparing two DNs as strings is wrong.

func EscapeFilter

func EscapeFilter(filter string) string

EscapeFilter escapes the RFC 4515 special set `()*\` and every byte outside 0 < c < 0x80 in an assertion value. Every value a backend puts inside a filter goes through here; hand-rolled quoting is the defect class the design exists to retire.

func EscapeValue

func EscapeValue(v string) string

EscapeValue is the exported spelling, for a caller that assembles an RDN instead of rendering a parsed DN. Building "OU=" + name + "," + parent by concatenation is correct right up to the first name containing a comma, at which point the name silently reparents the object.

func IdentityArg

func IdentityArg(id Identity) string

IdentityArg returns the identity's value.

func IdentityForm

func IdentityForm(id Identity) string

IdentityForm returns the identity's form: "guid", "dn", "sid" or "sam".

func Int

func Int(i int) *int

Int is the pointer helper for optional integers.

func Parent

func Parent(dn string) (string, error)

Parent is the string convenience: it returns the container of dn, or "" if dn has no parent.

func RevealSecret

func RevealSecret(s Secret) string

RevealSecret returns the plaintext. It is a function rather than a method so that every site that extracts a password is greppable by name — which is the property Secret exists to provide.

func SplitDN

func SplitDN(dn string) ([]string, error)

SplitDN returns the DN's components, most specific first, each in its escaped RFC 4514 form. It parses rather than splitting on commas: an escaped comma is part of a component, not a separator, so a lexical split turns "OU=Sales\, EMEA,DC=corp" into three components and reparents the object.

func String

func String(s string) *string

String is the pointer helper for the tri-state spec fields. Named for how it reads at the call site: Description: adcore.String("x").

func ValidateContainer

func ValidateContainer(op, container string) error

ValidateContainer rejects a container that is empty or is not a distinguished name, before any round trip.

func ValidateName

func ValidateName(op, name string) error

ValidateName rejects an empty name before any round trip.

func WithIdentity

func WithIdentity(err error, op string, id Identity) error

WithIdentity stamps the identity onto an error a backend returned, so a diagnostic can name what was acted on.

Types

type ACE

type ACE struct {
	Trustee             string // SID
	Type                ACEType
	Rights              []Right
	ObjectType          string // GUID; "" = all
	InheritedObjectType string // GUID; "" = all child classes
	Inheritance         Inheritance
	Inherited           bool // read-only: system-stamped copy; never managed
}

ACE is one explicit access-control entry as the library reads and writes it. Object types are GUIDs at this layer (already resolved from friendly names).

type ACESpec

type ACESpec struct {
	Rights      []Right
	ObjectType  string
	Scope       Inheritance
	ObjectClass string
	Type        ACEType
}

ACESpec is the friendly, unresolved form a delegation template emits and the provider maps to config. ObjectType and ObjectClass are friendly names or GUIDs.

type ACEType

type ACEType string

ACEType is an access-control entry's allow/deny sense.

const (
	ACEAllow ACEType = "Allow"
	ACEDeny  ACEType = "Deny"
)

type ACLDirectory

type ACLDirectory interface {
	Get(ctx context.Context, id Identity) ([]ACE, error)
	Grant(ctx context.Context, id Identity, aces []ACE) error
	Revoke(ctx context.Context, id Identity, aces []ACE) error
}

ACL takes resolved ACEs, not the friendly ACESpec a delegation template emits: resolving a name to a schema GUID is a directory read, so it belongs to the caller that already holds a Schema, not to the write.

type AttributeTypeAndValue

type AttributeTypeAndValue struct {
	Type  string
	Value string
}

AttributeTypeAndValue is one type=value pair inside an RDN.

type Computer

type Computer struct {
	GUID                       string
	DN                         string
	Name                       string
	SamAccountName             string
	Container                  string
	SID                        string
	Enabled                    bool
	DNSHostName                string
	Description                string
	DisplayName                string
	Location                   string
	ManagedBy                  string // read back as a DN
	TrustedForDelegation       bool
	ServicePrincipalNames      []string
	AllowedToDelegateTo        []string // msDS-AllowedToDelegateTo (SPNs)
	PrincipalsAllowed          []string // RBCD principals, as objectGUIDs
	KerberosEncryptionType     []string
	AccountExpiration          *time.Time
	OperatingSystem            string
	OperatingSystemVersion     string
	OperatingSystemServicePack string
}

Computer is an Active Directory computer account (objectClass "computer"). OperatingSystem* are read-only: the joined machine owns them.

type ComputerDirectory

type ComputerDirectory interface {
	Create(ctx context.Context, spec ComputerSpec) (*Computer, error)
	Get(ctx context.Context, id Identity) (*Computer, error)
	Search(ctx context.Context, q Query) ([]Computer, error)
	Update(ctx context.Context, id Identity, spec ComputerSpec) (*Computer, error)
	Delete(ctx context.Context, id Identity) error
}

type ComputerSpec

type ComputerSpec struct {
	Name                   string // CN; required
	SamAccountName         string // required; "$" is added by AD; length is NOT capped here
	Container              string // parent DN; required
	DNSHostName            *string
	Description            *string
	DisplayName            *string
	Location               *string
	ManagedBy              *string
	Enabled                *bool
	TrustedForDelegation   *bool
	ServicePrincipalNames  *[]string  // nil leaves alone, non-nil (incl. empty) replaces
	AllowedToDelegateTo    *[]string  // nil leaves alone, non-nil (incl. empty) replaces
	PrincipalsAllowed      []Identity // full-replace; nil leaves alone, non-nil (incl. empty) replaces
	KerberosEncryptionType *[]string  // nil leaves alone, non-nil replaces
	AccountExpiration      OptTime
}

ComputerSpec is the desired state of a computer account. Pointer fields follow the same tri-state convention as GMSASpec: nil leaves the attribute alone, a pointer to "" clears it, a pointer to a value sets it.

Unlike GMSASpec, SamAccountName has no length cap here: the 15-char NetBIOS limit gMSA enforces is a gMSA-specific constraint (the "$" AD appends must still fit in 20 bytes downlevel-logon-name space), not a general AD rule — AD accepts a computer sAMAccountName well past 15 characters, so validate must not reject one. There are also no create-only fields: DNSHostName, unlike a gMSA's, is settable any time.

func (ComputerSpec) Validate

func (s ComputerSpec) Validate(op string, forCreate bool) error

type DN

type DN struct {
	RDNs []RelativeDN
}

DN is a parsed distinguished name, most specific RDN first.

func ParseDN

func ParseDN(str string) (*DN, error)

ParseDN parses the string form of a distinguished name. Hex-encoded BER attribute values (the "#04024869" form) are rejected rather than decoded: Active Directory never emits them, and decoding would require an ASN.1 parser for no gain.

func (*DN) AncestorOfFold

func (d *DN) AncestorOfFold(other *DN) bool

AncestorOfFold reports whether d is a strict ancestor of other.

func (*DN) EqualFold

func (d *DN) EqualFold(other *DN) bool

EqualFold reports whether two DNs name the same object, comparing attribute types and values case-insensitively. AD's DN syntax is case-insensitive and case-preserving, so this is the only correct comparison.

func (*DN) Parent

func (d *DN) Parent() *DN

Parent returns the DN with its first RDN removed, or nil at the root.

func (*DN) String

func (d *DN) String() string

String renders the DN in RFC 4514 form, escaping the characters that would otherwise change its structure.

type Delegation

type Delegation struct{}

Delegation expands a curated delegation task into the concrete ACEs that implement it. It performs no directory I/O: the expansion is pure, so a consumer can compute a plan from it without a round trip. Object types are friendly names; the caller resolves them to GUIDs.

It is named Delegation rather than DelegationClient because it is a stateless struct that never reaches a directory; the Client suffix misled once it stopped being reached through one.

func (*Delegation) Template

func (d *Delegation) Template(task DelegationTask) ([]ACESpec, error)

Template returns the ACE specs a task expands into. These mirror the AD "Delegate Control" wizard's common tasks; the object/attribute names are cross-checked against the schema well-known table and intended to be verified end-to-end on the lab.

type DelegationTask

type DelegationTask string

DelegationTask names a curated bundle of ACEs.

const (
	TaskResetUserPasswords    DelegationTask = "reset_user_passwords"
	TaskManageUsers           DelegationTask = "manage_users"
	TaskModifyGroupMembership DelegationTask = "modify_group_membership"
	TaskManageGroups          DelegationTask = "manage_groups"
)

func Tasks

func Tasks() []DelegationTask

Tasks returns every task name, in a stable order.

type DeleteOptions

type DeleteOptions struct {
	// Unprotect lifts ProtectedFromAccidentalDeletion before deleting. Without
	// it, deleting an OU created with AD's own default fails.
	Unprotect bool
}

DeleteOptions is taken only by OU.Delete. Making the unprotect step an explicit option keeps the destructive part visible at the call site.

type Directory

type Directory struct {
	OU             OUDirectory
	Group          GroupDirectory
	User           UserDirectory
	ServiceAccount ServiceAccountDirectory
	Computer       ComputerDirectory
	ACL            ACLDirectory
	Schema         SchemaDirectory

	// Server is the pinned domain controller every operation targets.
	Server string
	// DNC is the domain's default naming context, e.g. "DC=corp,DC=local".
	DNC string

	Closer io.Closer
}

Directory is what a backend hands a consumer. It is a struct of interfaces rather than an interface of accessors so that a backend whose sub-clients are already struct fields satisfies it without changing its own API.

Every guarantee a consumer relies on — read-back after write, delete verification, a pinned domain controller, serialized writes per identity — is the implementation's to uphold. RunDirectorySuite in adcoretest asserts them behaviourally against any implementation.

func (Directory) Close

func (d Directory) Close() error

Close releases the backend's resources.

type Error

type Error struct {
	Kind          Kind   // the only thing callers switch on
	Op            string // "User.Create"
	Identity      string // the identity acted on, in form:value notation
	ExceptionType string // verbatim, e.g. …ADIdentityNotFoundException
	Code          int    // Win32 code; decode via MS-ERREF
	ServerMessage string // the DC's own words, via IHasServerErrorMessage
	FQID          string // cmdlet-specific; diagnostics only
	Target        string
	Tombstoned    bool // set when an already-exists was traced to a deleted object
	Err           error
}

Error is the single error type this library returns. AD's raw detail is carried alongside the normalized Kind so a caller can render an exact message without switching on Microsoft's type names.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is matches the Kind sentinels, so errors.Is(err, ErrNotFound) works without exposing the sentinel's concrete type.

func (*Error) Unwrap

func (e *Error) Unwrap() error

type GMSA

type GMSA struct {
	GUID                          string
	DN                            string
	Name                          string
	SamAccountName                string
	Container                     string
	SID                           string
	DNSHostName                   string
	Description                   string
	DisplayName                   string
	Enabled                       bool
	TrustedForDelegation          bool
	PrincipalsAllowed             []string // objectGUIDs, resolved from the DNs AD returns
	ServicePrincipalNames         []string
	KerberosEncryptionType        []string
	ManagedPasswordIntervalInDays int
	AccountExpiration             *time.Time // nil means never
}

GMSA is a group Managed Service Account as this library reads it back.

type GMSASpec

type GMSASpec struct {
	Name                          string // CN; required
	SamAccountName                string // required; <= 15 chars, "$" is added by AD
	Container                     string // parent DN; required
	DNSHostName                   *string
	Description                   *string
	DisplayName                   *string
	Enabled                       *bool
	TrustedForDelegation          *bool
	PrincipalsAllowed             []Identity // full-replace; nil leaves alone, non-nil (incl. empty) replaces
	ServicePrincipalNames         *[]string  // nil leaves alone, non-nil (incl. empty) replaces
	KerberosEncryptionType        *[]string  // nil leaves alone, non-nil replaces
	AccountExpiration             OptTime
	ManagedPasswordIntervalInDays *int // create-only; ignored on Update
}

GMSASpec is the desired state of a group Managed Service Account. Pointer fields follow the tri-state convention: nil leaves the attribute alone, a pointer to "" clears it, a pointer to a value sets it.

func (GMSASpec) Validate

func (s GMSASpec) Validate(op string, forCreate bool) error

forCreate follows the same convention GroupSpec.Validate uses: the op string is for error stamping only, never for branching. Branching on it (as an earlier version of this method did, comparing op == "GMSA.Create") silently breaks the moment a caller's op string doesn't match that literal — which is exactly what happened here, since ServiceAccountClient.Create (following the <Resource>.<Verb> convention every other sub-client uses) passes "ServiceAccount.Create", not "GMSA.Create".

type Group

type Group struct {
	GUID           string
	DN             string
	Name           string
	SamAccountName string
	Container      string
	Scope          GroupScope
	Category       GroupCategory
	Description    string
	ManagedBy      string
	SID            string
}

Group is a security or distribution group.

type GroupCategory

type GroupCategory string

GroupCategory distinguishes a security principal from a distribution list.

const (
	GroupCategorySecurity     GroupCategory = "security"
	GroupCategoryDistribution GroupCategory = "distribution"
)

type GroupDirectory

type GroupDirectory interface {
	Create(ctx context.Context, spec GroupSpec) (*Group, error)
	Get(ctx context.Context, id Identity) (*Group, error)
	Search(ctx context.Context, q Query) ([]Group, error)
	Update(ctx context.Context, id Identity, spec GroupSpec) (*Group, error)
	Delete(ctx context.Context, id Identity) error

	Members(ctx context.Context, id Identity) ([]Member, error)
	MembersRecursive(ctx context.Context, id Identity) ([]Member, error)
	AddMembers(ctx context.Context, id Identity, members []Identity) error
	RemoveMembers(ctx context.Context, id Identity, members []Identity) error
	IsMember(ctx context.Context, id, member Identity) (bool, error)
}

type GroupScope

type GroupScope string

GroupScope is the group's replication and membership scope.

const (
	GroupScopeGlobal      GroupScope = "global"
	GroupScopeDomainLocal GroupScope = "domainlocal"
	GroupScopeUniversal   GroupScope = "universal"
)

type GroupSpec

type GroupSpec struct {
	Name           string // the CN; a change means Rename-ADObject
	SamAccountName string // required; changes through Set-ADGroup
	Container      string // parent DN; a change means Move-ADObject
	Scope          GroupScope
	Category       GroupCategory // defaults to security
	Description    *string
	ManagedBy      *string
}

GroupSpec is the desired state of a group.

func (GroupSpec) Validate

func (s GroupSpec) Validate(op string, forCreate bool) error

type Identity

type Identity interface {
	String() string
	// contains filtered or unexported methods
}

Identity is an AD identity argument. The interface is sealed by unexported methods: the only values that satisfy it come from the four constructors below, so no caller can hand a backend an arbitrary string as an identity — which is what keeps a caller's value from becoming PowerShell script text or an unescaped LDAP filter term.

Backends in other modules read the parts through IdentityArg and IdentityForm. Those are functions rather than interface methods precisely so that the seal survives the module boundary: exporting the methods would let any package implement Identity.

func ByDN

func ByDN(dn string) Identity

ByDN identifies an object by distinguished name.

func ByGUID

func ByGUID(guid string) Identity

ByGUID identifies an object by objectGUID. This is the canonical form: it survives rename and move, which DN and sAMAccountName do not.

func BySAM

func BySAM(sam string) Identity

BySAM identifies a security principal by sAMAccountName.

func BySID

func BySID(sid string) Identity

BySID identifies a security principal by SID.

type Inheritance

type Inheritance string

Inheritance is how an ACE propagates. It maps onto System.DirectoryServices.ActiveDirectorySecurityInheritance.

const (
	InheritanceThis        Inheritance = "this"        // this object only
	InheritanceDescendants Inheritance = "descendants" // all descendants, scoped by InheritedObjectType
	InheritanceChildren    Inheritance = "children"    // immediate children only
)

type KeyedMutex

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

KeyedMutex serializes writes to one target. Two writers naming different objects proceed concurrently, which is what keeps Terraform's parallel graph walk useful.

func NewKeyedMutex

func NewKeyedMutex() *KeyedMutex

NewKeyedMutex returns a KeyedMutex ready for use.

func (*KeyedMutex) Lock

func (k *KeyedMutex) Lock(key string) (unlock func())

Lock blocks until key is available and returns the release function.

func (*KeyedMutex) Size

func (k *KeyedMutex) Size() int

Size reports how many keys are currently held or waited on. Test support.

type Kind

type Kind int

Kind is the normalized condition a caller switches on. Adding a condition later adds a Kind, not a type: the public surface does not track Microsoft's exception list.

const (
	KindUnknown Kind = iota
	KindNotFound
	KindAlreadyExists
	KindDenied
	KindConstraint
	KindPassword
	KindReferral
	// KindTransient means the operation provably did not execute: the
	// failure occurred before the script (or, for a transport, the request
	// carrying it) was ever sent, so re-issuing the identical request
	// cannot duplicate a side effect. This is the bar a producer of
	// KindTransient must clear — not "this looks like it might be worth
	// retrying," but "nothing happened, by construction, every time." An AD
	// result code that comes back through the envelope (RPC_S_SERVER_BUSY
	// and friends in classByCode) clears it: the request reached AD and was
	// refused before anything was done. A transport-level
	// failure clears it only when it is raised by a pre-send check (a
	// semaphore, a circuit breaker, a not-yet-connected guard) that runs
	// before any bytes carrying the request leave the process.
	//
	// Anything that merely *might* not have executed is not transient, and
	// must map to KindTransport (or another non-retryable Kind) instead. In
	// particular, a context cancellation or deadline observed while awaiting
	// a response is not transient: depending on the transport, the script
	// may already have reached the server before the deadline fired, so the
	// failure and a completed execution are indistinguishable from here.
	// This is the exact bug that once made transport/winrm/wrap.go's
	// mapExecuteError treat context.Canceled/context.DeadlineExceeded as
	// KindTransient — up to Retry.MaxAttempts re-issues of an operation that
	// may have already run. See mapExecuteError's doc for the WSMan-specific
	// mechanics and why a caller-side cancellation loses nothing by staying
	// non-retryable (core.backoff already aborts on the caller's own
	// ctx.Done()).
	KindTransient
	KindTransport
	KindInvalidAttribute
	KindSchema
	// KindReplication means the write succeeded and the replication wait did
	// not complete. It is never retried, and the caller must persist the model
	// it was returned alongside.
	KindReplication
	// KindTooManyResults means a search matched more than its size limit. It is
	// never retried; the caller narrows the filter or raises the limit.
	KindTooManyResults
	// KindUnsupported means the operation cannot run against this transport's
	// endpoint — e.g. an ACL op against a ConstrainedLanguage endpoint, whose
	// .NET DirectoryServices calls are unavailable. Never retried.
	KindUnsupported
)

func ClassifyCode

func ClassifyCode(code int) (Kind, bool)

ClassifyCode maps a Win32 error code (MS-ERREF) to a Kind. It reports false for an unmapped code, which the caller must treat as KindUnknown: an unrecognized condition is never retried.

Both backends share this table. The PowerShell backend reads the code off the exception the ActiveDirectory module raises; the LDAP backend parses it out of the diagnostic message, which AD prefixes with exactly these codes in hex (for example "0000208D: NameErr:"). Sharing the table is what makes the two produce identical Kinds from identical conditions.

func (Kind) Retryable

func (k Kind) Retryable() bool

Retryable is deliberately narrow, and narrow for two different reasons. Retrying an access-denied or a duplicate-object error only delays a clear message — that failure is final, retrying wastes time. Retrying anything that is not KindTransient risks something worse than wasted time: KindTransient is the only Kind whose contract guarantees the operation provably did not execute (see its doc), so it is the only Kind a backend may safely re-issue. A Kind added later must not be folded into this check unless it can make that same guarantee.

func (Kind) String

func (k Kind) String() string

type Member

type Member struct {
	GUID  string
	DN    string
	Class string // user, group, computer, foreignSecurityPrincipal, …
	SID   string
}

Member is one entry read back from a group's membership.

type OU

type OU struct {
	GUID        string
	DN          string
	Name        string
	Container   string // derived from DN; never echoed by the script
	Description string
	Protected   bool
}

OU is an organizational unit as this library reads it back.

type OUDirectory

type OUDirectory interface {
	Create(ctx context.Context, spec OUSpec) (*OU, error)
	Get(ctx context.Context, id Identity) (*OU, error)
	Search(ctx context.Context, q Query) ([]OU, error)
	Update(ctx context.Context, id Identity, spec OUSpec) (*OU, error)
	Delete(ctx context.Context, id Identity, opts DeleteOptions) error
}

type OUSpec

type OUSpec struct {
	Name        string // the RDN; required on create, a change means Rename-ADObject
	Container   string // parent DN; required on create, a change means Move-ADObject
	Description *string
	Protected   *bool // ProtectedFromAccidentalDeletion
}

OUSpec is the desired state of an organizational unit. A nil pointer leaves the attribute alone; a pointer to "" clears it; a pointer to a value sets it.

type OptTime

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

OptTime is the three-state carrier a *time.Time cannot express: time.Time has no empty sentinel the way string does. The zero value leaves the attribute alone.

func ClearTime

func ClearTime() OptTime

ClearTime clears accountExpires, which in AD means "never expires".

func SetTime

func SetTime(t time.Time) OptTime

SetTime writes accountExpires.

func (OptTime) IsClear

func (o OptTime) IsClear() bool

IsClear reports whether the attribute should be cleared.

func (OptTime) IsSet

func (o OptTime) IsSet() bool

IsSet reports whether a value should be written.

func (OptTime) Value

func (o OptTime) Value() time.Time

Value is meaningful only when IsSet reports true.

type PresenceCheck

type PresenceCheck struct {
	// Present reports whether the object was still resolvable. The JSON tag
	// is the name the PowerShell probe writes and must not be renamed with
	// the field.
	Present       bool   `json:"found"`
	ExceptionType string `json:"type"`
	ErrorCode     int    `json:"errorCode"`
	Message       string `json:"message"`

	Kind Kind `json:"-"`
}

PresenceCheck is the result of re-reading an object after a delete. The probe never decides whether an object is gone: it hands the failure back so the backend's classifier, which fails closed, makes the call.

Kind is the backend's own classification of the probe's failure. It is a field rather than something ConfirmAbsent derives because the two backends classify from different evidence — go-adpwsh from the .NET exception type the ActiveDirectory module raises, go-adldap from an LDAP result code — and neither vocabulary belongs in a backend-neutral module.

func (PresenceCheck) ConfirmAbsent

func (p PresenceCheck) ConfirmAbsent(op string, id Identity, dn string) error

ConfirmAbsent turns a presence probe into the delete verdict. A destroy that silently no-ops is worse than one that fails: Terraform drops the resource from state and the object is then unmanaged and invisible.

type Query

type Query struct {
	Filter     string      // a COMPLETE LDAP filter; "" ⇒ "(objectClass=*)"
	SearchBase string      // DN; "" ⇒ the pinned domain's defaultNamingContext
	Scope      SearchScope // "" ⇒ subtree
	SizeLimit  int         // ≤ 0 ⇒ DefaultSizeLimit
}

Query is a directory search. The zero value searches the whole domain subtree for every object of the sub-client's class, capped at the default limit.

func (Query) WithDefaults

func (q Query) WithDefaults(dnc string) Query

WithDefaults resolves the zero-value fields against the pinned domain.

type RelativeDN

type RelativeDN struct {
	Attributes []AttributeTypeAndValue
}

RelativeDN is one comma-separated component; it holds more than one attribute only for the multi-valued (plus-joined) form.

type RetryConfig

type RetryConfig struct {
	MaxAttempts    int
	InitialBackoff time.Duration
	MaxBackoff     time.Duration
	Jitter         float64 // fraction of the backoff, 0..1
}

RetryConfig is values, not code. It governs re-attempts, and applies only to errors classified transient — see Kind.Retryable for why that set is as narrow as it is.

func (RetryConfig) WithDefaults

func (r RetryConfig) WithDefaults() RetryConfig

WithDefaults fills the zero-value fields.

type Right string

Right is one System.DirectoryServices.ActiveDirectoryRights value, by name.

type SchemaDirectory

type SchemaDirectory interface {
	Resolve(ctx context.Context, refs []SchemaRef) (map[SchemaRef]string, error)
}

Resolve is batched because the round trip is per call, not per name: an ACL grant resolves a dozen names at once and a one-at-a-time signature would make that a dozen searches.

type SchemaRef

type SchemaRef struct {
	Kind SchemaRefKind
	Name string
}

SchemaRef is a friendly name to resolve to a GUID.

type SchemaRefKind

type SchemaRefKind string

SchemaRefKind is which schema partition a name is resolved against.

const (
	RefAttribute     SchemaRefKind = "attribute"
	RefClass         SchemaRefKind = "class"
	RefExtendedRight SchemaRefKind = "extended_right"
)

type SearchScope

type SearchScope string

SearchScope is the depth a directory search descends to.

const (
	SearchScopeBase     SearchScope = "base"
	SearchScopeOneLevel SearchScope = "onelevel"
	SearchScopeSubtree  SearchScope = "subtree"
)

type Secret

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

Secret carries a password without letting it reach a log line, a state file, or a %v verb. Its plaintext is readable only through RevealSecret, which a backend's payload builder calls deliberately at the moment of serialization. The guarantee is on the type, not on each call site.

func NewSecret

func NewSecret(s string) Secret

NewSecret wraps a plaintext password.

func (Secret) Format

func (s Secret) Format(f fmt.State, verb rune)

Format makes every verb safe, not just the string ones. fmt consults a Stringer only for %v, %s, %q, %x and %X; under %d it walks the struct and prints the field, so a Stringer alone leaks the plaintext as "{%!d(string=hunter2)}". Implementing Formatter is what makes "a Secret never reaches a log line" true whatever verb the log line happens to use.

func (Secret) GoString

func (Secret) GoString() string

GoString makes %#v safe.

func (Secret) IsZero

func (s Secret) IsZero() bool

IsZero reports whether the secret was never set.

func (Secret) MarshalJSON

func (Secret) MarshalJSON() ([]byte, error)

MarshalJSON always fails. A Secret must be revealed deliberately; it must never be serialized by a struct walk into a log line or a state file.

func (Secret) String

func (Secret) String() string

String makes %v, %s and the print helpers safe.

type ServiceAccountDirectory

type ServiceAccountDirectory interface {
	Create(ctx context.Context, spec GMSASpec) (*GMSA, error)
	Get(ctx context.Context, id Identity) (*GMSA, error)
	Search(ctx context.Context, q Query) ([]GMSA, error)
	Update(ctx context.Context, id Identity, spec GMSASpec) (*GMSA, error)
	Delete(ctx context.Context, id Identity) error
}

type User

type User struct {
	GUID                  string
	DN                    string
	Name                  string
	SamAccountName        string
	UserPrincipalName     string
	DisplayName           string
	GivenName             string
	Surname               string
	Description           string
	Container             string
	Enabled               bool
	SID                   string
	ChangePasswordAtLogon bool
	CanChangePassword     bool
	PasswordExpires       bool
	AccountExpiration     *time.Time // nil means the account never expires
}

User is a user account.

type UserDirectory

type UserDirectory interface {
	Create(ctx context.Context, spec UserSpec) (*User, error)
	Get(ctx context.Context, id Identity) (*User, error)
	Search(ctx context.Context, q Query) ([]User, error)
	Update(ctx context.Context, id Identity, spec UserSpec) (*User, error)
	Delete(ctx context.Context, id Identity) error
	SetPassword(ctx context.Context, id Identity, password Secret) error
}

type UserSpec

type UserSpec struct {
	SamAccountName    string  // required on create
	Container         string  // required on create; a change means Move-ADObject
	Name              *string // the CN; defaults to SamAccountName on create; a change means Rename-ADObject
	UserPrincipalName *string
	DisplayName       *string
	GivenName         *string
	Surname           *string
	Description       *string

	Enabled               *bool
	Password              *Secret
	ChangePasswordAtLogon *bool
	CanChangePassword     *bool
	PasswordExpires       *bool
	AccountExpiration     OptTime
}

UserSpec is the desired state of a user account.

func (UserSpec) Validate

func (s UserSpec) Validate(op string) error

Directories

Path Synopsis
Package adcorefake is an in-memory adcore.Directory.
Package adcorefake is an in-memory adcore.Directory.
Package adcoretest asserts the guarantees an adcore.Directory must uphold, against any implementation.
Package adcoretest asserts the guarantees an adcore.Directory must uphold, against any implementation.
Package schema holds the Active Directory schema catalog: every attribute's type and constraints, and every exported class's effective set of allowed attributes.
Package schema holds the Active Directory schema catalog: every attribute's type and constraints, and every exported class's effective set of allowed attributes.

Jump to

Keyboard shortcuts

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