Documentation
¶
Overview ¶
Package accesspolicies discovers and decodes the DALgo access policies a DataTug user keeps on disk, runs a query through them as a secured application would, and explains what the policies did to that query.
Index ¶
- Constants
- Variables
- func AuthorizeWrite(ctx context.Context, o WriteOptions, operation access.Operations, ...) error
- func BaseResource(query dal.Query) access.Resource
- func CanonicalQueryID(id string) string
- func Fingerprint(loaded []Loaded, unrestricted bool, principal *access.Principal) string
- func ParseVariables(pairs []string) (map[string]any, error)
- func Policies(loaded []Loaded) []access.Policy
- func ProjectQueryResource(projectID, queryID string) access.Resource
- func QueryParameters(query dal.StructuredQuery) ([]string, error)
- func ResolveDir(flagValue string) (dir string, explicit bool, err error)
- type Line
- type LoadOptions
- type Loaded
- type Options
- type Result
- type WriteDeniedError
- type WriteOptions
Constants ¶
const ( ProjectsCollection = "datatug_projects" ProjectQueriesCollection = "queries" )
ProjectsCollection and ProjectQueriesCollection name DataTug project files as DALgo policy resources: a saved query is the record /datatug_projects/{projectID}/queries/{queryID}, where queryID is the query's canonical, folder-qualified ID kept as one path segment. The "datatug_" prefix keeps project files apart from every data source's own collection names, so a grant on a data collection (path: /Customer) never grants a project write, while a catch-all grant (path: /**) - the demo-project-1 admin rule set - covers them.
The separation rests on that prefix alone: policy paths share one namespace, so a data source with a collection literally named "datatug_projects" would put its rows under the same paths as project files, and a grant written for either would apply to both. Never give a data collection that name.
const DefaultDir = ".datatug/policies"
DefaultDir is the per-user policies directory relative to the home directory.
const DirEnv = "DATATUG_POLICIES_DIR"
DirEnv names the environment variable that overrides the default policies directory.
Variables ¶
var ErrInvalidQuery = errors.New("invalid query")
ErrInvalidQuery marks a query the command refuses before execution: an unresolved or misplaced parameter, or an alias under a field allow-list.
var ErrNoPolicies = errors.New("no access policies loaded; pass --no-policies to run unrestricted")
ErrNoPolicies is returned by Load when nothing was loaded and the caller did not ask to run unrestricted.
Functions ¶
func AuthorizeWrite ¶ added in v0.25.0
func AuthorizeWrite(ctx context.Context, o WriteOptions, operation access.Operations, resource access.Resource) error
AuthorizeWrite decides whether o's principal may perform operation (one of Insert, Set, Update or Delete) on resource, returning nil when it may and a *WriteDeniedError when it may not.
It is for project files only - saved queries, named by ProjectQueryResource - and must never decide a data write. It refuses any grant that holds only under a row condition, check or field list, and it evaluates each policy's project-write view, whose query ids are canonicalized; a data write must go through DALgo's residual-enforcing write session, which checks those conditions against the rows written. Nor may grants from any other source, such as incident-scoped read grants, be appended to o.Policies: every policy listed must allow a write, and none of them is written to widen one.
Deny by default:
- An Unrestricted session is the explicit --no-policies local-owner profile and is allowed.
- Otherwise at least one policy must be loaded and every loaded policy must allow the write, for the principal on the context - the same intersection a secured session applies to reads. A principal no grant binds, a read-only grant and a missing principal are all denied.
- A grant that holds only under a row condition, a check or a field allow-list is denied too: a project file has no rows or fields to evaluate it against, so such a grant cannot be enforced here.
How a policy names a query. A saved query is the record /datatug_projects/{projectID}/queries/{queryID} (ProjectQueryResource):
- queryID is the query's folder-qualified id kept as ONE path segment. A policy names the query "revenue" in folder "reports" as /datatug_projects/demo/queries/reports%2Frevenue, the "/" inside the id percent-encoded, and every query in every folder as /datatug_projects/demo/queries/* (or .../queries/**).
- Query ids match by CanonicalQueryID on both sides, the resource and every query id in the loaded policies' paths: case- and Unicode-normalization-insensitively, so "Revenue", "REVENUE" and "revenue" are one query to every rule, as they are one file on APFS.
- Folder-scoped rules are not supported. A rule path below the query-id segment (.../queries/reports/**, .../queries/reports/x) can never match a query, so rather than let such a rule silently not apply, every project query write is refused while a policy holding one is loaded, and the refusal names the rule. Reads are unaffected: they run through the policy as loaded.
func BaseResource ¶
BaseResource names the collection a query reads.
func CanonicalQueryID ¶ added in v0.25.0
CanonicalQueryID returns the spelling of a saved query's id that project write authorization compares. Two ids with the same CanonicalQueryID get the same decision, on every platform, because both the resource being written (ProjectQueryResource) and every query id a policy path names (AuthorizeWrite's view of a loaded policy) are compared in this form.
A query id names files, and the file systems DataTug serves from resolve more than one spelling to the same file. The canonical form is coarser than every one of them, so it can only ever merge more spellings into one decision, never split one file's spellings apart:
- APFS and Linux ext4 with casefolding compare names after canonical decomposition and full case folding, so NFC "café" and NFD "café", and "straße" and "STRASSE", are one name;
- HFS+ decomposes and folds case through its own older table, and ignores the default-ignorable code points (a zero-width joiner, a soft hyphen, a byte-order mark) entirely;
- NTFS and exFAT compare through an uppercase table, which merges spellings full case folding does not - "ı" with "i" and "I".
So one round of the canonical form decomposes (NFD), applies full case folding, drops every default-ignorable code point, and maps each remaining rune to the smallest rune of its case class - the transitive closure of simple upper-, lower- and title-casing and simple case folding, which covers every one of those tables' mappings, whichever direction each maps in. Full folding merges what maps to more than one rune ("ß" to "ss"), and the case class merges what the folding tables leave apart ("ı" with "I", and the Cherokee letters, whose folding maps in the opposite direction to most scripts').
The result is a fixed point: rounds are applied until the string stops changing, so C(C(x)) == C(x) for every string. Convergence is exhaustive over every Unicode scalar value in the package's own tests; a string that somehow did not converge within canonicalRounds becomes one fixed unstableCanonicalID, which is coarser still - such ids all share one decision - and never splits a file's spellings.
func Fingerprint ¶ added in v0.27.0
Fingerprint returns a deterministic server-attested identity for the exact policy documents in their evaluation order. An unrestricted session has a stable, distinct fingerprint as well.
func ParseVariables ¶
ParseVariables turns name=value pairs into policy variables. Names follow DALgo's parameter grammar; currentUser, principal.* and path.* are reserved for --as, --role, --group and path captures. Values are YAML scalars or flow sequences so numbers, booleans and lists keep their type; quote a value to force a string; a value that is not valid YAML is kept as the literal string.
func ProjectQueryResource ¶ added in v0.25.0
ProjectQueryResource returns the policy resource for the saved query queryID of project projectID. The query id is kept in its canonical caseless form (CanonicalQueryID), so every spelling a file system resolves to the same query file names the same resource.
func QueryParameters ¶ added in v0.25.0
func QueryParameters(query dal.StructuredQuery) ([]string, error)
QueryParameters lists the parameter names (without the "$") query references, sorted and de-duplicated. A parameter anywhere other than the right-hand side of a where comparison is an ErrInvalidQuery - the same rule Run applies before it executes a query - so a query this accepts is one Run can bind.
Types ¶
type Line ¶
type Line struct {
Policy string
Source string
Resource string
Rule string
Allowed bool
Condition string // as access.Decision.Condition renders it; parameter names, never values
Bindings []string // "name=value" for the caller-supplied variables the condition references
FieldLists [][]string
Via string // binding attribution from a principal-bound policy ("role:editor")
Explanation string
}
Line is what one policy decided for a query's base collection.
type LoadOptions ¶
type LoadOptions struct {
// Dir is the --policies-dir value; empty means environment or default.
Dir string
// Files are additional --policy documents, appended after the directory.
Files []string
// None skips discovery (--no-policies); it conflicts with Files.
None bool
}
LoadOptions says where policies come from.
type Loaded ¶
Loaded is one decoded access document and the file it came from.
func DecodeLoaded ¶ added in v0.25.0
DecodeLoaded decodes one access document held in data, as LoadFile does for a file; source is where it came from and is never shown to a client. Besides the policy every read runs through, it prepares the document's project-write view (prepareProjectWrites): a document that cannot be prepared still loads, and AuthorizeWrite refuses project writes under it.
func Load ¶
func Load(o LoadOptions) ([]Loaded, error)
Load discovers and decodes the caller's policies. A missing default directory is empty; a missing explicit one is an error. Nothing loaded is ErrNoPolicies unless None is set.
type Options ¶
type Options struct {
// Principal is the caller; nil runs anonymously.
Principal *access.Principal
// Variables are the caller's --var values.
Variables map[string]any
// Policies are the loaded documents.
Policies []Loaded
// Unrestricted must be set explicitly to run with no policies at all.
Unrestricted bool
}
Options configure one secured run.
type Result ¶
type Result struct {
// Reader yields the rows the secured session admits.
Reader dal.RecordsReader
// Lines explains, per policy, what applied to the query; nil when
// unrestricted.
Lines []Line
// Query is the query as executed, with parameters substituted.
Query dal.Query
}
Result is what Run returns before any row is read.
func Run ¶
Run executes query over session as a policy-secured application would for the principal: it puts the principal and variables on the context, substitutes the query's own parameters, refuses aliases and references to fields the caller may not see, explains the policies' decisions and opens the reader through access.SecureReadSession. Denials wrap access.ErrAccessDenied; refused queries wrap ErrInvalidQuery.
type WriteDeniedError ¶ added in v0.25.0
type WriteDeniedError struct {
// Policy is the name of the policy that refused the write; empty when
// the write was refused by default, with no policy deciding.
Policy string
Operation access.Operations
Resource string
Reason string
}
WriteDeniedError reports a refused project write. It names the deciding policy, the operation, the resource and a reason, and never the policy file's location on the serving machine. It unwraps to access.ErrAccessDenied.
func (*WriteDeniedError) Error ¶ added in v0.25.0
func (e *WriteDeniedError) Error() string
func (*WriteDeniedError) Unwrap ¶ added in v0.25.0
func (e *WriteDeniedError) Unwrap() error
Unwrap makes errors.Is(err, access.ErrAccessDenied) true.
type WriteOptions ¶ added in v0.25.0
type WriteOptions struct {
// Principal is the caller; nil means no principal, which no grant binds.
Principal *access.Principal
// Policies are the loaded documents; every one must allow the write.
Policies []Loaded
// Unrestricted is the explicit --no-policies local-owner profile.
Unrestricted bool
}
WriteOptions is the identity and policy set a project write is decided under - a `datatug serve` session's fixed principal and loaded policies.