Provide a generic permission system that can be used by applications to grant and revoke aggregate-specific permissions from users.
Background
Many applications need some kind of authorization system that grants specific users permissions to do some kinds of actions. There exist many different authorization patterns and libraries out in the wild that can and should be used if the authorization requirements are more complex than this RFC tries to solve for.
Goal of this RFC is to implement a permission system for goes aggregates where users are granted permission to perform specific actions against specific aggregate instances. This should cover authorization needs for most simple to medium-complex apps.
Example
Given an ecommerce app where a customer makes an order for some products. Backend users of the ecommerce app need several different permissions to act on the order (view, delete, update etc.). The customer also needs permissions to view and update the order but has no user account. Backend users should get access to the order solely through their role, while the customer needs access through some kind of secret that is provided to the customer in the order mails. Based on this, the app requires
- role-based authorization for backend users, and
- action-based authorization for "API key / secret key" users.
Proposal
Proposal is to implement an authorization system that consists of three concepts:
An action represents any kind of action that can be performed against an aggregate. Permissions to perform an action are granted to actors and roles.
An actor represents a user of the application, which can be of any type (real-world human, system user, API key etc.).
A role represents a group of actions that are granted to actors. An actor that has a role may perform any action that was granted to that role.
Actors
An Actor
represents a user within the system. An actor can be anything, from a real-world human to a system user to an API key.
Example: Account User
package example
func example(userID, orderID uuid.UUID) {
actor := auth.NewActor(userID)
actor.Grant("order", orderID, "view", "update", "...")
}
Example: System User
package example
func example(orderID uuid.UUID) {
actor := auth.NewActor(uuid.New())
actor.Grant("order", orderID, "*") // grant all actions
actor.Grant("order", uuid.Nil, "*") // grant all actions on all orders
actor.Grant("*", uuid.Nil, "*") // grant everything
}
Example: Secret Key User
package example
func example(secret string, orderID uuid.UUID) {
actor := auth.NewStringActor(uuid.New())
// string-actors must be identified before they can be granted permissions
err := actor.Grant("order", orderID, "view")
errors.Is(err, auth.ErrMissingID) == true
actor.Identify(secret)
actor.Grant("order", orderID, "view")
}
Roles
A Role
grants a group of actors the permission to perform an arbitrary amount of actions.
package example
const AdminRole = "admin"
func example(orderID uuid.UUID) {
role := auth.NewRole(uuid.New())
// first, a role must be given a name
role.Identify("admin")
// then it can be granted permissions
role.Grant("order", orderID, "view", "update", "...")
}
Actors are added to and removed from roles:
package example
func example(r *auth.Role, actors []uuid.UUID) {
r.Add(actors...)
r.Remove(actors...)
}
Permissions
Permissions
is a read-model that projects the permissions of a specific actor from actor events and role events.
package example
func example(actorID uuid.UUID, orderID) {
perms := auth.NewPermissions(actorID)
// apply projection ...
canView := perms.Allows("view", "order", orderID)
cannotView := perms.Disallows("view", "order", orderID)
}
HTTP Middleware
HTTP middleware that can be used to add authorization to HTTP APIs is provided.
package example
func example(perms auth.PermissionRepository) {
// create middleware that attaches the id of the current actor to the request context
mw := middleware.Authorize(func(auth middleware.Authorizer, r *http.Request) {
// auth.Authorize() may be called multiple times to authorize multiple actors for the current request
auth.Authorize(auth.NewUUIDActor(<extract-user-id-from-request>))
// middleware.Authorizer provides lookup for ids of actors other than uuid-Actors
sid := "<extract-string-id-from-request>"
id, err := auth.Lookup(sid)
auth.Authorize(auth.NewStringActor(id))
})
// create middleware that extracts the actor id from the "fooId" JSON field of the request body
// as a UUID and attaches it to the request context
mw := middleware.AuthorizeField("fooId", uuid.Parse, auth.NewUUIDActor)
// create middleware for the "view" action of the aggregate returned by the passed function.
// if any of the authorized actors is allowed to do the action, the middleware calls the handler
mw := middleware.Permission(perms, "view", func(r *http.Request) aggregate.Ref {
return aggregate.Ref{Name: "foo", ID: "<extract-from-request>"}
})
// create middleware for the "view" action of the "foo" aggregate. the id of the aggregate is
// extracted from the "fooId" JSON field of the request body
mw := middleware.PermissionField(perms, "view", "foo", "fooId")
// create middleware factory to avoid having to pass the PermissionRepository for each middleware
f := middleware.NewFactory(perms)
mw := f.Permission("view", func(r *http.Request) aggregate.Ref { ... })
}