modernice/goes

Authorization System

Closed this issue · 0 comments

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:

  • Actions
  • Actors
  • Roles

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 { ... })
}