From bff0db4be498f93fceb64edbc52637d06ea607b5 Mon Sep 17 00:00:00 2001 From: Stefan Benz Date: Mon, 24 Oct 2022 16:04:52 +0200 Subject: [PATCH] fix(instance): update logic for pats and machinekeys --- internal/api/grpc/management/user.go | 33 +-- .../api/grpc/management/user_converter.go | 45 +++- internal/command/instance.go | 46 ++-- internal/command/org.go | 45 ++-- internal/command/user.go | 11 + internal/command/user_machine_key.go | 244 +++++++++++++----- .../command/user_personal_access_token.go | 218 +++++++++++----- internal/domain/application_key.go | 8 +- internal/domain/authn_key.go | 8 +- internal/domain/expiration.go | 8 +- internal/domain/machine_key.go | 26 +- 11 files changed, 474 insertions(+), 218 deletions(-) diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 22f051189a..1cf1151978 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -2,7 +2,6 @@ package management import ( "context" - "time" "github.com/zitadel/logging" "github.com/zitadel/oidc/v2/pkg/oidc" @@ -725,27 +724,24 @@ func (s *Server) ListMachineKeys(ctx context.Context, req *mgmt_pb.ListMachineKe } func (s *Server) AddMachineKey(ctx context.Context, req *mgmt_pb.AddMachineKeyRequest) (*mgmt_pb.AddMachineKeyResponse, error) { - key, err := s.command.AddUserMachineKey(ctx, AddMachineKeyRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + machineKey := AddMachineKeyRequestToCommand(req, authz.GetCtxData(ctx).OrgID) + details, err := s.command.AddUserMachineKey(ctx, machineKey) if err != nil { return nil, err } - keyDetails, err := key.Detail() + keyDetails, err := machineKey.Detail() if err != nil { return nil, err } return &mgmt_pb.AddMachineKeyResponse{ - KeyId: key.KeyID, + KeyId: machineKey.KeyID, KeyDetails: keyDetails, - Details: obj_grpc.AddToDetailsPb( - key.Sequence, - key.ChangeDate, - key.ResourceOwner, - ), + Details: obj_grpc.DomainToChangeDetailsPb(details), }, nil } func (s *Server) RemoveMachineKey(ctx context.Context, req *mgmt_pb.RemoveMachineKeyRequest) (*mgmt_pb.RemoveMachineKeyResponse, error) { - objectDetails, err := s.command.RemoveUserMachineKey(ctx, req.UserId, req.KeyId, authz.GetCtxData(ctx).OrgID) + objectDetails, err := s.command.RemoveUserMachineKey(ctx, RemoveMachineKeyRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } @@ -788,28 +784,21 @@ func (s *Server) ListPersonalAccessTokens(ctx context.Context, req *mgmt_pb.List } func (s *Server) AddPersonalAccessToken(ctx context.Context, req *mgmt_pb.AddPersonalAccessTokenRequest) (*mgmt_pb.AddPersonalAccessTokenResponse, error) { - expDate := time.Time{} - if req.ExpirationDate != nil { - expDate = req.ExpirationDate.AsTime() - } scopes := []string{oidc.ScopeOpenID, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner} - pat, token, err := s.command.AddPersonalAccessToken(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, expDate, scopes, domain.UserTypeMachine) + pat := AddPersonalAccessTokenRequestToCommand(req, authz.GetCtxData(ctx).OrgID, scopes, domain.UserTypeMachine) + details, err := s.command.AddPersonalAccessToken(ctx, pat) if err != nil { return nil, err } return &mgmt_pb.AddPersonalAccessTokenResponse{ TokenId: pat.TokenID, - Token: token, - Details: obj_grpc.AddToDetailsPb( - pat.Sequence, - pat.ChangeDate, - pat.ResourceOwner, - ), + Token: pat.Token, + Details: obj_grpc.DomainToChangeDetailsPb(details), }, nil } func (s *Server) RemovePersonalAccessToken(ctx context.Context, req *mgmt_pb.RemovePersonalAccessTokenRequest) (*mgmt_pb.RemovePersonalAccessTokenResponse, error) { - objectDetails, err := s.command.RemovePersonalAccessToken(ctx, req.UserId, req.TokenId, authz.GetCtxData(ctx).OrgID) + objectDetails, err := s.command.RemovePersonalAccessToken(ctx, RemovePersonalAccessTokenRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go index 63b2c44b06..65ed479178 100644 --- a/internal/api/grpc/management/user_converter.go +++ b/internal/api/grpc/management/user_converter.go @@ -7,6 +7,7 @@ import ( "github.com/zitadel/logging" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/pkg/grpc/user" "github.com/zitadel/zitadel/internal/api/authz" @@ -243,21 +244,59 @@ func ListMachineKeysRequestToQuery(ctx context.Context, req *mgmt_pb.ListMachine } -func AddMachineKeyRequestToDomain(req *mgmt_pb.AddMachineKeyRequest) *domain.MachineKey { +func AddMachineKeyRequestToCommand(req *mgmt_pb.AddMachineKeyRequest, resourceOwner string) *command.MachineKey { expDate := time.Time{} if req.ExpirationDate != nil { expDate = req.ExpirationDate.AsTime() } - return &domain.MachineKey{ + return &command.MachineKey{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.UserId, + AggregateID: req.UserId, + ResourceOwner: resourceOwner, }, ExpirationDate: expDate, Type: authn.KeyTypeToDomain(req.Type), } } +func RemoveMachineKeyRequestToCommand(req *mgmt_pb.RemoveMachineKeyRequest, resourceOwner string) *command.MachineKey { + return &command.MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + ResourceOwner: resourceOwner, + }, + KeyID: req.KeyId, + } +} + +func AddPersonalAccessTokenRequestToCommand(req *mgmt_pb.AddPersonalAccessTokenRequest, resourceOwner string, scopes []string, allowedUserType domain.UserType) *command.PersonalAccessToken { + expDate := time.Time{} + if req.ExpirationDate != nil { + expDate = req.ExpirationDate.AsTime() + } + + return &command.PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + ResourceOwner: resourceOwner, + }, + ExpirationDate: expDate, + Scopes: scopes, + AllowedUserType: allowedUserType, + } +} + +func RemovePersonalAccessTokenRequestToCommand(req *mgmt_pb.RemovePersonalAccessTokenRequest, resourceOwner string) *command.PersonalAccessToken { + return &command.PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + ResourceOwner: resourceOwner, + }, + TokenID: req.TokenId, + } +} + func ListPersonalAccessTokensRequestToQuery(ctx context.Context, req *mgmt_pb.ListPersonalAccessTokensRequest) (*query.PersonalAccessTokenSearchQueries, error) { resourceOwner, err := query.NewPersonalAccessTokenResourceOwnerSearchQuery(authz.GetCtxData(ctx).OrgID) if err != nil { diff --git a/internal/command/instance.go b/internal/command/instance.go index 9e096dc5bc..9acd5974b7 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -13,7 +13,6 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/notification/channels/smtp" "github.com/zitadel/zitadel/internal/repository/instance" @@ -376,6 +375,27 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str ) } + var pat *PersonalAccessToken + var machineKey *MachineKey + if setup.Org.Machine != nil { + if setup.Org.Machine.Pat { + pat = NewPersonalAccessToken(orgID, userID, setup.Org.Machine.PatExpirationDate, setup.Org.Machine.PatScopes, domain.UserTypeMachine) + pat.TokenID, err = c.idGenerator.Next() + if err != nil { + return "", "", nil, nil, err + } + validations = append(validations, prepareAddPersonalAccessToken(pat, c.keyAlgorithm)) + } + if setup.Org.Machine.MachineKey { + machineKey = NewMachineKey(orgID, userID, setup.Org.Machine.MachineKeyExpirationDate, setup.Org.Machine.MachineKeyType) + machineKey.KeyID, err = c.idGenerator.Next() + if err != nil { + return "", "", nil, nil, err + } + validations = append(validations, prepareAddUserMachineKey(machineKey, c.machineKeySize)) + } + } + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...) if err != nil { return "", "", nil, nil, err @@ -386,32 +406,18 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str return "", "", nil, nil, err } - var pat string - var machineKey []byte + var token string + var key []byte if setup.Org.Machine != nil { if setup.Org.Machine.Pat { - _, token, err := c.AddPersonalAccessToken(ctx, userID, orgID, setup.Org.Machine.PatExpirationDate, setup.Org.Machine.PatScopes, domain.UserTypeMachine) - if err != nil { - return "", "", nil, nil, err - } - pat = token + token = pat.Token } if setup.Org.Machine.MachineKey { - key, err := c.AddUserMachineKey(ctx, &domain.MachineKey{ - ObjectRoot: models.ObjectRoot{ - AggregateID: userID, - }, - ExpirationDate: setup.Org.Machine.MachineKeyExpirationDate, - Type: setup.Org.Machine.MachineKeyType, - }, orgID) - if err != nil { - return "", "", nil, nil, err - } - machineKey = key.PrivateKey + key = machineKey.PrivateKey } } - return instanceID, pat, machineKey, &domain.ObjectDetails{ + return instanceID, token, key, &domain.ObjectDetails{ Sequence: events[len(events)-1].Sequence(), EventDate: events[len(events)-1].CreationDate(), ResourceOwner: orgID, diff --git a/internal/command/org.go b/internal/command/org.go index b809a5f157..7534de955f 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/zitadel/internal/errors" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/org" user_repo "github.com/zitadel/zitadel/internal/repository/user" ) @@ -49,10 +48,30 @@ func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, user AddOrgCommand(ctx, orgAgg, o.Name, userIDs...), } + var pat *PersonalAccessToken + var machineKey *MachineKey if o.Human != nil { validations = append(validations, AddHumanCommand(userAgg, o.Human, c.userPasswordAlg, c.userEncryption)) } else if o.Machine != nil { validations = append(validations, AddMachineCommand(userAgg, o.Machine.Machine)) + if o.Machine.Pat { + pat = NewPersonalAccessToken(orgID, userID, o.Machine.PatExpirationDate, o.Machine.PatScopes, domain.UserTypeMachine) + tokenID, err := c.idGenerator.Next() + if err != nil { + return "", "", nil, nil, err + } + pat.TokenID = tokenID + validations = append(validations, prepareAddPersonalAccessToken(pat, c.keyAlgorithm)) + } + if o.Machine.MachineKey { + machineKey = NewMachineKey(orgID, userID, o.Machine.MachineKeyExpirationDate, o.Machine.MachineKeyType) + keyID, err := c.idGenerator.Next() + if err != nil { + return "", "", nil, nil, err + } + machineKey.KeyID = keyID + validations = append(validations, prepareAddUserMachineKey(machineKey, c.keySize)) + } } validations = append(validations, c.AddOrgMemberCommand(orgAgg, userID, roles...)) @@ -70,32 +89,18 @@ func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, user return "", "", nil, nil, err } - var pat string - var machineKey []byte + var token string + var key []byte if o.Machine != nil { if o.Machine.Pat { - _, token, err := c.AddPersonalAccessToken(ctx, userID, orgID, o.Machine.PatExpirationDate, o.Machine.PatScopes, domain.UserTypeMachine) - if err != nil { - return "", "", nil, nil, err - } - pat = token + token = pat.Token } if o.Machine.MachineKey { - key, err := c.AddUserMachineKey(ctx, &domain.MachineKey{ - ObjectRoot: models.ObjectRoot{ - AggregateID: userID, - }, - ExpirationDate: o.Machine.MachineKeyExpirationDate, - Type: o.Machine.MachineKeyType, - }, orgID) - if err != nil { - return "", "", nil, nil, err - } - machineKey = key.PrivateKey + key = machineKey.PrivateKey } } - return userID, pat, machineKey, &domain.ObjectDetails{ + return userID, token, key, &domain.ObjectDetails{ Sequence: events[len(events)-1].Sequence(), EventDate: events[len(events)-1].CreationDate(), ResourceOwner: orgID, diff --git a/internal/command/user.go b/internal/command/user.go index a956138f97..ee67740d4d 100644 --- a/internal/command/user.go +++ b/internal/command/user.go @@ -396,6 +396,17 @@ func (c *Commands) checkUserExists(ctx context.Context, userID, resourceOwner st return nil } +func checkUserExists(ctx context.Context, filter preparation.FilterToQueryReducer, userID, resourceOwner string) error { + existingUser, err := userWriteModelByID(ctx, filter, userID, resourceOwner) + if err != nil { + return err + } + if !isUserStateExists(existingUser.UserState) { + return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4M0fs", "Errors.User.NotFound") + } + return nil +} + func (c *Commands) userWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *UserWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/command/user_machine_key.go b/internal/command/user_machine_key.go index 5bb1b09e92..4e871698e7 100644 --- a/internal/command/user_machine_key.go +++ b/internal/command/user_machine_key.go @@ -2,89 +2,195 @@ package command import ( "context" + "time" + "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/user" - "github.com/zitadel/zitadel/internal/telemetry/tracing" ) -func (c *Commands) AddUserMachineKey(ctx context.Context, machineKey *domain.MachineKey, resourceOwner string) (*domain.MachineKey, error) { - err := c.checkUserExists(ctx, machineKey.AggregateID, resourceOwner) - if err != nil { - return nil, err - } - keyID, err := c.idGenerator.Next() - if err != nil { - return nil, err - } - keyWriteModel := NewMachineKeyWriteModel(machineKey.AggregateID, keyID, resourceOwner) - err = c.eventstore.FilterToQueryReducer(ctx, keyWriteModel) - if err != nil { - return nil, err - } +type MachineKey struct { + models.ObjectRoot - if err = domain.EnsureValidExpirationDate(machineKey); err != nil { - return nil, err - } - - if err = domain.SetNewAuthNKeyPair(machineKey, c.machineKeySize); err != nil { - return nil, err - } - - events, err := c.eventstore.Push(ctx, - user.NewMachineKeyAddedEvent( - ctx, - UserAggregateFromWriteModel(&keyWriteModel.WriteModel), - keyID, - machineKey.Type, - machineKey.ExpirationDate, - machineKey.PublicKey)) - if err != nil { - return nil, err - } - err = AppendAndReduce(keyWriteModel, events...) - if err != nil { - return nil, err - } - - key := keyWriteModelToMachineKey(keyWriteModel) - key.PrivateKey = machineKey.PrivateKey - return key, nil + KeyID string + Type domain.AuthNKeyType + ExpirationDate time.Time + PrivateKey []byte + PublicKey []byte } -func (c *Commands) RemoveUserMachineKey(ctx context.Context, userID, keyID, resourceOwner string) (*domain.ObjectDetails, error) { - keyWriteModel, err := c.machineKeyWriteModelByID(ctx, userID, keyID, resourceOwner) - if err != nil { - return nil, err +func NewMachineKey(resourceOwner string, userID string, expirationDate time.Time, keyType domain.AuthNKeyType) *MachineKey { + return &MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: userID, + ResourceOwner: resourceOwner, + }, + ExpirationDate: expirationDate, + Type: keyType, } - if !keyWriteModel.Exists() { - return nil, errors.ThrowNotFound(nil, "COMMAND-4m77G", "Errors.User.Machine.Key.NotFound") - } - - pushedEvents, err := c.eventstore.Push(ctx, - user.NewMachineKeyRemovedEvent(ctx, UserAggregateFromWriteModel(&keyWriteModel.WriteModel), keyID)) - if err != nil { - return nil, err - } - err = AppendAndReduce(keyWriteModel, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToObjectDetails(&keyWriteModel.WriteModel), nil } -func (c *Commands) machineKeyWriteModelByID(ctx context.Context, userID, keyID, resourceOwner string) (writeModel *MachineKeyWriteModel, err error) { - if userID == "" { - return nil, errors.ThrowInvalidArgument(nil, "COMMAND-4n8vs", "Errors.User.UserIDMissing") - } - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() +func (key *MachineKey) SetPublicKey(publicKey []byte) { + key.PublicKey = publicKey +} +func (key *MachineKey) SetPrivateKey(privateKey []byte) { + key.PrivateKey = privateKey +} +func (key *MachineKey) GetExpirationDate() time.Time { + return key.ExpirationDate +} +func (key *MachineKey) SetExpirationDate(t time.Time) { + key.ExpirationDate = t +} - writeModel = NewMachineKeyWriteModel(userID, keyID, resourceOwner) - err = c.eventstore.FilterToQueryReducer(ctx, writeModel) +func (key *MachineKey) Detail() ([]byte, error) { + if len(key.PrivateKey) == 0 { + return nil, errors.ThrowPreconditionFailed(nil, "KEY-sp2l2m", "Errors.Internal") + } + if key.Type == domain.AuthNKeyTypeJSON { + return domain.MachineKeyMarshalJSON(key.KeyID, key.PrivateKey, key.AggregateID) + } + return nil, errors.ThrowPreconditionFailed(nil, "KEY-dsg52", "Errors.Internal") +} + +func (key *MachineKey) content() error { + if key.ResourceOwner == "" { + return errors.ThrowInvalidArgument(nil, "COMMAND-kqpoix", "Errors.ResourceOwnerMissing") + } + if key.AggregateID == "" { + return errors.ThrowInvalidArgument(nil, "COMMAND-xuiwk2", "Errors.User.UserIDMissing") + } + if key.KeyID == "" { + return errors.ThrowInvalidArgument(nil, "COMMAND-0p2m1h", "Errors.IDMissing") + } + return nil +} + +func (key *MachineKey) valid() (err error) { + if err := key.content(); err != nil { + return err + } + key.ExpirationDate, err = domain.ValidateExpirationDate(key.ExpirationDate) + return err +} +func (key *MachineKey) checkAggregate(ctx context.Context, filter preparation.FilterToQueryReducer) error { + return checkUserExists(ctx, filter, key.AggregateID, key.ResourceOwner) +} + +func (c *Commands) AddUserMachineKey(ctx context.Context, machineKey *MachineKey) (*domain.ObjectDetails, error) { + if machineKey.KeyID == "" { + keyID, err := c.idGenerator.Next() + if err != nil { + return nil, err + } + machineKey.KeyID = keyID + } + + validation := prepareAddUserMachineKey(machineKey, c.machineKeySize) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) if err != nil { return nil, err } - return writeModel, nil + events, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return &domain.ObjectDetails{ + Sequence: events[len(events)-1].Sequence(), + EventDate: events[len(events)-1].CreationDate(), + ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, + }, nil +} + +func prepareAddUserMachineKey(machineKey *MachineKey, keySize int) preparation.Validation { + return func() (_ preparation.CreateCommands, err error) { + if err := machineKey.valid(); err != nil { + return nil, err + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + if err := machineKey.checkAggregate(ctx, filter); err != nil { + return nil, err + } + if len(machineKey.PublicKey) == 0 { + if err = domain.SetNewAuthNKeyPair(machineKey, keySize); err != nil { + return nil, err + } + } + writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner) + if err != nil { + return nil, err + } + if writeModel.Exists() { + return nil, errors.ThrowAlreadyExists(nil, "COMMAND-091mops", "Errors.User.Machine.Key.AlreadyExists") + } + return []eventstore.Command{ + user.NewMachineKeyAddedEvent( + ctx, + UserAggregateFromWriteModel(&writeModel.WriteModel), + writeModel.KeyID, + writeModel.KeyType, + writeModel.ExpirationDate, + machineKey.PublicKey, + ), + }, nil + }, nil + } +} + +func (c *Commands) RemoveUserMachineKey(ctx context.Context, machineKey *MachineKey) (*domain.ObjectDetails, error) { + validation := prepareRemoveUserMachineKey(machineKey) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) + if err != nil { + return nil, err + } + events, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return &domain.ObjectDetails{ + Sequence: events[len(events)-1].Sequence(), + EventDate: events[len(events)-1].CreationDate(), + ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, + }, nil +} + +func prepareRemoveUserMachineKey(machineKey *MachineKey) preparation.Validation { + return func() (_ preparation.CreateCommands, err error) { + if err := machineKey.content(); err != nil { + return nil, err + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner) + if err != nil { + return nil, err + } + if !writeModel.Exists() { + return nil, errors.ThrowNotFound(nil, "COMMAND-4m77G", "Errors.User.Machine.Key.NotFound") + } + return []eventstore.Command{ + user.NewMachineKeyRemovedEvent( + ctx, + UserAggregateFromWriteModel(&writeModel.WriteModel), + writeModel.KeyID, + ), + }, nil + }, nil + } +} + +func getMachineKeyWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, keyID, resourceOwner string) (_ *MachineKeyWriteModel, err error) { + writeModel := NewMachineKeyWriteModel(userID, keyID, resourceOwner) + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + if len(events) == 0 { + return writeModel, nil + } + writeModel.AppendEvents(events...) + err = writeModel.Reduce() + return writeModel, err } diff --git a/internal/command/user_personal_access_token.go b/internal/command/user_personal_access_token.go index 56722c9c08..e51fb4feac 100644 --- a/internal/command/user_personal_access_token.go +++ b/internal/command/user_personal_access_token.go @@ -2,91 +2,187 @@ package command import ( "context" + "encoding/base64" "time" + "github.com/zitadel/zitadel/internal/command/preparation" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/user" - "github.com/zitadel/zitadel/internal/telemetry/tracing" ) -func (c *Commands) AddPersonalAccessToken(ctx context.Context, userID, resourceOwner string, expirationDate time.Time, scopes []string, allowedUserType domain.UserType) (*domain.Token, string, error) { - userWriteModel, err := c.userWriteModelByID(ctx, userID, resourceOwner) +type PersonalAccessToken struct { + models.ObjectRoot + + ExpirationDate time.Time + Scopes []string + AllowedUserType domain.UserType + + TokenID string + Token string +} + +func NewPersonalAccessToken(resourceOwner string, userID string, expirationDate time.Time, scopes []string, allowedUserType domain.UserType) *PersonalAccessToken { + return &PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: userID, + ResourceOwner: resourceOwner, + }, + ExpirationDate: expirationDate, + Scopes: scopes, + AllowedUserType: allowedUserType, + } +} + +func (pat *PersonalAccessToken) content() error { + if pat.ResourceOwner == "" { + return errors.ThrowInvalidArgument(nil, "COMMAND-xs0k2n", "Errors.ResourceOwnerMissing") + } + if pat.AggregateID == "" { + return errors.ThrowInvalidArgument(nil, "COMMAND-0pzb1", "Errors.User.UserIDMissing") + } + if pat.TokenID == "" { + return errors.ThrowInvalidArgument(nil, "COMMAND-68xm2o", "Errors.IDMissing") + } + return nil +} + +func (pat *PersonalAccessToken) valid() (err error) { + if err := pat.content(); err != nil { + return err + } + pat.ExpirationDate, err = domain.ValidateExpirationDate(pat.ExpirationDate) + return err +} + +func (pat *PersonalAccessToken) checkAggregate(ctx context.Context, filter preparation.FilterToQueryReducer) error { + userWriteModel, err := userWriteModelByID(ctx, filter, pat.AggregateID, pat.ResourceOwner) if err != nil { - return nil, "", err + return err } if !isUserStateExists(userWriteModel.UserState) { - return nil, "", errors.ThrowPreconditionFailed(nil, "COMMAND-Dggw2", "Errors.User.NotFound") + return errors.ThrowPreconditionFailed(nil, "COMMAND-Dggw2", "Errors.User.NotFound") } - if allowedUserType != domain.UserTypeUnspecified && userWriteModel.UserType != allowedUserType { - return nil, "", errors.ThrowPreconditionFailed(nil, "COMMAND-Df2f1", "Errors.User.WrongType") + if pat.AllowedUserType != domain.UserTypeUnspecified && userWriteModel.UserType != pat.AllowedUserType { + return errors.ThrowPreconditionFailed(nil, "COMMAND-Df2f1", "Errors.User.WrongType") } - tokenID, err := c.idGenerator.Next() - if err != nil { - return nil, "", err - } - tokenWriteModel := NewPersonalAccessTokenWriteModel(userID, tokenID, resourceOwner) - err = c.eventstore.FilterToQueryReducer(ctx, tokenWriteModel) - if err != nil { - return nil, "", err - } - - expirationDate, err = domain.ValidateExpirationDate(expirationDate) - if err != nil { - return nil, "", err - } - - events, err := c.eventstore.Push(ctx, - user.NewPersonalAccessTokenAddedEvent( - ctx, - UserAggregateFromWriteModel(&tokenWriteModel.WriteModel), - tokenID, - expirationDate, - scopes, - ), - ) - if err != nil { - return nil, "", err - } - err = AppendAndReduce(tokenWriteModel, events...) - if err != nil { - return nil, "", err - } - return personalTokenWriteModelToToken(tokenWriteModel, c.keyAlgorithm) + return nil } -func (c *Commands) RemovePersonalAccessToken(ctx context.Context, userID, tokenID, resourceOwner string) (*domain.ObjectDetails, error) { - tokenWriteModel, err := c.personalAccessTokenWriteModelByID(ctx, userID, tokenID, resourceOwner) +func (c *Commands) AddPersonalAccessToken(ctx context.Context, pat *PersonalAccessToken) (_ *domain.ObjectDetails, err error) { + if pat.TokenID == "" { + pat.TokenID, err = c.idGenerator.Next() + if err != nil { + return nil, err + } + } + validation := prepareAddPersonalAccessToken(pat, c.keyAlgorithm) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) if err != nil { return nil, err } - if !tokenWriteModel.Exists() { - return nil, errors.ThrowNotFound(nil, "COMMAND-4m77G", "Errors.User.PAT.NotFound") - } - - pushedEvents, err := c.eventstore.Push(ctx, - user.NewPersonalAccessTokenRemovedEvent(ctx, UserAggregateFromWriteModel(&tokenWriteModel.WriteModel), tokenID)) + events, err := c.eventstore.Push(ctx, cmds...) if err != nil { return nil, err } - err = AppendAndReduce(tokenWriteModel, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToObjectDetails(&tokenWriteModel.WriteModel), nil + return &domain.ObjectDetails{ + Sequence: events[len(events)-1].Sequence(), + EventDate: events[len(events)-1].CreationDate(), + ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, + }, nil } -func (c *Commands) personalAccessTokenWriteModelByID(ctx context.Context, userID, tokenID, resourceOwner string) (writeModel *PersonalAccessTokenWriteModel, err error) { - if userID == "" { - return nil, errors.ThrowInvalidArgument(nil, "COMMAND-4n8vs", "Errors.User.UserIDMissing") - } - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() +func prepareAddPersonalAccessToken(pat *PersonalAccessToken, algorithm crypto.EncryptionAlgorithm) preparation.Validation { + return func() (_ preparation.CreateCommands, err error) { + if err := pat.valid(); err != nil { + return nil, err + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) (_ []eventstore.Command, err error) { + writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner) + if err != nil { + return nil, err + } - writeModel = NewPersonalAccessTokenWriteModel(userID, tokenID, resourceOwner) - err = c.eventstore.FilterToQueryReducer(ctx, writeModel) + pat.Token, err = createToken(algorithm, writeModel.TokenID, writeModel.AggregateID) + if err != nil { + return nil, err + } + + return []eventstore.Command{ + user.NewPersonalAccessTokenAddedEvent( + ctx, + UserAggregateFromWriteModel(&writeModel.WriteModel), + pat.TokenID, + pat.ExpirationDate, + pat.Scopes, + ), + }, nil + }, nil + } +} + +func (c *Commands) RemovePersonalAccessToken(ctx context.Context, pat *PersonalAccessToken) (*domain.ObjectDetails, error) { + validation := prepareRemovePersonalAccessToken(pat) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) if err != nil { return nil, err } - return writeModel, nil + events, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return &domain.ObjectDetails{ + Sequence: events[len(events)-1].Sequence(), + EventDate: events[len(events)-1].CreationDate(), + ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, + }, nil +} + +func prepareRemovePersonalAccessToken(pat *PersonalAccessToken) preparation.Validation { + return func() (_ preparation.CreateCommands, err error) { + if err := pat.content(); err != nil { + return nil, err + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) (_ []eventstore.Command, err error) { + writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner) + if err != nil { + return nil, err + } + if !writeModel.Exists() { + return nil, errors.ThrowNotFound(nil, "COMMAND-4m77G", "Errors.User.PAT.NotFound") + } + return []eventstore.Command{ + user.NewPersonalAccessTokenRemovedEvent( + ctx, + UserAggregateFromWriteModel(&writeModel.WriteModel), + pat.TokenID, + ), + }, nil + }, nil + } +} + +func createToken(algorithm crypto.EncryptionAlgorithm, tokenID, userID string) (string, error) { + encrypted, err := algorithm.Encrypt([]byte(tokenID + ":" + userID)) + if err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(encrypted), nil +} + +func getPersonalAccessTokenWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, tokenID, resourceOwner string) (_ *PersonalAccessTokenWriteModel, err error) { + writeModel := NewPersonalAccessTokenWriteModel(userID, tokenID, resourceOwner) + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + if len(events) == 0 { + return writeModel, nil + } + writeModel.AppendEvents(events...) + err = writeModel.Reduce() + return writeModel, err } diff --git a/internal/domain/application_key.go b/internal/domain/application_key.go index 01453cb69f..7d6407ee5f 100644 --- a/internal/domain/application_key.go +++ b/internal/domain/application_key.go @@ -20,19 +20,19 @@ type ApplicationKey struct { PublicKey []byte } -func (k *ApplicationKey) setPublicKey(publicKey []byte) { +func (k *ApplicationKey) SetPublicKey(publicKey []byte) { k.PublicKey = publicKey } -func (k *ApplicationKey) setPrivateKey(privateKey []byte) { +func (k *ApplicationKey) SetPrivateKey(privateKey []byte) { k.PrivateKey = privateKey } -func (k *ApplicationKey) expirationDate() time.Time { +func (k *ApplicationKey) GetExpirationDate() time.Time { return k.ExpirationDate } -func (k *ApplicationKey) setExpirationDate(expiration time.Time) { +func (k *ApplicationKey) SetExpirationDate(expiration time.Time) { k.ExpirationDate = expiration } diff --git a/internal/domain/authn_key.go b/internal/domain/authn_key.go index 213aa4c427..eb45e359ef 100644 --- a/internal/domain/authn_key.go +++ b/internal/domain/authn_key.go @@ -8,8 +8,8 @@ import ( ) type authNKey interface { - setPublicKey([]byte) - setPrivateKey([]byte) + SetPublicKey([]byte) + SetPrivateKey([]byte) expiration } @@ -44,8 +44,8 @@ func SetNewAuthNKeyPair(key authNKey, keySize int) error { if err != nil { return err } - key.setPrivateKey(privateKey) - key.setPublicKey(publicKey) + key.SetPrivateKey(privateKey) + key.SetPublicKey(publicKey) return nil } diff --git a/internal/domain/expiration.go b/internal/domain/expiration.go index d622924152..9b74702d16 100644 --- a/internal/domain/expiration.go +++ b/internal/domain/expiration.go @@ -12,16 +12,16 @@ var ( ) type expiration interface { - expirationDate() time.Time - setExpirationDate(time.Time) + GetExpirationDate() time.Time + SetExpirationDate(time.Time) } func EnsureValidExpirationDate(key expiration) error { - date, err := ValidateExpirationDate(key.expirationDate()) + date, err := ValidateExpirationDate(key.GetExpirationDate()) if err != nil { return err } - key.setExpirationDate(date) + key.SetExpirationDate(date) return nil } diff --git a/internal/domain/machine_key.go b/internal/domain/machine_key.go index 9f821fbe48..43abfd83b9 100644 --- a/internal/domain/machine_key.go +++ b/internal/domain/machine_key.go @@ -42,17 +42,7 @@ func (key *MachineKey) Detail() ([]byte, error) { } func (key *MachineKey) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - Type string `json:"type"` - KeyID string `json:"keyId"` - Key string `json:"key"` - UserID string `json:"userId"` - }{ - Type: "serviceaccount", - KeyID: key.KeyID, - Key: string(key.PrivateKey), - UserID: key.AggregateID, - }) + return MachineKeyMarshalJSON(key.KeyID, key.PrivateKey, key.AggregateID) } type MachineKeyState int32 @@ -68,3 +58,17 @@ const ( func (f MachineKeyState) Valid() bool { return f >= 0 && f < machineKeyStateCount } + +func MachineKeyMarshalJSON(keyID string, privateKey []byte, userID string) ([]byte, error) { + return json.Marshal(struct { + Type string `json:"type"` + KeyID string `json:"keyId"` + Key string `json:"key"` + UserID string `json:"userId"` + }{ + Type: "serviceaccount", + KeyID: keyID, + Key: string(privateKey), + UserID: userID, + }) +}