Skip to content

Reference

AuthZ

Reference for go-sequent authorization at the command boundary: security.Authorizer, AllowAll, DenyByDefault, WithAuthorizer filters, Middleware, ErrDenied and the AuthContext trust model.

AuthZ

Package: security

go-sequent checks authorization at the command boundary, before a handler runs. Authentication (who is this?) is the job of your host application. AuthZ (may they do this?) is a hook in the write path.

The interface

type Authorizer interface {
    Authorize(ctx context.Context, cmd command.Command) error
}

Return nil to allow. Return an error to deny. Denials from the built-in authorizers wrap security.ErrDenied:

err := app.Service.Execute(ctx, cmd)
if errors.Is(err, security.ErrDenied) {
    // 403 in your API layer
}

security.AuthorizerFunc adapts a plain function.

Built-in authorizers

Authorizer Behaviour
security.AllowAll{} Allows every command. For tests and demos.
security.DenyByDefault{Allow: map[string]bool{...}} Allows only listed command types. Everything else returns ErrDenied.
app := sequent.New(cfg, sequent.WithAuthorizer(security.DenyByDefault{
    Allow: map[string]bool{
        "bank.Deposit": true,
        "bank.Withdraw": true,
    },
}))

For financial applications, start from DenyByDefault. A new command type is then blocked until someone adds it to the list on purpose.

bank.AllowBankCommands() in the example returns a DenyByDefault that allows the four bank command types.

Filter or middleware

There are two ways to install an authorizer:

Way Where it runs
sequent.WithAuthorizer(auth) Adds security.Filter{Auth: auth} to Service.Filters. Runs after the middleware chain, before Validate.
security.Middleware(auth) A command.Middleware. Runs at its position in the chain.

The difference matters when other middleware can short-circuit. The idempotency middleware returns early for a replayed key, so a filter does not run on replays. If you need authorization to run on every attempt, add security.Middleware(auth) ahead of idempotency, and after tenancy if the authorizer needs the tenant:

app.Service.Middleware = append(
    []command.Middleware{tenancy.Middleware, security.Middleware(auth)},
    app.Service.Middleware...,
)

What an authorizer can see

The Command and its Meta: TenantID, UserID, AuthContext, Source. It also gets the context.Context, so you can read values set by your HTTP layer.

Do not trust a UserID set by a client. Verify the caller in your application first, then set Meta.UserID and Meta.AuthContext from verified claims. go-sequent treats AuthContext as opaque; your authorizer decides what it means.

auth := security.AuthorizerFunc(func(ctx context.Context, cmd command.Command) error {
    m := cmd.Meta()
    claims, ok := m.AuthContext.(*MyClaims)
    if !ok || claims.TenantID != m.TenantID {
        return fmt.Errorf("%w: %s", security.ErrDenied, cmd.CommandType())
    }
    if !claims.Can(cmd.CommandType()) {
        return fmt.Errorf("%w: %s", security.ErrDenied, cmd.CommandType())
    }
    return nil
})

Aggregate-level checks

Rules like “only the account owner can withdraw” depend on aggregate state. Put them in aggregate methods, using only aggregate state and claims carried on the command. Do not run ad-hoc database queries from the aggregate.

Audit trail

Every successful command is stored with its tenant, user, idempotency key, correlation and causation ids and source. command.AuditOf(cmd) builds the same data as a struct. Combine it with the event stream and the hash chain for a record of who did what.

What AuthZ is not

  • It is not authentication.
  • It does not sign events. go-sequent has no event signatures; integrity is the hash chain.
  • It does not enforce tenant isolation by itself. Tenancy middleware rejects a command whose tenant differs from the context. See Tenancy.

Docs track the go-sequent library. When APIs drift, the source wins.