Documentation
¶
Overview ¶
Package users holds the user providers that read accounts out of a database.
A user provider is the seam between a guard and wherever the users are kept. A guard knows how a session or a token proves identity; a provider knows how to find the row and how to check the password. Both providers here implement auth.UserProvider, which is declared in the parent package with its other contracts.
Authorization, which is the question a reviewer asks first ¶
A provider runs at sign-in, where there is no subject yet -- establishing one is what is about to happen -- so there is no Policy to run and nothing to inherit a Grant from. Every statement in this package is therefore taken under auth.SystemGrant, under RetrieveUser or UpdateUser.
The tenant that grant carries is the provider's, from configuration, fixed when the application was wired. It is never read off the request: not from a header, not from a subdomain, not from the credentials map. A provider whose tenant came in with the request would let an anonymous caller choose whose users the sign-in form searches.
Reads are authorized and scoped exactly as writes are. Every statement below -- RetrieveByID, RetrieveByToken and RetrieveByCredentials as much as the two updates -- goes through the same Grant and the same tenant filter.
Two shapes worth naming ¶
- The model is a constructor function, func() auth.Authenticatable, rather than a type named in a string: Go cannot construct a type from a name at run time. The query that model would have opened comes in beside it, for the same reason: an interface value has no query of its own.
- A row becomes a user through Hydratable, which is where the columns are written onto the value the constructor made.
Index ¶
- Constants
- Variables
- type Connection
- type DatabaseUserProvider
- func (p *DatabaseUserProvider) RehashPasswordIfRequired(ctx context.Context, user auth.Authenticatable, credentials map[string]any, ...) error
- func (p *DatabaseUserProvider) RetrieveByCredentials(ctx context.Context, credentials map[string]any) (auth.Authenticatable, error)
- func (p *DatabaseUserProvider) RetrieveByID(ctx context.Context, identifier any) (auth.Authenticatable, error)
- func (p *DatabaseUserProvider) RetrieveByToken(ctx context.Context, identifier any, token string) (auth.Authenticatable, error)
- func (p *DatabaseUserProvider) UpdateRememberToken(ctx context.Context, user auth.Authenticatable, token string) error
- func (p *DatabaseUserProvider) ValidateCredentials(_ context.Context, user auth.Authenticatable, credentials map[string]any) bool
- type Fillable
- type GenericUser
- func (u *GenericUser) GetAuthIdentifier() any
- func (u *GenericUser) GetAuthIdentifierName() string
- func (u *GenericUser) GetAuthPassword() string
- func (u *GenericUser) GetAuthPasswordName() string
- func (u *GenericUser) GetRememberToken() string
- func (u *GenericUser) GetRememberTokenName() string
- func (u *GenericUser) SetRememberToken(token string)
- type Hydratable
- type ModelUserProvider
- func (p *ModelUserProvider) CreateModel() auth.Authenticatable
- func (p *ModelUserProvider) GetHasher() auth.Hasher
- func (p *ModelUserProvider) GetModel() func() auth.Authenticatable
- func (p *ModelUserProvider) GetQueryCallback() func(*query.Builder)
- func (p *ModelUserProvider) RehashPasswordIfRequired(ctx context.Context, user auth.Authenticatable, credentials map[string]any, ...) error
- func (p *ModelUserProvider) RetrieveByCredentials(ctx context.Context, credentials map[string]any) (auth.Authenticatable, error)
- func (p *ModelUserProvider) RetrieveByID(ctx context.Context, identifier any) (auth.Authenticatable, error)
- func (p *ModelUserProvider) RetrieveByToken(ctx context.Context, identifier any, token string) (auth.Authenticatable, error)
- func (p *ModelUserProvider) SetHasher(hasher auth.Hasher) *ModelUserProvider
- func (p *ModelUserProvider) SetModel(model func() auth.Authenticatable) *ModelUserProvider
- func (p *ModelUserProvider) UpdateRememberToken(ctx context.Context, user auth.Authenticatable, token string) error
- func (p *ModelUserProvider) ValidateCredentials(_ context.Context, user auth.Authenticatable, credentials map[string]any) bool
- func (p *ModelUserProvider) WithQuery(queryCallback func(*query.Builder)) *ModelUserProvider
Constants ¶
const ( // RetrieveUser is the action every read in this package is authorized // under: RetrieveByID, RetrieveByToken and RetrieveByCredentials. RetrieveUser auth.Action = "user.retrieve" // UpdateUser is the action the two writes are authorized under: // UpdateRememberToken and RehashPasswordIfRequired. UpdateUser auth.Action = "user.update" )
The actions the providers in this package authorize their own statements under.
A user provider runs at the moment somebody is proving who they are, so there is no subject to authorize yet and no Policy that could be asked about one. That is what auth.SystemGrant is for, and these are the two names it is asked under: reading a user, and writing the two columns a sign-in may rewrite -- the remember token and the password hash.
They are constants and not configuration. An action a caller could choose would be an action a caller could widen, and `aru doctor` audits system grants by the name they are taken under.
Variables ¶
var ErrNoPassword = errors.New("users: the credentials carry no password")
ErrNoPassword is what RehashPasswordIfRequired answers when the credentials it was handed carry no password to hash.
Rehashing happens right after a sign-in that already proved a plain password, so an absent one means the caller passed different credentials to the check and to the rehash.
var ErrNotHydratable = errors.New("users: the model cannot be filled from a row (it does not implement users.Hydratable)")
ErrNotHydratable is what a provider answers when the value its model constructor returned cannot be filled from a row.
It is the one thing this package cannot check at compile time. The constructor is declared as func() auth.Authenticatable because that is the contract a guard consumes, and filling a value from a row is not part of that contract, so it is asked for by assertion instead. Wire a user type that embeds *model.Model[T] -- or write the two methods -- and this never fires.
Functions ¶
This section is empty.
Types ¶
type Connection ¶
type Connection interface {
// Table opens a builder against a table. It takes a context because a
// hesape builder binds its connection at the moment it is made.
Table(table any, as ...string) *query.Builder
}
Connection is the part of a database connection a user provider asks for: a query builder against one table.
It is declared here, with its consumer, rather than imported from the database package -- the same choice query.Connection makes, and for the same reason. A provider needs one method of a connection, and depending on the whole interface to get it is a dependency the wiring cannot substitute in a test.
*database.Connection satisfies it as written.
type DatabaseUserProvider ¶
type DatabaseUserProvider struct {
// contains filtered or unexported fields
}
DatabaseUserProvider is the auth.UserProvider that reads a table directly and wraps each row in a GenericUser.
It is ModelUserProvider with the model taken out. There is no user type to construct and nothing to hydrate, which is what it is for: an application that has not declared a user struct, or a second table of users that does not deserve one.
Everything ModelUserProvider says about the Grant is true here. Every statement is taken under auth.SystemGrant, because a provider runs before there is a subject to authorize; the action is RetrieveUser or UpdateUser; and the tenant comes from configuration, never from the request. Reads are scoped exactly as writes are.
func NewDatabaseUserProvider ¶
func NewDatabaseUserProvider(connection Connection, hasher auth.Hasher, table, tenant string) *DatabaseUserProvider
NewDatabaseUserProvider returns a provider over the named table.
The fourth argument is the tenant every statement is filtered by; see the type's doc for where it must come from.
func (*DatabaseUserProvider) RehashPasswordIfRequired ¶
func (p *DatabaseUserProvider) RehashPasswordIfRequired(ctx context.Context, user auth.Authenticatable, credentials map[string]any, force bool) error
RehashPasswordIfRequired upgrades a hash that was made with weaker parameters than the ones in force.
There is no model to write through, so the column is written by the statement, and the GenericUser it was handed is updated so that the rest of the request reads the new hash.
func (*DatabaseUserProvider) RetrieveByCredentials ¶
func (p *DatabaseUserProvider) RetrieveByCredentials(ctx context.Context, credentials map[string]any) (auth.Authenticatable, error)
RetrieveByCredentials finds the account these credentials name.
Every key holding the word "password" is dropped before a clause is built, and credentials that are nothing but password keys match nobody rather than matching the first row in the table.
func (*DatabaseUserProvider) RetrieveByID ¶
func (p *DatabaseUserProvider) RetrieveByID(ctx context.Context, identifier any) (auth.Authenticatable, error)
RetrieveByID finds the account with this identifier.
A nil user and a nil error mean nobody has that identifier.
func (*DatabaseUserProvider) RetrieveByToken ¶
func (p *DatabaseUserProvider) RetrieveByToken(ctx context.Context, identifier any, token string) (auth.Authenticatable, error)
RetrieveByToken is the user behind a "remember me" cookie.
The comparison is constant time, and an empty stored token never matches.
func (*DatabaseUserProvider) UpdateRememberToken ¶
func (p *DatabaseUserProvider) UpdateRememberToken(ctx context.Context, user auth.Authenticatable, token string) error
UpdateRememberToken writes a new remember token for this account.
The instance the caller holds is updated as well as the row, which matters the moment a guard sets a cookie from the user it just updated. ModelUserProvider does both too.
func (*DatabaseUserProvider) ValidateCredentials ¶
func (p *DatabaseUserProvider) ValidateCredentials(_ context.Context, user auth.Authenticatable, credentials map[string]any) bool
ValidateCredentials reports whether these credentials belong to this account. It is ModelUserProvider.ValidateCredentials, unchanged.
type Fillable ¶
type Fillable interface {
// ForceFill merges the attributes into the value, past any guard on them.
ForceFill(attributes map[string]any) error
}
Fillable is what RehashPasswordIfRequired needs to put the new hash on the user it was handed.
It is separate from Hydratable because the two do different things: SetRawAttributes replaces the row, ForceFill merges into it. Rehashing with the first would leave a user holding nothing but a password.
A user type that does not implement it is still rehashed in storage; only the instance in memory keeps the old hash, and it is about to be replaced by the next read anyway.
type GenericUser ¶
type GenericUser struct {
// Attributes is every column of the row.
//
// It is exported so that the columns beyond the ones the contract names are
// reachable at all: index the map to read one, assign to write one.
Attributes map[string]any
}
GenericUser is a user that is nothing but the row it came from.
It is what DatabaseUserProvider hands back. There is no user type to construct, because the point of that provider is not having one -- so the columns stay a map and the four auth.Authenticatable names are read out of it.
It lives in this package rather than beside the guards because the provider below is the only thing that makes one, and a type with a single producer belongs with it.
func NewGenericUser ¶
func NewGenericUser(attributes map[string]any) *GenericUser
NewGenericUser returns a user over the row.
func (*GenericUser) GetAuthIdentifier ¶
func (u *GenericUser) GetAuthIdentifier() any
GetAuthIdentifier is the id itself.
func (*GenericUser) GetAuthIdentifierName ¶
func (u *GenericUser) GetAuthIdentifierName() string
GetAuthIdentifierName is the column holding the id: "id".
func (*GenericUser) GetAuthPassword ¶
func (u *GenericUser) GetAuthPassword() string
GetAuthPassword is the hash, never the plain password.
The contract says string, so a column that is not one reads as empty -- which every caller already treats as "this account has no password", and which is the safe reading of a column nobody can compare a hash against.
func (*GenericUser) GetAuthPasswordName ¶
func (u *GenericUser) GetAuthPasswordName() string
GetAuthPasswordName is the column holding the hash: "password".
func (*GenericUser) GetRememberToken ¶
func (u *GenericUser) GetRememberToken() string
GetRememberToken is the token the "remember me" cookie is checked against.
func (*GenericUser) GetRememberTokenName ¶
func (u *GenericUser) GetRememberTokenName() string
GetRememberTokenName is the column holding it: "remember_token".
func (*GenericUser) SetRememberToken ¶
func (u *GenericUser) SetRememberToken(token string)
SetRememberToken writes that token onto the row.
type Hydratable ¶
type Hydratable interface {
// SetRawAttributes writes the row onto the value. sync decides whether the
// original attributes are synced, and hydration passes true.
SetRawAttributes(attributes map[string]any, sync bool) error
}
Hydratable is the one thing a user provider needs of a user type that auth.Authenticatable does not declare: filling an instance from one row.
The query builder answers with a query.Record, so the step from row to user has to be somewhere, and this is it.
The signature is hesape/database/model's, unchanged, so a user type that embeds *model.Model[T] satisfies this by embedding it.
type ModelUserProvider ¶ added in v0.15.0
type ModelUserProvider struct {
// contains filtered or unexported fields
}
ModelUserProvider is the auth.UserProvider that finds users through the application's own user type.
Where the Grant comes from, which is the first question to ask of this file ¶
A user provider runs at sign-in. There is no subject yet -- proving who somebody is is what is about to happen -- so there is no Policy to run and no Grant to inherit. Every statement here is therefore taken under auth.SystemGrant, with RetrieveUser or UpdateUser as the action.
The tenant that grant carries comes from the provider, which got it from configuration when the application was wired. It never comes from the request -- not from a header, not from a subdomain the handler parsed, not from the credentials map. A tenant taken off the request would let a caller name whose users the sign-in form searches, which is every account in the system reachable from an unauthenticated endpoint.
The read paths are authorized exactly as the writes are: RetrieveByID and RetrieveByCredentials hold a Grant and are filtered by its tenant, because a query without one reads every customer's users.
An application that serves several tenants builds one provider per tenant. A provider is four fields and holds no connection state of its own.
What stands in for the model, and why ¶
Go cannot construct a type from a name in a string, so the model is a constructor function the wiring supplies -- func() auth.Authenticatable.
The query that model would have opened comes in the same way. An auth.Authenticatable is an interface value, and an interface value has no connection, table or scopes behind it, so the wiring hands over the query factory next to the constructor.
func NewModelUserProvider ¶ added in v0.15.0
func NewModelUserProvider( hasher auth.Hasher, model func() auth.Authenticatable, newQuery func(ctx context.Context) *query.Builder, tenant string, ) *ModelUserProvider
NewModelUserProvider returns a provider over the user type model builds.
model constructs one, newQuery opens a statement against the table it lives in, and tenant is what every statement is filtered by. See the type's doc for why the first two are separate arguments.
func (*ModelUserProvider) CreateModel ¶ added in v0.15.0
func (p *ModelUserProvider) CreateModel() auth.Authenticatable
CreateModel is a fresh instance of the user type, with nothing filled in.
It calls the constructor the provider was given, for the reason in the type's doc.
func (*ModelUserProvider) GetHasher ¶ added in v0.15.0
func (p *ModelUserProvider) GetHasher() auth.Hasher
GetHasher is the hasher this provider checks passwords with.
func (*ModelUserProvider) GetModel ¶ added in v0.15.0
func (p *ModelUserProvider) GetModel() func() auth.Authenticatable
GetModel is the constructor this provider builds a user type with.
func (*ModelUserProvider) GetQueryCallback ¶ added in v0.15.0
func (p *ModelUserProvider) GetQueryCallback() func(*query.Builder)
GetQueryCallback is the callback every retrieval query runs through, or nil.
func (*ModelUserProvider) RehashPasswordIfRequired ¶ added in v0.15.0
func (p *ModelUserProvider) RehashPasswordIfRequired(ctx context.Context, user auth.Authenticatable, credentials map[string]any, force bool) error
RehashPasswordIfRequired upgrades a hash that was made with weaker parameters than the ones in force.
It runs on a sign-in that has already proved the plain password, which is the only moment the plain password exists and the whole reason this is possible at all. A hash that already meets the parameters is left alone unless force says otherwise, and that is the common case -- so the statement below is not issued on an ordinary sign-in.
The column is written by the statement, and the instance in memory is updated when the user type can be filled -- see Fillable. A user type that cannot keeps the old hash in memory for the rest of the request, and the row is correct either way.
func (*ModelUserProvider) RetrieveByCredentials ¶ added in v0.15.0
func (p *ModelUserProvider) RetrieveByCredentials(ctx context.Context, credentials map[string]any) (auth.Authenticatable, error)
RetrieveByCredentials finds the account these credentials name.
Every key holding the word "password" is dropped before a clause is built, so no statement this method issues ever compares a password. Credentials that are nothing but password keys match nobody: the alternative is a query with no where clause, which would answer with the first user in the table.
func (*ModelUserProvider) RetrieveByID ¶ added in v0.15.0
func (p *ModelUserProvider) RetrieveByID(ctx context.Context, identifier any) (auth.Authenticatable, error)
RetrieveByID finds the account with this identifier.
A nil user and a nil error mean nobody has that identifier. An error means the statement failed, which is a different outcome and reads differently at the call site.
func (*ModelUserProvider) RetrieveByToken ¶ added in v0.15.0
func (p *ModelUserProvider) RetrieveByToken(ctx context.Context, identifier any, token string) (auth.Authenticatable, error)
RetrieveByToken is the user behind a "remember me" cookie.
The row is found by identifier and only then is the token compared, in constant time. A row whose remember token is empty never matches, so a user who has never ticked the box cannot be signed in with an empty cookie.
func (*ModelUserProvider) SetHasher ¶ added in v0.15.0
func (p *ModelUserProvider) SetHasher(hasher auth.Hasher) *ModelUserProvider
SetHasher replaces it, and returns the provider.
func (*ModelUserProvider) SetModel ¶ added in v0.15.0
func (p *ModelUserProvider) SetModel(model func() auth.Authenticatable) *ModelUserProvider
SetModel replaces it, and returns the provider.
func (*ModelUserProvider) UpdateRememberToken ¶ added in v0.15.0
func (p *ModelUserProvider) UpdateRememberToken(ctx context.Context, user auth.Authenticatable, token string) error
UpdateRememberToken writes a new remember token for this account.
The statement writes one column and never mentions updated_at, so being remembered does not look like the account was edited.
The instance the caller holds is updated too.
func (*ModelUserProvider) ValidateCredentials ¶ added in v0.15.0
func (p *ModelUserProvider) ValidateCredentials(_ context.Context, user auth.Authenticatable, credentials map[string]any) bool
ValidateCredentials reports whether these credentials belong to this account.
A missing password, and a user whose password column is empty, are both false. The empty column matters: a row created by an invite flow that has never had a password set must not be signed in to by offering an empty one.
A password that is not a string is coerced rather than refused, and an empty one is handed to the hasher rather than refused early -- see [passwordOf].
The context is unused because nothing here touches storage; it is on the signature because auth.UserProvider declares it, so that a provider which does need one can be written without changing the contract.
func (*ModelUserProvider) WithQuery ¶ added in v0.15.0
func (p *ModelUserProvider) WithQuery(queryCallback func(*query.Builder)) *ModelUserProvider
WithQuery sets the callback that modifies every retrieval query -- a soft-delete filter, an "active only" clause -- and returns the provider.
It does not reach the two writes. A nil callback clears it.