Documentation
¶
Overview ¶
Package permission administers who may do what, and decides nothing.
The authority over every action stays where it already was: a policy written in Go, reached through a Grant, refusing by default. What this package owns is the administration of that -- named groups, the actions each group carries, the people in them -- and the middleware that puts the result on the acting subject before any policy runs.
There is no table of permissions and there is no way to make one. The set of actions a group may carry is the application's own code, handed in as Config.Actions, and a write naming anything outside it is refused. A row here links a group to a permission that already exists because some policy reads it; it cannot bring one into being.
The files are laid out by role rather than by layer, so the whole package reads top to bottom:
module.go -> registration, routes, handlers and migrations config.go -> what the application passes in catalogue.go -> the closed set of actions a group may carry model.go -> the entities, and what they may answer with policy.go -> who may do what service.go -> the rules and Model access, after authorization resolver.go -> what a request carries into every policy views.go -> the files the application takes ownership of
An application registers it explicitly. There is no service provider, no container and no discovery: the wiring is a few lines somebody wrote, and reading them is how they learn what the application is made of.
The first group has to come from outside a request, because nobody can administer permissions until somebody carries them. A seed builds the subject it acts as -- an identifier, the tenant, and the actions of this package as its roles -- and calls the same service every screen calls. There is no second write path and no escape hatch: the seed is authorized by the same policies, by a subject the operator running it constructed.
Index ¶
- Constants
- Variables
- func Actions() []security.Action
- func GroupActions(db *data.DB) *model.Model[GroupAction]
- func GroupUsers(db *data.DB) *model.Model[GroupUser]
- func Groups(db *data.DB) *model.Model[Group]
- func PublishedPaths() []string
- func Versions(db *data.DB) *model.Model[Version]
- func ViewNames() []string
- func ViewPackages() []string
- type ActionChoice
- type ActionPolicy
- type ActionSection
- type Catalogue
- type CatalogueDomain
- type CatalogueEntry
- type CataloguePageData
- type CatalogueView
- type Change
- type Config
- type CreateGroupRequest
- type Domain
- type Effective
- type EffectiveGrant
- type Group
- type GroupAction
- type GroupPage
- type GroupPageData
- type GroupPolicy
- type GroupQuery
- type GroupRef
- type GroupUser
- type GroupsPageData
- type MatrixCell
- type MatrixPageData
- type MatrixRow
- type MatrixView
- type MemberPageData
- type MembershipPolicy
- type Module
- type PermissionService
- func (s *PermissionService) ActionsOf(ctx context.Context, actor security.Subject, groupID string) ([]security.Action, error)
- func (s *PermissionService) Catalogue() Catalogue
- func (s *PermissionService) CreateGroup(ctx context.Context, actor security.Subject, in CreateGroupRequest) (*Group, error)
- func (s *PermissionService) DeleteGroup(ctx context.Context, actor security.Subject, id string) error
- func (s *PermissionService) EffectiveFor(ctx context.Context, actor security.Subject, userID string) (Effective, error)
- func (s *PermissionService) FindGroup(ctx context.Context, actor security.Subject, id string) (*Group, error)
- func (s *PermissionService) ListGroups(ctx context.Context, actor security.Subject, q GroupQuery) (GroupPage, error)
- func (s *PermissionService) MembersOf(ctx context.Context, actor security.Subject, groupID string) ([]string, error)
- func (s *PermissionService) PreviewActions(ctx context.Context, actor security.Subject, groupID string, ...) (Change, error)
- func (s *PermissionService) PreviewMembers(ctx context.Context, actor security.Subject, groupID string, wanted []string) (Change, error)
- func (s *PermissionService) ResolveOwn(ctx context.Context, actor security.Subject) (Resolution, error)
- func (s *PermissionService) SetActions(ctx context.Context, actor security.Subject, groupID string, ...) (Change, error)
- func (s *PermissionService) SetMembers(ctx context.Context, actor security.Subject, groupID string, wanted []string) (Change, error)
- func (s *PermissionService) UpdateGroup(ctx context.Context, actor security.Subject, id string, in UpdateGroupRequest) (*Group, error)
- func (s *PermissionService) Version(ctx context.Context, actor security.Subject) (int64, error)
- func (s *PermissionService) ViewCatalogue(ctx context.Context, actor security.Subject) (CatalogueView, error)
- func (s *PermissionService) ViewMatrix(ctx context.Context, actor security.Subject, q GroupQuery) (MatrixView, error)
- type Resolution
- type Resolver
- type SummaryPageData
- type UpdateGroupRequest
- type Version
Constants ¶
const ( // DefaultPrefix is where the routes are mounted when Config leaves Prefix // empty. DefaultPrefix = "/permission" // DefaultPageSize is how many groups one page answers with when Config // leaves PageSize at zero. DefaultPageSize = 25 // MaxPageSize is the ceiling PageSize is refused above. A page nobody // bounded is a page that reads the whole table on the day the table is // large. MaxPageSize = 200 // DefaultCacheSize is how many resolved subjects one process remembers // before it forgets all of them. DefaultCacheSize = 4096 )
The defaults for the optional settings. They are constants rather than literals inside Config.withDefaults, so the value a reader finds here is the value the package uses.
const ( // GroupsTable holds one row per group. GroupsTable = "permission_groups" // GroupActionsTable holds one row per action a group carries. GroupActionsTable = "permission_group_actions" // GroupUsersTable holds one row per person in a group. GroupUsersTable = "permission_group_users" // VersionsTable holds one row per tenant, carrying the token that changes // whenever anything above does. VersionsTable = "permission_versions" )
The tables this package owns.
They are named here once and read from here everywhere, so the migration, the models and anything that inspects the schema cannot drift into two spellings of one table.
const ( // ViewGroupsIndex is the listing, with its search and its pages. ViewGroupsIndex = "vendor.permission.groups.index" // ViewGroupsShow is one group: what it carries and who is in it. ViewGroupsShow = "vendor.permission.groups.show" // ViewCatalogue is every action the application declares, by domain. ViewCatalogue = "vendor.permission.catalogue.index" // ViewMatrix is the grid of groups against actions. ViewMatrix = "vendor.permission.matrix.index" // ViewSummary is the fragment that says what a bulk write would change, // before it is applied. ViewSummary = "vendor.permission.matrix.summary" // ViewMember is one person's effective permissions and where each comes // from. ViewMember = "vendor.permission.users.show" )
The names the published views are rendered by.
They are derived from the paths in the archive, and the archive is what the publication writes, so a view that moved cannot keep an old name here without the module refusing to boot.
const ( // PermissionView is reading one group with the actions and members it // carries, and reading the effective permissions of one person. PermissionView security.Action = "permission.view" // PermissionList is paging through the groups and reading the catalogue. PermissionList security.Action = "permission.list" // PermissionCreate is adding a group. PermissionCreate security.Action = "permission.create" // PermissionUpdate is changing a group's name or description. PermissionUpdate security.Action = "permission.update" // PermissionDelete is removing a group. PermissionDelete security.Action = "permission.delete" // PermissionGrant is attaching an action to a group. PermissionGrant security.Action = "permission.grant" // PermissionRevoke is detaching an action from a group. PermissionRevoke security.Action = "permission.revoke" // PermissionAssign is putting a person into a group. PermissionAssign security.Action = "permission.assign" // PermissionUnassign is taking a person out of a group. PermissionUnassign security.Action = "permission.unassign" // PermissionResolve is reading the groups one is a member of. // // It is decided by identity and not by membership: whoever asks may read // their own rows and nobody else's, so attaching it to a group would grant // nothing. That is why Actions leaves it out, and why leaving it out is not // an omission -- a screen offering a permission that changes nothing is a // screen nobody can reason about. PermissionResolve security.Action = "permission.resolve" )
The actions of this package.
They are constants and never built from a variable: an action assembled at run time cannot be compared against the one a Grant was issued for, and it cannot be read out of the source by anything that enumerates what an application may grant.
They are the administration of the catalogue, and they are themselves part of it: whoever administers permissions holds a permission to do so, granted through a group like any other.
const ( // MaxSlugLength is how long a slug may be. It is short because the slug is // an identifier somebody types. MaxSlugLength = 64 // MaxNameLength and MaxDescriptionLength bound the free text. MaxNameLength = 120 MaxDescriptionLength = 500 // MaxBulkSize is how many actions or members one bulk write may name. A // request that named a hundred thousand would be authorized a hundred // thousand times before anything was written. MaxBulkSize = 500 )
The bounds on what a group may be called.
const PublishCommand = "aru vendor:publish --apply"
PublishCommand is what an application runs to take ownership of the views this package offers.
It is spelled out as a constant so that whatever says it -- the refusal below, or a message an application writes for its own operators -- says one thing. A person who is told two different commands for one job tries both.
Variables ¶
var ( // ErrNotFound is returned when no row matches, including when the row // exists in another tenant. The two cases are deliberately // indistinguishable. ErrNotFound = errors.New("permission: record not found") // ErrUnknownAction is returned when an action is not in the catalogue. It // is what makes a group unable to carry a permission no policy reads. ErrUnknownAction = errors.New("permission: the action is not in the catalogue") // ErrLastMember is returned when taking somebody out of a system group // would leave it empty. // // It is not an authorization refusal and is deliberately not spelled as // one: the subject was allowed to do it, and the state it would leave // behind is what is refused. An application that answered it as 403 would // tell somebody to ask for a permission they already have. ErrLastMember = errors.New("permission: a system group cannot be left without a member") // ErrSlugTaken is returned when a group with that slug already exists in // the tenant. ErrSlugTaken = errors.New("permission: a group with this slug already exists") )
The refusals this package answers with.
They are distinguished from one another because they are answered with different statuses and because only one of them is the caller's fault.
Functions ¶
func Actions ¶
Actions are the actions of this package that a group may carry, sorted.
An application splices them into the catalogue it hands to New, so that the administration of permissions can itself be administered instead of being reachable only by whoever seeded the first group.
func GroupActions ¶
func GroupActions(db *data.DB) *model.Model[GroupAction]
GroupActions returns the configured model for the group actions table.
It keeps no timestamps: the row is the link and nothing else, and a column that is written and never read is a column somebody eventually filters by.
func GroupUsers ¶
GroupUsers returns the configured model for the group members table.
It keeps no timestamps, for the reason GroupActions keeps none.
func Groups ¶
Groups returns the configured model for the groups table.
The primary key is application-generated text, so it does not increment. The tenant scope remains on the model's tenant_id default.
func PublishedPaths ¶
func PublishedPaths() []string
PublishedPaths are the files in the archive, each relative to the root of the application, sorted.
func Versions ¶
Versions returns the configured model for the version table.
Its key is the tenant column, which is the one place in this package where the two are the same: the table holds one row per customer, so the scope and the key are one question.
func ViewNames ¶
func ViewNames() []string
ViewNames are the names the published views are rendered by, sorted.
The name is the path under the view root with its separators turned into dots, which is what the view compiler writes into the registration call. It is derived from the same archive the publication carries, so a view that was renamed cannot keep an old name here.
func ViewPackages ¶
func ViewPackages() []string
ViewPackages are the directories the compiled views land in, each relative to the root of the application, sorted and without repeats.
Importing one is what puts its views in the binary: a compiled view calls the registry from init(), and a package nothing imports is not linked at all. The import is named rather than written into bootstrap/app.go, because one line somebody reads beats a file that changed while they were not looking.
Types ¶
type ActionChoice ¶
ActionChoice is one action on the group screen, and whether the group carries it.
type ActionPolicy ¶
type ActionPolicy struct{}
ActionPolicy decides who may attach an action to a group and detach it again.
It sees the action being granted, which is what lets it hold the rule that matters most here: nobody hands out what they do not hold. Without it, whoever may edit any group may write themselves every permission the catalogue has, and every other check in this package passes while it happens.
type ActionSection ¶
type ActionSection struct {
// Domain is the part before the first dot, shared by every action below.
Domain string
Choices []ActionChoice
}
ActionSection is one domain of the catalogue as the group screen draws it.
type Catalogue ¶
type Catalogue struct {
// contains filtered or unexported fields
}
Catalogue is the set of actions a group may carry.
It is not a table and there is no way to add to it from a screen. A row that attached a group to an action nothing decides with would be a permission somebody was told they had, and no policy would ever read it -- so the write path checks every action against this set and refuses what is not in it.
The application builds it from what its own code declares and hands it to New. Nothing here discovers actions: a catalogue assembled at boot would hold only the modules that happened to be linked, and the screen administers permissions for the whole application.
The zero value is empty and refuses every action, which is the safe direction: a module wired without a catalogue grants nothing rather than everything.
func NewCatalogue ¶
NewCatalogue returns the catalogue of these actions, or the reason it cannot be one.
It refuses an action that is not in module.verb form, because that shape is what the grouping reads and an action with no domain would sit in a section with no name. It refuses the empty set, because a module whose catalogue is empty can never grant anything and would say so one screen at a time instead of once, here.
Repeats are not an error. The same action reaching it twice is what happens when an application splices several lists together, and refusing that would make the caller deduplicate a set this function returns deduplicated anyway.
func (Catalogue) All ¶
All returns every action, sorted. The slice is a copy, so a caller that sorts or truncates it does not change the catalogue every write is checked against.
func (Catalogue) Domains ¶
Domains returns the catalogue grouped by domain, sorted. The slices are copies, for the reason All returns one.
type CatalogueDomain ¶
type CatalogueDomain struct {
// Name is the part of every action below it that comes before the first
// dot.
Name string
// Entries are the actions of this domain, sorted.
Entries []CatalogueEntry
}
CatalogueDomain is one section of the catalogue screen.
type CatalogueEntry ¶
type CatalogueEntry struct {
// Action is the identifier, and it is the identifier that is stored. What a
// person reads is derived from it when the page is drawn, so a label can be
// rewritten in any language without a row changing.
Action security.Action
// Groups are the groups that carry it, sorted by slug. Empty means the
// action exists in the code and no group grants it.
Groups []GroupRef
}
CatalogueEntry is one action of the catalogue and the groups that carry it.
type CataloguePageData ¶
type CataloguePageData struct {
hview.Page
Prefix string
Domains []CatalogueDomain
}
CataloguePageData is every action the application declares, by domain, and the groups that carry each.
type CatalogueView ¶
type CatalogueView struct {
// Domains are the sections, sorted by name.
Domains []CatalogueDomain
}
CatalogueView is the catalogue as a screen draws it.
type Change ¶
type Change struct {
// Added and Removed are what the write puts in and takes out, sorted.
// Unchanged is what was already as asked, which is what makes the summary
// readable: a list of twelve permissions where two are changing says more
// than a list of two.
Added []string
Removed []string
Unchanged []string
}
Change is what a bulk write would do, or did.
It is returned by the preview and by the write, from the same computation, so the summary somebody approved is the summary that was applied rather than a second reading of the same intent.
type Config ¶
type Config struct {
// Tenant is the customer a visitor with no session is read as.
//
// It is required, and it comes from the application's own configuration --
// never from the request. A tenant a visitor could name is a visitor who
// chooses whose rows they read. Everywhere there is a session, the tenant
// comes from the Grant instead, and this value is not consulted at all.
Tenant string
// Actions is every action the application may grant, and it is required.
//
// It is the application's own list, read out of its code, and it is handed
// in rather than discovered: a set assembled at boot would hold only the
// modules that happened to be linked into this binary, and the screen
// administers permissions for the whole application.
//
// It has to contain this package's own actions, which Actions returns, or
// the administration of permissions would be reachable only by whoever
// seeded the first group. Splice them in:
//
// Actions: append(myapp.Actions(), permission.Actions()...)
//
// Repeats cost nothing. What is not in it cannot be attached to a group,
// which is the whole of why a screen cannot invent a permission.
Actions []security.Action
// Prefix is the path the routes are mounted under. Empty means
// DefaultPrefix.
Prefix string
// PageSize is how many groups one page answers with. Zero means
// DefaultPageSize, and anything above MaxPageSize is refused rather than
// clamped: a number somebody wrote and did not get is worse than a number
// somebody wrote and was told about.
PageSize int
// CacheSize is how many resolved subjects one process remembers. Zero means
// DefaultCacheSize. It bounds memory and nothing else: the remembered
// answer is only used while the tenant's token is unchanged, so forgetting
// early costs a query and can never serve a permission that was revoked.
CacheSize int
}
Config is what the application passes when it wires this package.
A typed struct rather than a map: a misspelled key in a map is a setting that silently keeps its default, and the failure shows up as behaviour nobody asked for rather than as an error. Here a field that does not exist does not compile.
type CreateGroupRequest ¶
type CreateGroupRequest struct {
// Slug is the stable identifier inside the tenant.
Slug string
// Name and Description are what people read.
Name string
Description string
// System declares a group the installation depends on: one that cannot be
// deleted, cannot have actions taken off it, and cannot be left without a
// member.
//
// It is here for whatever seeds an installation, and the request the HTTP
// handler builds never sets it -- the handler reads three named fields and
// this is not one of them. A form that could declare a group undeletable
// would let anybody make one, and there would be no way back through this
// package.
System bool
}
CreateGroupRequest is the input contract for a new group.
The fields are explicit and there is no mass assignment, so a request body cannot write a column nobody meant to expose. There is no TenantID here and there must never be one: the tenant comes from the Grant, which comes from the session.
func (CreateGroupRequest) Validate ¶
func (r CreateGroupRequest) Validate() validation.Errors
Validate reports the errors per field.
type Domain ¶
type Domain struct {
// Name is the part before the first dot, shared by every action below.
Name string
// Actions are the actions of this domain, sorted.
Actions []security.Action
}
Domain is one group of the catalogue: the part of an action before its first dot, and every action that starts with it.
It is what a screen draws a section from. The name is the identifier and not a label: what a person reads is resolved when the page is rendered, so it can be written in their language without anything in the database changing.
type Effective ¶
type Effective struct {
// UserID is who this is about.
UserID string
// Grants are the actions they hold, sorted by action.
Grants []EffectiveGrant
// Groups are the groups they belong to, sorted by slug.
Groups []GroupRef
}
Effective is what one person may do, and why.
type EffectiveGrant ¶
type EffectiveGrant struct {
// Action is what the person may do.
Action security.Action
// Groups are the groups that grant it, sorted by slug.
Groups []GroupRef
}
EffectiveGrant is one action a person holds, and every group that gives it to them.
The origin is a list and not a single group, because two groups granting the same action is the normal case and the question a screen is asked is "why do they have this" -- which has as many answers as there are groups.
type Group ¶
type Group struct {
model.Model[Group]
// ID is the identifier. It is generated by the application rather than by
// the database, because a DEFAULT that produces a uuid is spelled
// differently in every engine.
ID string `db:"id"`
// TenantID is the customer the row belongs to. It is written from the
// Grant and read back so the policy can compare it against the subject's
// tenant.
TenantID string `db:"tenant_id"`
// Slug is the stable, lowercase name of the group inside its tenant. It is
// what a seed and a migration name a group by, and it does not change:
// renaming it would silently create a second group beside whatever already
// referred to the first.
Slug string `db:"slug"`
// Name is what a person calls this group, and Description is why it exists.
// Both are free text, both are editable, and neither is ever compared
// against anything.
Name string `db:"name"`
Description string `db:"description"`
// IsSystem marks a group the installation depends on. It cannot be deleted
// and its actions cannot be detached, because it is the group that carries
// the administration of permissions: an application that emptied it would
// have nobody left who could fill it again.
IsSystem bool `db:"is_system"`
// CreatedAt and UpdatedAt are when the row was written and last changed, in
// UTC.
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
Group is a named set of actions, and the people who carry them.
It embeds the model, so a row returned by a query carries the connection and can be saved again. Build new rows through Groups: a struct literal has no connection and its write methods return model.ErrUnwired.
type GroupAction ¶
type GroupAction struct {
model.Model[GroupAction]
// ID is the identifier, generated by the application.
ID string `db:"id"`
// TenantID is the customer the row belongs to.
TenantID string `db:"tenant_id"`
// GroupID is the group this row attaches the action to.
GroupID string `db:"group_id"`
// Action is the action, in module.verb form, and it exists in the
// catalogue: nothing writes this column without checking that first.
Action string `db:"action"`
}
GroupAction is one action a group carries.
Action is a plain string and not the action type the policies take, because the column is read and written by the driver: a named type is one more thing between what the database holds and what compares it. It is converted at the two edges that care, and the conversion is checked against the catalogue before anything is written.
type GroupPage ¶
type GroupPage struct {
// Items are the groups, in slug order.
Items []GroupRef
// Next is what the next request passes back as a cursor, empty when this
// page is the last one.
Next string
}
GroupPage is a page of groups and where the next one resumes.
type GroupPageData ¶
type GroupPageData struct {
hview.Page
Prefix string
Group GroupRef
// System says the group cannot be deleted and cannot have actions taken
// off it, so the screen draws those controls as unavailable rather than
// offering a change the server refuses.
System bool
// Description is what the group is for.
Description string
// Sections are every action of the catalogue, by domain, each saying
// whether this group carries it.
Sections []ActionSection
// Members are who is in the group.
Members []string
}
GroupPageData is one group: what it carries, and who is in it.
type GroupPolicy ¶
type GroupPolicy struct{}
GroupPolicy decides who may administer a group.
It is the only authority over a Group, and it decides from what the subject carries: the effective actions filled in before any policy runs. A subject that carries nothing is refused every action here, which is the state an application starts in and the state it returns to when the last group that granted these actions is emptied.
type GroupQuery ¶
type GroupQuery struct {
// Search narrows the listing to groups whose slug or name contains it.
//
// The characters the pattern syntax reserves are removed before the term is
// used, so nothing typed into the box can become a wildcard. The statement
// carries no escape clause, and one engine of the three reads a backslash
// there as a literal backslash -- so escaping would mean a search that
// behaves differently depending on where the application is deployed.
Search string
// Cursor is where this page resumes, taken from the previous page's Next.
Cursor string
// Limit is how many rows the page holds. Zero means the default, and
// anything above the maximum is brought down to it.
Limit int
}
GroupQuery is what a listing asks for.
type GroupRef ¶
type GroupRef struct {
// ID addresses the group, Slug identifies it and Name is what a person
// reads.
ID string
Slug string
Name string
}
GroupRef is a group as something else refers to it: enough to name it on a screen, and no more.
type GroupUser ¶
type GroupUser struct {
model.Model[GroupUser]
// ID is the identifier, generated by the application.
ID string `db:"id"`
// TenantID is the customer the row belongs to.
TenantID string `db:"tenant_id"`
// GroupID is the group, and UserID is who is in it.
GroupID string `db:"group_id"`
UserID string `db:"user_id"`
}
GroupUser is one person in a group.
type GroupsPageData ¶
type GroupsPageData struct {
hview.Page
// Prefix is where this module answers, so the markup composes its own
// addresses instead of hard-coding one the configuration can change.
Prefix string
// Search is the term the listing was narrowed by, echoed back into the
// field so the box still says what is being looked at.
Search string
// Groups are the rows, and Next is the cursor of the following page, empty
// on the last one.
Groups []GroupRef
Next string
// Rejected holds the field errors of a create that was refused, so the form
// comes back with the reason on it rather than blank.
Rejected validation.Errors
}
GroupsPageData is the listing.
type MatrixCell ¶
MatrixCell is one square of the matrix: an action, and whether the row's group carries it.
type MatrixPageData ¶
type MatrixPageData struct {
hview.Page
Prefix string
Search string
Matrix MatrixView
}
MatrixPageData is the grid of groups against actions.
type MatrixRow ¶
type MatrixRow struct {
Group GroupRef
// System marks a group whose actions cannot be detached, so a screen can
// draw the squares as fixed rather than offering a change that is refused.
System bool
Cells []MatrixCell
}
MatrixRow is one group of the matrix and its squares, in the same order as the matrix's actions.
type MatrixView ¶
type MatrixView struct {
// Actions are the columns, sorted, and Domains is the same set grouped so
// that a screen can draw a heading over each run of columns.
Actions []security.Action
Domains []Domain
// Rows are the groups, in slug order, and Next is where the following page
// resumes.
Rows []MatrixRow
Next string
}
MatrixView is the group by permission grid.
type MemberPageData ¶
MemberPageData is one person's effective permissions and where each of them comes from.
type MembershipPolicy ¶
type MembershipPolicy struct{}
MembershipPolicy decides who may put a person into a group, take them out, and read the groups they belong to.
It sees who the row is about, which is what lets it refuse the shortest path to more power there is: adding yourself to a group. Everything else in this package would allow it -- the group is legitimate, the actions on it were granted by somebody who held them, and the person doing it may administer groups.
type Module ¶
type Module struct {
// contains filtered or unexported fields
}
Module is what the application registers.
It implements foundation.Module, which is Name and Routes and nothing else -- that pair is the whole public contract between a package and the framework.
It also implements foundation.Migratable, because it owns tables, and foundation.Publishable, because it hands view sources to the project.
func New ¶
func New(cfg Config, db *data.DB, sessions *security.SessionStore, csrf *security.CSRF) (*Module, error)
New returns the module, or the reason it cannot be built.
The collaborators are parameters and not fields somebody fills in afterwards: a module that could be registered half-wired is a module whose first request is the thing that reports the missing half.
It returns an error rather than panicking or carrying on, because everything it refuses is a wiring mistake, and a wiring mistake found at boot costs one restart. The same mistake found later is a request that reached a nil handle.
func (*Module) Boot ¶
Boot refuses to serve when a view this package renders is not in the binary.
A compiled view registers itself from init(), so by the time anything boots the question has one answer already: either the application published the files, compiled them and imported the package they became, or it did not. Asking here turns "did anybody run the install command" into one refusal at start-up that names the views and the command, instead of a 500 on the first request that reached one of them.
It also holds the destination. Every file the archive offers has to land under the vendor directory named after this module: an archive that reached resources/views/home.kyse.go would land on a page the application wrote, and what publishes the files writes what the archive says.
func (*Module) Migrations ¶
func (m *Module) Migrations() []foundation.Migration
Migrations declares the schema this module owns.
They are returned in the order their names sort in, which is the order they apply in: the name carries the order, and nothing else decides it.
func (*Module) Name ¶
Name is the module identifier: a lowercase slug, stable, no spaces.
It is what route listings group by and what the route names are prefixed with, so changing it changes addresses that other code has already written down.
func (*Module) Publishes ¶
func (m *Module) Publishes() []foundation.Publication
Publishes declares the files this package offers, each at the path it takes relative to the root of the project.
One tree, under one tag. A publication may carry a page, a component, a configuration file, a migration, a catalogue of sentences or an asset, and this package offers the first of those and nothing else. Each absence is a decision rather than an omission:
- configuration is the Config struct New is handed, checked by the compiler and validated before the module exists. A file copied into the project beside it would be a second place to say the same thing, and only one of the two could be the one the code reads.
- a stylesheet, a script or any other asset is registered with the view layer and served from an address derived from its own bytes. Copying one into the project would put a second copy of those bytes under a second address, and a page can only reference one of them.
- translations are overridden by writing the lines the application wants into its own vendor tree, which the catalogue loader already reads. A copy of every line this package ships is a copy that goes stale, and it goes stale without saying so.
- migrations are declared and collected, never copied. A copy in the project's own migration directory is found by the runner as well, so one schema change applies twice under two names.
What is left is the markup, and it is here for the reason the others are not: it is the one thing the project is expected to edit. A package cannot know what a screen should say in a product it has never seen.
func (*Module) Roles ¶
func (m *Module) Roles() fhttp.Middleware
Roles returns middleware that fills in what the acting subject may do.
Mount it after whatever establishes the session and before anything that consults a policy. Every policy in the application reads the actions it leaves on the subject, so a request that skipped it is a request where every policy sees a subject carrying nothing -- which refuses rather than admits, and is the direction a missing step has to fail in.
It carries the subject on the request's context. A handler reads it with auth.SubjectFrom, and one that loads the session itself instead gets the subject as it was stored, without the actions -- so the two are not interchangeable, and this is the one that has them.
A request with no session passes through untouched: there is nobody to resolve, and answering a visitor with a refusal here would refuse them the public pages too.
func (*Module) Routes ¶
Routes registers the module's routes under the configured prefix.
They are named, so a URL is built from a name rather than written out a second time somewhere else -- two spellings of one address disagree, and the failure when they do is a link to a 404.
Each one declares the action it requires. The declaration decides nothing: the handler still asks the service, the service still asks the policy, and the policy is still what refuses. What it is for is the catalogue -- a screen that lists what may be granted reads the router rather than a list beside it, and a list beside it is wrong the first time somebody adds a route.
The whole group runs behind the middleware that fills in what the subject may do, so the panel works whether or not the application mounted it globally. Mounting it twice costs nothing: the second pass finds the answer the first one remembered.
type PermissionService ¶
type PermissionService struct {
// contains filtered or unexported fields
}
PermissionService holds the rules of this package.
It receives its collaborators through the constructor. There is no container and no resolution by reflection: what this service is made of is written at the one place that builds it, and reading that place is how somebody learns what the package touches.
Everything a handler is allowed to do goes through here. The service is the only owner of the database handle, so the request layer cannot reach a Model before a policy has answered.
func NewPermissionService ¶
func NewPermissionService(db *data.DB, catalogue Catalogue) *PermissionService
NewPermissionService wires the service over the application's database handle and the catalogue its code declares.
func (*PermissionService) ActionsOf ¶
func (s *PermissionService) ActionsOf(ctx context.Context, actor security.Subject, groupID string) ([]security.Action, error)
ActionsOf returns the actions one group carries, sorted.
func (*PermissionService) Catalogue ¶
func (s *PermissionService) Catalogue() Catalogue
Catalogue returns the closed set of actions a group may carry.
It is the value the service was built with and it never changes: what a screen may offer and what a write may store are one set, read from one place.
func (*PermissionService) CreateGroup ¶
func (s *PermissionService) CreateGroup(ctx context.Context, actor security.Subject, in CreateGroupRequest) (*Group, error)
CreateGroup adds a group.
The candidate is authorized before it is stored, and the candidate is what the policy sees -- so a rule about what may be created is a rule about the group being created, and not about the person alone.
func (*PermissionService) DeleteGroup ¶
func (s *PermissionService) DeleteGroup(ctx context.Context, actor security.Subject, id string) error
DeleteGroup removes a group, the actions it carried and the memberships it held.
The three deletions and the version bump are one transaction. A group whose row is gone while its memberships survive is a group that grants nothing and cannot be found to be repaired.
func (*PermissionService) EffectiveFor ¶
func (s *PermissionService) EffectiveFor(ctx context.Context, actor security.Subject, userID string) (Effective, error)
EffectiveFor returns what one person may do, and which group gives them each of it.
The origin is the answer to the only question anybody asks a permissions screen twice: not what somebody has, but why. A list without it sends the person reading it through every group by hand.
func (*PermissionService) FindGroup ¶
func (s *PermissionService) FindGroup(ctx context.Context, actor security.Subject, id string) (*Group, error)
FindGroup returns one group, and asks the policy twice.
The first call is on the empty candidate, because there is no way to read the row without a Grant and no way to hold a Grant without a decision. What it decides is whether this subject may view groups at all.
The second call is on the row that came back, and it is the one a rule about the record itself depends on: the first call saw an empty value, so anything the policy says about which tenant owns the row never ran. The read is already scoped by tenant, so the second call is not what keeps customers apart. It is what keeps the policy honest.
func (*PermissionService) ListGroups ¶
func (s *PermissionService) ListGroups(ctx context.Context, actor security.Subject, q GroupQuery) (GroupPage, error)
ListGroups returns a page of groups.
It authorizes once, on the empty candidate, and the tenant filter in the statement is what bounds the rows. A policy call per row would be one call per record on a page and would still not narrow the query -- a listing that has to read a customer's rows in order to decide it may not read them has already read them.
func (*PermissionService) MembersOf ¶
func (s *PermissionService) MembersOf(ctx context.Context, actor security.Subject, groupID string) ([]string, error)
MembersOf returns who is in one group, sorted.
func (*PermissionService) PreviewActions ¶
func (s *PermissionService) PreviewActions(ctx context.Context, actor security.Subject, groupID string, wanted []security.Action) (Change, error)
PreviewActions reports what SetActions would change, and writes nothing.
It exists so that a screen can show the difference before it is applied, from the same computation the write uses. A summary produced by a second reading of the same intent is a summary that can disagree with what happens next.
func (*PermissionService) PreviewMembers ¶
func (s *PermissionService) PreviewMembers(ctx context.Context, actor security.Subject, groupID string, wanted []string) (Change, error)
PreviewMembers reports what SetMembers would change, and writes nothing.
func (*PermissionService) ResolveOwn ¶
func (s *PermissionService) ResolveOwn(ctx context.Context, actor security.Subject) (Resolution, error)
ResolveOwn returns the effective actions of the subject asking.
It is what fills in the actions every other policy reads, so it cannot itself depend on them: the policy admits a subject asking about its own rows and refuses every other question. That is the whole of the exception, and it is an exception to which rule applies rather than to whether one does -- the read is authorized, it is scoped by the tenant on the Grant, and it answers about one person.
func (*PermissionService) SetActions ¶
func (s *PermissionService) SetActions(ctx context.Context, actor security.Subject, groupID string, wanted []security.Action) (Change, error)
SetActions makes a group carry exactly these actions.
It is the only way an action is attached or detached, and that is deliberate: a per-action call beside it would be a second spelling of the same write, and the two would need the same authorization, the same catalogue check and the same version bump written twice. Attaching one action is this call with one more in the list.
Every added action is authorized on its own, so the rule that nobody grants what they do not hold is asked once per action rather than once per request.
func (*PermissionService) SetMembers ¶
func (s *PermissionService) SetMembers(ctx context.Context, actor security.Subject, groupID string, wanted []string) (Change, error)
SetMembers makes a group hold exactly these people.
Emptying a system group is refused, and it is refused here rather than in a policy because the policy is handed one row and the question is about how many are left. It is not an authorization refusal: the subject was allowed to do it, and what is refused is the state it would leave behind -- an application whose only group carrying the administration of permissions has nobody in it, and therefore nobody who could put anybody back.
func (*PermissionService) UpdateGroup ¶
func (s *PermissionService) UpdateGroup(ctx context.Context, actor security.Subject, id string, in UpdateGroupRequest) (*Group, error)
UpdateGroup changes what a group is called.
It does not change what the group carries. Nothing about the name or the description reaches an authorization decision, so this path never touches the version token: a rename is not a permission change and invalidating every resolved subject for one would be a stampede for nothing.
func (*PermissionService) Version ¶
Version returns the tenant's permission token.
It changes whenever anything that could change a decision changes, and it is compared for equality: a remembered resolution is still the answer while the token it was read at is still the token.
func (*PermissionService) ViewCatalogue ¶
func (s *PermissionService) ViewCatalogue(ctx context.Context, actor security.Subject) (CatalogueView, error)
ViewCatalogue returns the catalogue grouped by domain, with the groups that carry each action.
func (*PermissionService) ViewMatrix ¶
func (s *PermissionService) ViewMatrix(ctx context.Context, actor security.Subject, q GroupQuery) (MatrixView, error)
ViewMatrix returns a page of groups against every action of the catalogue.
type Resolution ¶
type Resolution struct {
// Version is the tenant's token at the moment the actions were read. It is
// compared for equality against the current one to decide whether a
// remembered answer is still the answer.
Version int64
// Roles are the effective actions, one entry per distinct action, sorted.
Roles []string
// Groups are the groups the subject belongs to, sorted by slug. They name
// the origin on a screen and are never read as authorization: a group is
// not a permission, and a decision taken on a group name would be a
// decision the catalogue never checked.
Groups []GroupRef
}
Resolution is what a request carries into every policy that runs after it: the effective actions of the acting subject, and the token they were read at.
type Resolver ¶
type Resolver struct {
// contains filtered or unexported fields
}
Resolver answers what the acting subject may do, and remembers the answer only while the tenant's token says it is still the answer.
The expensive half of the question -- the memberships, the groups, the actions those groups carry, and the assembly of the three -- is what is remembered. The cheap half, the tenant's token, is read every time, because what a remembered answer costs is the window between a permission being taken away and the person losing it. A remembered answer that outlives a revocation is the defect this whole mechanism exists to have none of, and a time-based expiry is exactly that defect with a number attached.
The memory is this process's. Another replica that changes a permission changes the token, and every process reads the token before it trusts what it remembers, so nothing has to be told about anything.
func NewResolver ¶
func NewResolver(service *PermissionService, limit int) *Resolver
NewResolver wires a resolver over the service, remembering at most limit subjects. A limit of zero or less means DefaultCacheSize.
func (*Resolver) Forget ¶
func (r *Resolver) Forget()
Forget drops every remembered answer.
It exists for the process that has just changed permissions and wants the next request in the same process to see them without waiting for anything. It is not what makes revocation correct -- the token is -- and nothing else calls it.
func (*Resolver) Resolve ¶
Resolve returns the effective actions of the subject.
It authorizes like everything else here: the subject asks about its own rows and the policy admits exactly that. A subject with no identifier is answered with nothing rather than with a query, because there is nobody to answer about.
type SummaryPageData ¶
type SummaryPageData struct {
// Prefix is where the confirmed write is sent.
Prefix string
Group GroupRef
// Kind is "actions" or "members", which is what decides where the
// confirmation goes and what the counts are called.
Kind string
// Change is the difference, from the same computation the write uses.
Change Change
// Fields are the values the confirmation resubmits, so that approving the
// summary sends the same request that produced it.
Fields []string
}
SummaryPageData is what a bulk write would change, drawn before it is applied.
It is a fragment: it extends no layout, because it is swapped into a page that already has one.
type UpdateGroupRequest ¶
type UpdateGroupRequest struct {
// Name and Description are what people read.
Name string
Description string
}
UpdateGroupRequest is what may be changed about an existing group.
The slug is absent on purpose. It is what a seed, a fixture and an operator name a group by, and changing it would leave every one of those pointing at nothing while the group carried on working for everybody else.
func (UpdateGroupRequest) Validate ¶
func (r UpdateGroupRequest) Validate() validation.Errors
Validate reports the errors per field.
type Version ¶
type Version struct {
model.Model[Version]
// TenantID is the customer, and the primary key.
TenantID string `db:"tenant_id"`
// Version is the token.
Version int64 `db:"version"`
}
Version is the token that changes whenever a tenant's permissions do.
One row per tenant, keyed by the tenant, so reading it is a point read on the primary key and writing it cannot produce a second row for the same customer.
The value is compared for equality and never ordered. It is not a count of changes and nothing may read it as one: two changes at once have to produce two different tokens, and a counter incremented from the same starting value twice produces one.