mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-25 08:42:16 +00:00
# Which Problems Are Solved There is no implementation of the permission framework in the new command pattern. We risk forgetting permission options as we are already migrating the service layer to the new pattern. # How the Problems Are Solved Use an permission checker interface, which will be a sub-interface of the Permission Repository. The permission checker is passed in the command executor and invoker as part of the command options. From here the implementation can call the correct permission method. This PR proposes a check method for each [level](https://github.com/zitadel/zitadel/wiki/Internal-AuthZ#levels). # Additional Changes - none # Additional Context - Draft design: https://github.com/zitadel/zitadel/wiki/Internal-AuthZ#enforcement - Related for queries: https://github.com/zitadel/zitadel/pull/10756
149 lines
4.2 KiB
Go
149 lines
4.2 KiB
Go
package domain
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
|
legacy_es "github.com/zitadel/zitadel/internal/eventstore"
|
|
)
|
|
|
|
// Commander is all that is needed to implement the command pattern.
|
|
// It is the interface all manipulations need to implement.
|
|
// If possible it should also be used for queries. We will find out if this is possible in the future.
|
|
type Commander interface {
|
|
Execute(ctx context.Context, opts *CommandOpts) (err error)
|
|
// Events returns the events that should be pushed to the event store after the command is executed.
|
|
// If the command does not produce events, it should return nil or an empty slice.
|
|
Events(ctx context.Context) []legacy_es.Command
|
|
fmt.Stringer
|
|
}
|
|
|
|
// Invoker is part of the command pattern.
|
|
// It is the interface that is used to execute commands.
|
|
type Invoker interface {
|
|
Invoke(ctx context.Context, command Commander, opts *CommandOpts) error
|
|
}
|
|
|
|
// CommandOpts are passed to each command
|
|
// it provides common fields used by commands like the database client.
|
|
type CommandOpts struct {
|
|
DB database.QueryExecutor
|
|
Invoker Invoker
|
|
Permissions PermissionChecker
|
|
}
|
|
|
|
type ensureTxOpts struct {
|
|
*database.TransactionOptions
|
|
}
|
|
|
|
type EnsureTransactionOpt func(*ensureTxOpts)
|
|
|
|
// EnsureTx ensures that the DB is a transaction. If it is not, it will start a new transaction.
|
|
// The returned close function will end the transaction. If the DB is already a transaction, the close function
|
|
// will do nothing because another [Commander] is already responsible for ending the transaction.
|
|
func (o *CommandOpts) EnsureTx(ctx context.Context, opts ...EnsureTransactionOpt) (close func(context.Context, error) error, err error) {
|
|
beginner, ok := o.DB.(database.Beginner)
|
|
if !ok {
|
|
// db is already a transaction
|
|
return func(_ context.Context, err error) error {
|
|
return err
|
|
}, nil
|
|
}
|
|
|
|
txOpts := &ensureTxOpts{
|
|
TransactionOptions: new(database.TransactionOptions),
|
|
}
|
|
for _, opt := range opts {
|
|
opt(txOpts)
|
|
}
|
|
|
|
tx, err := beginner.Begin(ctx, txOpts.TransactionOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
o.DB = tx
|
|
|
|
return func(ctx context.Context, err error) error {
|
|
return tx.End(ctx, err)
|
|
}, nil
|
|
}
|
|
|
|
// EnsureClient ensures that the o.DB is a client. If it is not, it will get a new client from the [database.Pool].
|
|
// The returned close function will release the client. If the o.DB is already a client or transaction, the close function
|
|
// will do nothing because another [Commander] is already responsible for releasing the client.
|
|
func (o *CommandOpts) EnsureClient(ctx context.Context) (close func(_ context.Context) error, err error) {
|
|
pool, ok := o.DB.(database.Pool)
|
|
if !ok {
|
|
// o.DB is already a client
|
|
return func(_ context.Context) error {
|
|
return nil
|
|
}, nil
|
|
}
|
|
client, err := pool.Acquire(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
o.DB = client
|
|
return func(ctx context.Context) error {
|
|
return client.Release(ctx)
|
|
}, nil
|
|
}
|
|
|
|
func (o *CommandOpts) Invoke(ctx context.Context, command Commander) error {
|
|
if o.Invoker == nil {
|
|
return command.Execute(ctx, o)
|
|
}
|
|
return o.Invoker.Invoke(ctx, command, o)
|
|
}
|
|
|
|
func DefaultOpts(invoker Invoker) *CommandOpts {
|
|
if invoker == nil {
|
|
invoker = &noopInvoker{}
|
|
}
|
|
return &CommandOpts{
|
|
DB: pool,
|
|
Invoker: invoker,
|
|
Permissions: &noopPermissionChecker{}, // prevent panics for now
|
|
}
|
|
}
|
|
|
|
// commandBatch is a batch of commands.
|
|
// It uses the [Invoker] provided by the opts to execute each command.
|
|
type commandBatch struct {
|
|
Commands []Commander
|
|
}
|
|
|
|
// Events implements Commander.
|
|
func (cmd *commandBatch) Events(ctx context.Context) []legacy_es.Command {
|
|
commands := make([]legacy_es.Command, 0, len(cmd.Commands))
|
|
for _, c := range cmd.Commands {
|
|
if e := c.Events(ctx); len(e) > 0 {
|
|
commands = append(commands, e...)
|
|
}
|
|
}
|
|
return commands
|
|
}
|
|
|
|
func BatchCommands(cmds ...Commander) *commandBatch {
|
|
return &commandBatch{
|
|
Commands: cmds,
|
|
}
|
|
}
|
|
|
|
// String implements [Commander].
|
|
func (cmd *commandBatch) String() string {
|
|
return "commandBatch"
|
|
}
|
|
|
|
func (b *commandBatch) Execute(ctx context.Context, opts *CommandOpts) (err error) {
|
|
for _, cmd := range b.Commands {
|
|
if err = opts.Invoke(ctx, cmd); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var _ Commander = (*commandBatch)(nil)
|