feat: actions (#2377)

* feat(actions): begin api

* feat(actions): begin api

* api and projections

* fix: handle multiple statements for a single event in projections

* export func type

* fix test

* update to new reduce interface

* flows in login

* feat: jwt idp

* feat: command side

* feat: add tests

* actions and flows

* fill idp views with jwt idps and return apis

* add jwtEndpoint to jwt idp

* begin jwt request handling

* add feature

* merge

* merge

* handle jwt idp

* cleanup

* bug fixes

* autoregister

* get token from specific header name

* fix: proto

* fixes

* i18n

* begin tests

* fix and log http proxy

* remove docker cache

* fixes

* usergrants in actions api

* tests adn cleanup

* cleanup

* fix add user grant

* set login context

* i18n

Co-authored-by: fabi <fabienne.gerschwiler@gmail.com>
This commit is contained in:
Livio Amstutz
2021-09-27 13:43:49 +02:00
committed by GitHub
parent 5c32fc9c12
commit ed80a8bb1e
73 changed files with 5197 additions and 64 deletions

112
internal/actions/actions.go Normal file
View File

@@ -0,0 +1,112 @@
package actions
import (
"errors"
"time"
"github.com/caos/logging"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/console"
"github.com/dop251/goja_nodejs/require"
)
var ErrHalt = errors.New("interrupt")
type jsAction func(*Context, *API) error
func Run(ctx *Context, api *API, script, name string, timeout time.Duration, allowedToFail bool) error {
if timeout <= 0 || timeout > 20 {
timeout = 20 * time.Second
}
prepareTimeout := timeout
if prepareTimeout > 5 {
prepareTimeout = 5 * time.Second
}
vm, err := prepareRun(script, prepareTimeout)
if err != nil {
return err
}
var fn jsAction
jsFn := vm.Get(name)
if jsFn == nil {
return errors.New("function not found")
}
err = vm.ExportTo(jsFn, &fn)
if err != nil {
return err
}
t := setInterrupt(vm, timeout)
defer func() {
t.Stop()
}()
errCh := make(chan error)
go func() {
defer func() {
r := recover()
if r != nil && !allowedToFail {
err, ok := r.(error)
if !ok {
e, ok := r.(string)
if ok {
err = errors.New(e)
}
}
errCh <- err
return
}
}()
err = fn(ctx, api)
if err != nil && !allowedToFail {
errCh <- err
return
}
errCh <- nil
}()
return <-errCh
}
func newRuntime() *goja.Runtime {
vm := goja.New()
printer := console.PrinterFunc(func(s string) {
logging.Log("ACTIONS-dfgg2").Debug(s)
})
registry := new(require.Registry)
registry.Enable(vm)
registry.RegisterNativeModule("console", console.RequireWithPrinter(printer))
console.Enable(vm)
return vm
}
func prepareRun(script string, timeout time.Duration) (*goja.Runtime, error) {
vm := newRuntime()
t := setInterrupt(vm, timeout)
defer func() {
t.Stop()
}()
errCh := make(chan error)
go func() {
defer func() {
r := recover()
if r != nil {
errCh <- r.(error)
return
}
}()
_, err := vm.RunString(script)
if err != nil {
errCh <- err
return
}
errCh <- nil
}()
return vm, <-errCh
}
func setInterrupt(vm *goja.Runtime, timeout time.Duration) *time.Timer {
vm.ClearInterrupt()
return time.AfterFunc(timeout, func() {
vm.Interrupt(ErrHalt)
})
}

105
internal/actions/api.go Normal file
View File

@@ -0,0 +1,105 @@
package actions
import (
"github.com/caos/zitadel/internal/domain"
"golang.org/x/text/language"
)
type API map[string]interface{}
func (a API) set(name string, value interface{}) {
map[string]interface{}(a)[name] = value
}
func (a *API) SetHuman(human *domain.Human) *API {
a.set("setFirstName", func(firstName string) {
human.FirstName = firstName
})
a.set("setLastName", func(lastName string) {
human.LastName = lastName
})
a.set("setNickName", func(nickName string) {
human.NickName = nickName
})
a.set("setDisplayName", func(displayName string) {
human.DisplayName = displayName
})
a.set("setPreferredLanguage", func(preferredLanguage string) {
human.PreferredLanguage = language.Make(preferredLanguage)
})
a.set("setGender", func(gender domain.Gender) {
human.Gender = gender
})
a.set("setUsername", func(username string) {
human.Username = username
})
a.set("setEmail", func(email string) {
if human.Email == nil {
human.Email = &domain.Email{}
}
human.Email.EmailAddress = email
})
a.set("setEmailVerified", func(verified bool) {
if human.Email == nil {
return
}
human.Email.IsEmailVerified = verified
})
a.set("setPhone", func(email string) {
if human.Phone == nil {
human.Phone = &domain.Phone{}
}
human.Phone.PhoneNumber = email
})
a.set("setPhoneVerified", func(verified bool) {
if human.Phone == nil {
return
}
human.Phone.IsPhoneVerified = verified
})
return a
}
func (a *API) SetExternalUser(user *domain.ExternalUser) *API {
a.set("setFirstName", func(firstName string) {
user.FirstName = firstName
})
a.set("setLastName", func(lastName string) {
user.LastName = lastName
})
a.set("setNickName", func(nickName string) {
user.NickName = nickName
})
a.set("setDisplayName", func(displayName string) {
user.DisplayName = displayName
})
a.set("setPreferredLanguage", func(preferredLanguage string) {
user.PreferredLanguage = language.Make(preferredLanguage)
})
a.set("setPreferredUsername", func(username string) {
user.PreferredUsername = username
})
a.set("setEmail", func(email string) {
user.Email = email
})
a.set("setEmailVerified", func(verified bool) {
user.IsEmailVerified = verified
})
a.set("setPhone", func(phone string) {
user.Phone = phone
})
a.set("setPhoneVerified", func(verified bool) {
user.IsPhoneVerified = verified
})
return a
}
func (a *API) SetMetadata(metadata *[]*domain.Metadata) *API {
a.set("metadata", metadata)
return a
}
func (a *API) SetUserGrants(usergrants *[]UserGrant) *API {
a.set("userGrants", usergrants)
return a
}

View File

@@ -0,0 +1,33 @@
package actions
import (
"encoding/json"
"github.com/caos/oidc/pkg/oidc"
)
type Context map[string]interface{}
func (c Context) set(name string, value interface{}) {
map[string]interface{}(c)[name] = value
}
func (c *Context) SetToken(t *oidc.Tokens) *Context {
if t.Token != nil && t.Token.AccessToken != "" {
c.set("accessToken", t.AccessToken)
}
if t.IDToken != "" {
c.set("idToken", t.IDToken)
}
if t.IDTokenClaims != nil {
c.set("getClaim", func(claim string) interface{} { return t.IDTokenClaims.GetClaim(claim) })
c.set("claimsJSON", func() (string, error) {
c, err := json.Marshal(t.IDTokenClaims)
if err != nil {
return "", err
}
return string(c), nil
})
}
return c
}

View File

@@ -0,0 +1,55 @@
package actions
import (
"errors"
"github.com/dop251/goja"
)
type UserGrant struct {
ProjectID string
ProjectGrantID string
Roles []string
}
func appendUserGrant(list *[]UserGrant) func(goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
userGrantMap := call.Argument(0).Export()
userGrant, _ := userGrantFromMap(userGrantMap)
*list = append(*list, userGrant)
return nil
}
}
func userGrantFromMap(grantMap interface{}) (UserGrant, error) {
m, ok := grantMap.(map[string]interface{})
if !ok {
return UserGrant{}, errors.New("invalid")
}
projectID, ok := m["projectID"].(string)
if !ok {
return UserGrant{}, errors.New("invalid")
}
var projectGrantID string
if id, ok := m["projectGrantID"]; ok {
projectGrantID, ok = id.(string)
if !ok {
return UserGrant{}, errors.New("invalid")
}
}
var roles []string
if r := m["roles"]; r != nil {
rs, ok := r.([]interface{})
if !ok {
return UserGrant{}, errors.New("invalid")
}
for _, role := range rs {
roles = append(roles, role.(string))
}
}
return UserGrant{
ProjectID: projectID,
ProjectGrantID: projectGrantID,
Roles: roles,
}, nil
}

View File

@@ -0,0 +1,126 @@
package action
import (
object_grpc "github.com/caos/zitadel/internal/api/grpc/object"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/query"
action_pb "github.com/caos/zitadel/pkg/grpc/action"
"google.golang.org/protobuf/types/known/durationpb"
)
func FlowTypeToDomain(flowType action_pb.FlowType) domain.FlowType {
switch flowType {
case action_pb.FlowType_FLOW_TYPE_EXTERNAL_AUTHENTICATION:
return domain.FlowTypeExternalAuthentication
default:
return domain.FlowTypeUnspecified
}
}
func TriggerTypeToDomain(triggerType action_pb.TriggerType) domain.TriggerType {
switch triggerType {
case action_pb.TriggerType_TRIGGER_TYPE_POST_AUTHENTICATION:
return domain.TriggerTypePostAuthentication
case action_pb.TriggerType_TRIGGER_TYPE_PRE_CREATION:
return domain.TriggerTypePreCreation
case action_pb.TriggerType_TRIGGER_TYPE_POST_CREATION:
return domain.TriggerTypePostCreation
default:
return domain.TriggerTypeUnspecified
}
}
func FlowToPb(flow *query.Flow) *action_pb.Flow {
return &action_pb.Flow{
Type: FlowTypeToPb(flow.Type),
Details: object_grpc.ChangeToDetailsPb(flow.Sequence, flow.ChangeDate, flow.ResourceOwner),
State: action_pb.FlowState_FLOW_STATE_ACTIVE, //TODO: state in next release
TriggerActions: TriggerActionsToPb(flow.TriggerActions),
}
}
func TriggerActionToPb(trigger domain.TriggerType, actions []*query.Action) *action_pb.TriggerAction {
return &action_pb.TriggerAction{
TriggerType: TriggerTypeToPb(trigger),
Actions: ActionsToPb(actions),
}
}
func FlowTypeToPb(flowType domain.FlowType) action_pb.FlowType {
switch flowType {
case domain.FlowTypeExternalAuthentication:
return action_pb.FlowType_FLOW_TYPE_EXTERNAL_AUTHENTICATION
default:
return action_pb.FlowType_FLOW_TYPE_UNSPECIFIED
}
}
func TriggerTypeToPb(triggerType domain.TriggerType) action_pb.TriggerType {
switch triggerType {
case domain.TriggerTypePostAuthentication:
return action_pb.TriggerType_TRIGGER_TYPE_POST_AUTHENTICATION
case domain.TriggerTypePreCreation:
return action_pb.TriggerType_TRIGGER_TYPE_PRE_CREATION
case domain.TriggerTypePostCreation:
return action_pb.TriggerType_TRIGGER_TYPE_POST_CREATION
default:
return action_pb.TriggerType_TRIGGER_TYPE_UNSPECIFIED
}
}
func TriggerActionsToPb(triggers map[domain.TriggerType][]*query.Action) []*action_pb.TriggerAction {
list := make([]*action_pb.TriggerAction, 0)
for trigger, actions := range triggers {
list = append(list, TriggerActionToPb(trigger, actions))
}
return list
}
func ActionsToPb(actions []*query.Action) []*action_pb.Action {
list := make([]*action_pb.Action, len(actions))
for i, action := range actions {
list[i] = ActionToPb(action)
}
return list
}
func ActionToPb(action *query.Action) *action_pb.Action {
return &action_pb.Action{
Id: action.ID,
Details: object_grpc.ChangeToDetailsPb(action.Sequence, action.ChangeDate, action.ResourceOwner),
State: ActionStateToPb(action.State),
Name: action.Name,
Script: action.Script,
Timeout: durationpb.New(action.Timeout),
AllowedToFail: action.AllowedToFail,
}
}
func ActionStateToPb(state domain.ActionState) action_pb.ActionState {
switch state {
case domain.ActionStateActive:
return action_pb.ActionState_ACTION_STATE_ACTIVE
case domain.ActionStateInactive:
return action_pb.ActionState_ACTION_STATE_INACTIVE
default:
return action_pb.ActionState_ACTION_STATE_UNSPECIFIED
}
}
func ActionNameQuery(q *action_pb.ActionNameQuery) (query.SearchQuery, error) {
return query.NewActionNameSearchQuery(object_grpc.TextMethodToQuery(q.Method), q.Name)
}
func ActionStateQuery(q *action_pb.ActionStateQuery) (query.SearchQuery, error) {
return query.NewActionStateSearchQuery(ActionStateToDomain(q.State))
}
func ActionStateToDomain(state action_pb.ActionState) domain.ActionState {
switch state {
case action_pb.ActionState_ACTION_STATE_ACTIVE:
return domain.ActionStateActive
case action_pb.ActionState_ACTION_STATE_INACTIVE:
return domain.ActionStateInactive
default:
return domain.ActionStateUnspecified
}
}

View File

@@ -79,6 +79,7 @@ func setDefaultFeaturesRequestToDomain(req *admin_pb.SetDefaultFeaturesRequest)
CustomTextLogin: req.CustomTextLogin || req.CustomText,
CustomTextMessage: req.CustomTextMessage,
LockoutPolicy: req.LockoutPolicy,
Actions: req.Actions,
}
}
@@ -104,5 +105,6 @@ func setOrgFeaturesRequestToDomain(req *admin_pb.SetOrgFeaturesRequest) *domain.
CustomTextLogin: req.CustomTextLogin || req.CustomText,
CustomTextMessage: req.CustomTextMessage,
LockoutPolicy: req.LockoutPolicy,
Actions: req.Actions,
}
}

View File

@@ -33,6 +33,7 @@ func FeaturesFromModel(features *features_model.FeaturesView) *features_pb.Featu
CustomTextLogin: features.CustomTextLogin,
MetadataUser: features.MetadataUser,
LockoutPolicy: features.LockoutPolicy,
Actions: features.Actions,
}
}

View File

@@ -0,0 +1,94 @@
package management
import (
"context"
"github.com/caos/zitadel/internal/api/authz"
action_grpc "github.com/caos/zitadel/internal/api/grpc/action"
obj_grpc "github.com/caos/zitadel/internal/api/grpc/object"
mgmt_pb "github.com/caos/zitadel/pkg/grpc/management"
)
func (s *Server) ListActions(ctx context.Context, req *mgmt_pb.ListActionsRequest) (*mgmt_pb.ListActionsResponse, error) {
query, _ := listActionsToQuery(authz.GetCtxData(ctx).OrgID, req)
actions, err := s.query.SearchActions(ctx, query)
if err != nil {
return nil, err
}
return &mgmt_pb.ListActionsResponse{
Result: action_grpc.ActionsToPb(actions),
}, nil
}
func (s *Server) GetAction(ctx context.Context, req *mgmt_pb.GetActionRequest) (*mgmt_pb.GetActionResponse, error) {
action, err := s.query.GetAction(ctx, req.Id, authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
return &mgmt_pb.GetActionResponse{
Action: action_grpc.ActionToPb(action),
}, nil
}
func (s *Server) CreateAction(ctx context.Context, req *mgmt_pb.CreateActionRequest) (*mgmt_pb.CreateActionResponse, error) {
id, details, err := s.command.AddAction(ctx, createActionRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
return &mgmt_pb.CreateActionResponse{
Id: id,
Details: obj_grpc.AddToDetailsPb(
details.Sequence,
details.EventDate,
details.ResourceOwner,
),
}, nil
}
func (s *Server) UpdateAction(ctx context.Context, req *mgmt_pb.UpdateActionRequest) (*mgmt_pb.UpdateActionResponse, error) {
details, err := s.command.ChangeAction(ctx, updateActionRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
return &mgmt_pb.UpdateActionResponse{
Details: obj_grpc.AddToDetailsPb(
details.Sequence,
details.EventDate,
details.ResourceOwner,
),
}, nil
}
func (s *Server) DeactivateAction(ctx context.Context, req *mgmt_pb.DeactivateActionRequest) (*mgmt_pb.DeactivateActionResponse, error) {
details, err := s.command.DeactivateAction(ctx, req.Id, authz.GetCtxData(ctx).OrgID)
return &mgmt_pb.DeactivateActionResponse{
Details: obj_grpc.AddToDetailsPb(
details.Sequence,
details.EventDate,
details.ResourceOwner,
),
}, err
}
func (s *Server) ReactivateAction(ctx context.Context, req *mgmt_pb.ReactivateActionRequest) (*mgmt_pb.ReactivateActionResponse, error) {
details, err := s.command.ReactivateAction(ctx, req.Id, authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
return &mgmt_pb.ReactivateActionResponse{
Details: obj_grpc.AddToDetailsPb(
details.Sequence,
details.EventDate,
details.ResourceOwner,
),
}, nil
}
func (s *Server) DeleteAction(ctx context.Context, req *mgmt_pb.DeleteActionRequest) (*mgmt_pb.DeleteActionResponse, error) {
flowTypes, err := s.query.GetFlowTypesOfActionID(ctx, req.Id)
if err != nil {
return nil, err
}
_, err = s.command.DeleteAction(ctx, req.Id, authz.GetCtxData(ctx).OrgID, flowTypes...)
return &mgmt_pb.DeleteActionResponse{}, err
}

View File

@@ -0,0 +1,64 @@
package management
import (
action_grpc "github.com/caos/zitadel/internal/api/grpc/action"
"github.com/caos/zitadel/internal/api/grpc/object"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/query"
mgmt_pb "github.com/caos/zitadel/pkg/grpc/management"
)
func createActionRequestToDomain(req *mgmt_pb.CreateActionRequest) *domain.Action {
return &domain.Action{
Name: req.Name,
Script: req.Script,
Timeout: req.Timeout.AsDuration(),
AllowedToFail: req.AllowedToFail,
}
}
func updateActionRequestToDomain(req *mgmt_pb.UpdateActionRequest) *domain.Action {
return &domain.Action{
ObjectRoot: models.ObjectRoot{
AggregateID: req.Id,
},
Name: req.Name,
Script: req.Script,
Timeout: req.Timeout.AsDuration(),
AllowedToFail: req.AllowedToFail,
}
}
func listActionsToQuery(id string, req *mgmt_pb.ListActionsRequest) (_ *query.ActionSearchQueries, err error) {
offset, limit, asc := object.ListQueryToModel(req.Query)
queries := make([]query.SearchQuery, len(req.Queries)+1)
queries[0], err = query.NewActionResourceOwnerQuery(id)
if err != nil {
return nil, err
}
for i, actionQuery := range req.Queries {
queries[i+1], err = ActionQueryToQuery(actionQuery.Query)
if err != nil {
return nil, err
}
}
return &query.ActionSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
},
Queries: queries,
}, nil
}
func ActionQueryToQuery(query interface{}) (query.SearchQuery, error) {
switch q := query.(type) {
case *mgmt_pb.ActionQuery_ActionNameQuery:
return action_grpc.ActionNameQuery(q.ActionNameQuery)
case *mgmt_pb.ActionQuery_ActionStateQuery:
return action_grpc.ActionStateQuery(q.ActionStateQuery)
}
return nil, nil
}

View File

@@ -0,0 +1,32 @@
package management
import (
"context"
"github.com/caos/zitadel/internal/api/authz"
caos_errors "github.com/caos/zitadel/internal/errors"
)
func checkExplicitProjectPermission(ctx context.Context, grantID, projectID string) error {
permissions := authz.GetRequestPermissionsFromCtx(ctx)
if authz.HasGlobalPermission(permissions) {
return nil
}
ids := authz.GetAllPermissionCtxIDs(permissions)
if grantID != "" && listContainsID(ids, grantID) {
return nil
}
if listContainsID(ids, projectID) {
return nil
}
return caos_errors.ThrowPermissionDenied(nil, "EVENT-Shu7e", "Errors.UserGrant.NoPermissionForProject")
}
func listContainsID(ids []string, id string) bool {
for _, i := range ids {
if i == id {
return true
}
}
return false
}

View File

@@ -0,0 +1,50 @@
package management
import (
"context"
"github.com/caos/zitadel/internal/api/authz"
action_grpc "github.com/caos/zitadel/internal/api/grpc/action"
obj_grpc "github.com/caos/zitadel/internal/api/grpc/object"
mgmt_pb "github.com/caos/zitadel/pkg/grpc/management"
)
func (s *Server) GetFlow(ctx context.Context, req *mgmt_pb.GetFlowRequest) (*mgmt_pb.GetFlowResponse, error) {
flow, err := s.query.GetFlow(ctx, action_grpc.FlowTypeToDomain(req.Type))
if err != nil {
return nil, err
}
return &mgmt_pb.GetFlowResponse{
Flow: action_grpc.FlowToPb(flow),
}, nil
}
func (s *Server) ClearFlow(ctx context.Context, req *mgmt_pb.ClearFlowRequest) (*mgmt_pb.ClearFlowResponse, error) {
details, err := s.command.ClearFlow(ctx, action_grpc.FlowTypeToDomain(req.Type), authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
return &mgmt_pb.ClearFlowResponse{
Details: obj_grpc.DomainToChangeDetailsPb(details),
}, err
}
func (s *Server) SetTriggerActions(ctx context.Context, req *mgmt_pb.SetTriggerActionsRequest) (*mgmt_pb.SetTriggerActionsResponse, error) {
details, err := s.command.SetTriggerActions(
ctx,
action_grpc.FlowTypeToDomain(req.FlowType),
action_grpc.TriggerTypeToDomain(req.TriggerType),
req.ActionIds,
authz.GetCtxData(ctx).OrgID,
)
if err != nil {
return nil, err
}
return &mgmt_pb.SetTriggerActionsResponse{
Details: obj_grpc.AddToDetailsPb(
details.Sequence,
details.EventDate,
details.ResourceOwner,
),
}, nil
}

View File

@@ -2,6 +2,7 @@ package management
import (
"context"
"github.com/caos/zitadel/internal/api/authz"
obj_grpc "github.com/caos/zitadel/internal/api/grpc/object"
"github.com/caos/zitadel/internal/api/grpc/user"
@@ -37,7 +38,11 @@ func (s *Server) ListUserGrants(ctx context.Context, req *mgmt_pb.ListUserGrantR
}
func (s *Server) AddUserGrant(ctx context.Context, req *mgmt_pb.AddUserGrantRequest) (*mgmt_pb.AddUserGrantResponse, error) {
grant, err := s.command.AddUserGrant(ctx, AddUserGrantRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
grant := AddUserGrantRequestToDomain(req)
if err := checkExplicitProjectPermission(ctx, grant.ProjectGrantID, grant.ProjectID); err != nil {
return nil, err
}
grant, err := s.command.AddUserGrant(ctx, grant, authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}

View File

@@ -6,6 +6,7 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/query"
"github.com/caos/zitadel/pkg/grpc/object"
object_pb "github.com/caos/zitadel/pkg/grpc/object"
)
@@ -105,3 +106,26 @@ func ListQueryToModel(query *object_pb.ListQuery) (offset, limit uint64, asc boo
}
return query.Offset, uint64(query.Limit), query.Asc
}
func TextMethodToQuery(method object_pb.TextQueryMethod) query.TextComparison {
switch method {
case object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS:
return query.TextEquals
case object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS_IGNORE_CASE:
return query.TextEqualsIgnore
case object.TextQueryMethod_TEXT_QUERY_METHOD_STARTS_WITH:
return query.TextStartsWith
case object.TextQueryMethod_TEXT_QUERY_METHOD_STARTS_WITH_IGNORE_CASE:
return query.TextStartsWithIgnore
case object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS:
return query.TextContains
case object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE:
return query.TextContainsIgnore
case object.TextQueryMethod_TEXT_QUERY_METHOD_ENDS_WITH:
return query.TextEndsWith
case object.TextQueryMethod_TEXT_QUERY_METHOD_ENDS_WITH_IGNORE_CASE:
return query.TextEndsWithIgnore
default:
return -1
}
}

View File

@@ -106,7 +106,7 @@ func (g *GatewayHandler) Serve(ctx context.Context) {
func createGateway(ctx context.Context, g Gateway, port string, customHeaders ...string) http.Handler {
mux := createMux(g, customHeaders...)
opts := createDialOptions(g)
err := g.RegisterGateway()(ctx, mux, http_util.Endpoint(port), opts)
err := g.RegisterGateway()(ctx, mux, "localhost"+http_util.Endpoint(port), opts)
logging.Log("SERVE-7B7G0E").OnError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Panic("failed to register grpc gateway")
return addInterceptors(mux, g)
}

View File

@@ -2,6 +2,7 @@ package repository
import (
"context"
"github.com/caos/zitadel/internal/domain"
)
@@ -31,6 +32,6 @@ type AuthRequestRepository interface {
VerifyPasswordless(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string, credentialData []byte, info *domain.BrowserInfo) error
LinkExternalUsers(ctx context.Context, authReqID, userAgentID string, info *domain.BrowserInfo) error
AutoRegisterExternalUser(ctx context.Context, user *domain.Human, externalIDP *domain.ExternalIDP, orgMemberRoles []string, authReqID, userAgentID, resourceOwner string, info *domain.BrowserInfo) error
AutoRegisterExternalUser(ctx context.Context, user *domain.Human, externalIDP *domain.ExternalIDP, orgMemberRoles []string, authReqID, userAgentID, resourceOwner string, metadatas []*domain.Metadata, info *domain.BrowserInfo) error
ResetLinkingUsers(ctx context.Context, authReqID, userAgentID string) error
}

View File

@@ -5,7 +5,6 @@ import (
"time"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
"github.com/caos/zitadel/internal/auth_request/model"
@@ -404,7 +403,7 @@ func (repo *AuthRequestRepo) ResetLinkingUsers(ctx context.Context, authReqID, u
return repo.AuthRequests.UpdateAuthRequest(ctx, request)
}
func (repo *AuthRequestRepo) AutoRegisterExternalUser(ctx context.Context, registerUser *domain.Human, externalIDP *domain.ExternalIDP, orgMemberRoles []string, authReqID, userAgentID, resourceOwner string, info *domain.BrowserInfo) (err error) {
func (repo *AuthRequestRepo) AutoRegisterExternalUser(ctx context.Context, registerUser *domain.Human, externalIDP *domain.ExternalIDP, orgMemberRoles []string, authReqID, userAgentID, resourceOwner string, metadatas []*domain.Metadata, info *domain.BrowserInfo) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
request, err := repo.getAuthRequest(ctx, authReqID, userAgentID)
@@ -423,6 +422,12 @@ func (repo *AuthRequestRepo) AutoRegisterExternalUser(ctx context.Context, regis
if err != nil {
return err
}
if len(metadatas) > 0 {
_, err = repo.Command.BulkSetUserMetadata(ctx, request.UserID, request.UserOrgID, metadatas...)
if err != nil {
return err
}
}
return repo.AuthRequests.UpdateAuthRequest(ctx, request)
}

View File

@@ -181,6 +181,12 @@ func checkFeatures(features *features_view_model.FeaturesView, requiredFeatures
}
continue
}
if requiredFeature == domain.FeatureActions {
if !features.Actions {
return MissingFeatureErr(requiredFeature)
}
continue
}
return MissingFeatureErr(requiredFeature)
}
return nil

View File

@@ -9,6 +9,7 @@ import (
"github.com/caos/zitadel/internal/config/types"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/action"
"github.com/caos/zitadel/internal/api/http"
sd "github.com/caos/zitadel/internal/config/systemdefaults"
@@ -78,6 +79,7 @@ func StartCommands(eventstore *eventstore.Eventstore, defaults sd.SystemDefaults
usr_grant_repo.RegisterEventMappers(repo.eventstore)
proj_repo.RegisterEventMappers(repo.eventstore)
keypair.RegisterEventMappers(repo.eventstore)
action.RegisterEventMappers(repo.eventstore)
repo.idpConfigSecretCrypto, err = crypto.NewAESCrypto(defaults.IDPConfigVerificationKey)
if err != nil {

View File

@@ -31,6 +31,7 @@ type FeaturesWriteModel struct {
CustomTextMessage bool
CustomTextLogin bool
LockoutPolicy bool
Actions bool
}
func (wm *FeaturesWriteModel) Reduce() error {
@@ -98,6 +99,9 @@ func (wm *FeaturesWriteModel) Reduce() error {
if e.LockoutPolicy != nil {
wm.LockoutPolicy = *e.LockoutPolicy
}
if e.Actions != nil {
wm.Actions = *e.Actions
}
case *features.FeaturesRemovedEvent:
wm.State = domain.FeaturesStateRemoved
}

View File

@@ -0,0 +1,52 @@
package command
import (
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/flow"
)
type FlowWriteModel struct {
eventstore.WriteModel
FlowType domain.FlowType
State domain.FlowState
Triggers map[domain.TriggerType][]string
}
func NewFlowWriteModel(flowType domain.FlowType, resourceOwner string) *FlowWriteModel {
return &FlowWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: resourceOwner,
ResourceOwner: resourceOwner,
},
FlowType: flowType,
Triggers: make(map[domain.TriggerType][]string),
}
}
func (wm *FlowWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *flow.TriggerActionsSetEvent:
if wm.Triggers == nil {
wm.Triggers = make(map[domain.TriggerType][]string)
}
wm.Triggers[e.TriggerType] = e.ActionIDs
case *flow.TriggerActionsCascadeRemovedEvent:
remove(wm.Triggers[e.TriggerType], e.ActionID)
case *flow.FlowClearedEvent:
wm.Triggers = nil
}
}
return wm.WriteModel.Reduce()
}
func remove(ids []string, id string) {
for i := 0; i < len(ids); i++ {
if ids[i] == id {
ids = append(ids[:i], ids[i+1:]...)
break
}
}
}

View File

@@ -54,6 +54,7 @@ func (c *Commands) setDefaultFeatures(ctx context.Context, existingFeatures *IAM
features.CustomTextMessage,
features.CustomTextLogin,
features.LockoutPolicy,
features.Actions,
)
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged")

View File

@@ -71,7 +71,8 @@ func (wm *IAMFeaturesWriteModel) NewSetEvent(
metadataUser,
customTextMessage,
customTextLogin,
lockoutPolicy bool,
lockoutPolicy,
actions bool,
) (*iam.FeaturesSetEvent, bool) {
changes := make([]features.FeaturesChanges, 0)
@@ -133,6 +134,9 @@ func (wm *IAMFeaturesWriteModel) NewSetEvent(
if wm.LockoutPolicy != lockoutPolicy {
changes = append(changes, features.ChangeLockoutPolicy(lockoutPolicy))
}
if wm.Actions != actions {
changes = append(changes, features.ChangeActions(actions))
}
if len(changes) == 0 {
return nil, false
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/eventstore/repository/mock"
action_repo "github.com/caos/zitadel/internal/repository/action"
iam_repo "github.com/caos/zitadel/internal/repository/iam"
key_repo "github.com/caos/zitadel/internal/repository/keypair"
"github.com/caos/zitadel/internal/repository/org"
@@ -35,6 +36,7 @@ func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore {
proj_repo.RegisterEventMappers(es)
usergrant.RegisterEventMappers(es)
key_repo.RegisterEventMappers(es)
action_repo.RegisterEventMappers(es)
return es
}

View File

@@ -0,0 +1,200 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/action"
"github.com/caos/zitadel/internal/repository/org"
)
func (c *Commands) AddAction(ctx context.Context, addAction *domain.Action, resourceOwner string) (_ string, _ *domain.ObjectDetails, err error) {
if !addAction.IsValid() {
return "", nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-eg2gf", "Errors.Action.Invalid")
}
addAction.AggregateID, err = c.idGenerator.Next()
if err != nil {
return "", nil, err
}
actionModel := NewActionWriteModel(addAction.AggregateID, resourceOwner)
actionAgg := ActionAggregateFromWriteModel(&actionModel.WriteModel)
pushedEvents, err := c.eventstore.PushEvents(ctx, action.NewAddedEvent(
ctx,
actionAgg,
addAction.Name,
addAction.Script,
addAction.Timeout,
addAction.AllowedToFail,
))
if err != nil {
return "", nil, err
}
err = AppendAndReduce(actionModel, pushedEvents...)
if err != nil {
return "", nil, err
}
return actionModel.AggregateID, writeModelToObjectDetails(&actionModel.WriteModel), nil
}
func (c *Commands) ChangeAction(ctx context.Context, actionChange *domain.Action, resourceOwner string) (*domain.ObjectDetails, error) {
if !actionChange.IsValid() || actionChange.AggregateID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Df2f3", "Errors.Action.Invalid")
}
existingAction, err := c.getActionWriteModelByID(ctx, actionChange.AggregateID, resourceOwner)
if err != nil {
return nil, err
}
if !existingAction.State.Exists() {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-Sfg2t", "Errors.Action.NotFound")
}
actionAgg := ActionAggregateFromWriteModel(&existingAction.WriteModel)
changedEvent, err := existingAction.NewChangedEvent(
ctx,
actionAgg,
actionChange.Name,
actionChange.Script,
actionChange.Timeout,
actionChange.AllowedToFail)
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.PushEvents(ctx, changedEvent)
if err != nil {
return nil, err
}
err = AppendAndReduce(existingAction, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingAction.WriteModel), nil
}
func (c *Commands) DeactivateAction(ctx context.Context, actionID string, resourceOwner string) (*domain.ObjectDetails, error) {
if actionID == "" || resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-DAhk5", "Errors.IDMissing")
}
existingAction, err := c.getActionWriteModelByID(ctx, actionID, resourceOwner)
if err != nil {
return nil, err
}
if !existingAction.State.Exists() {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-NRmhu", "Errors.Action.NotFound")
}
if existingAction.State != domain.ActionStateActive {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Dgj92", "Errors.Action.NotActive")
}
actionAgg := ActionAggregateFromWriteModel(&existingAction.WriteModel)
events := []eventstore.EventPusher{
action.NewDeactivatedEvent(ctx, actionAgg),
}
pushedEvents, err := c.eventstore.PushEvents(ctx, events...)
if err != nil {
return nil, err
}
err = AppendAndReduce(existingAction, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingAction.WriteModel), nil
}
func (c *Commands) ReactivateAction(ctx context.Context, actionID string, resourceOwner string) (*domain.ObjectDetails, error) {
if actionID == "" || resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-BNm56", "Errors.IDMissing")
}
existingAction, err := c.getActionWriteModelByID(ctx, actionID, resourceOwner)
if err != nil {
return nil, err
}
if !existingAction.State.Exists() {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-Aa22g", "Errors.Action.NotFound")
}
if existingAction.State != domain.ActionStateInactive {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-J53zh", "Errors.Action.NotInactive")
}
actionAgg := ActionAggregateFromWriteModel(&existingAction.WriteModel)
events := []eventstore.EventPusher{
action.NewReactivatedEvent(ctx, actionAgg),
}
pushedEvents, err := c.eventstore.PushEvents(ctx, events...)
if err != nil {
return nil, err
}
err = AppendAndReduce(existingAction, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingAction.WriteModel), nil
}
func (c *Commands) DeleteAction(ctx context.Context, actionID, resourceOwner string, flowTypes ...domain.FlowType) (*domain.ObjectDetails, error) {
if actionID == "" || resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Gfg3g", "Errors.IDMissing")
}
existingAction, err := c.getActionWriteModelByID(ctx, actionID, resourceOwner)
if err != nil {
return nil, err
}
if !existingAction.State.Exists() {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-Dgh4h", "Errors.Action.NotFound")
}
actionAgg := ActionAggregateFromWriteModel(&existingAction.WriteModel)
events := []eventstore.EventPusher{
action.NewRemovedEvent(ctx, actionAgg, existingAction.Name),
}
orgAgg := org.NewAggregate(resourceOwner, resourceOwner).Aggregate
for _, flowType := range flowTypes {
events = append(events, org.NewTriggerActionsCascadeRemovedEvent(ctx, &orgAgg, flowType, actionID))
}
pushedEvents, err := c.eventstore.PushEvents(ctx, events...)
if err != nil {
return nil, err
}
err = AppendAndReduce(existingAction, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingAction.WriteModel), nil
}
func (c *Commands) removeActionsFromOrg(ctx context.Context, resourceOwner string) ([]eventstore.EventPusher, error) {
existingActions, err := c.getActionsByOrgWriteModelByID(ctx, resourceOwner)
if err != nil {
return nil, err
}
if len(existingActions.Actions) == 0 {
return nil, nil
}
events := make([]eventstore.EventPusher, 0, len(existingActions.Actions))
for id, name := range existingActions.Actions {
actionAgg := NewActionAggregate(id, resourceOwner)
events = append(events, action.NewRemovedEvent(ctx, actionAgg, name))
}
return events, nil
}
func (c *Commands) getActionWriteModelByID(ctx context.Context, actionID string, resourceOwner string) (*ActionWriteModel, error) {
actionWriteModel := NewActionWriteModel(actionID, resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, actionWriteModel)
if err != nil {
return nil, err
}
return actionWriteModel, nil
}
func (c *Commands) getActionsByOrgWriteModelByID(ctx context.Context, resourceOwner string) (*ActionsListByOrgModel, error) {
actionWriteModel := NewActionsListByOrgModel(resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, actionWriteModel)
if err != nil {
return nil, err
}
return actionWriteModel, nil
}

View File

@@ -0,0 +1,194 @@
package command
import (
"context"
"time"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/action"
)
type ActionWriteModel struct {
eventstore.WriteModel
Name string
Script string
Timeout time.Duration
AllowedToFail bool
State domain.ActionState
}
func NewActionWriteModel(actionID string, resourceOwner string) *ActionWriteModel {
return &ActionWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: actionID,
ResourceOwner: resourceOwner,
},
}
}
func (wm *ActionWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *action.AddedEvent:
wm.Name = e.Name
wm.Script = e.Script
wm.Timeout = e.Timeout
wm.AllowedToFail = e.AllowedToFail
wm.State = domain.ActionStateActive
case *action.ChangedEvent:
if e.Name != nil {
wm.Name = *e.Name
}
if e.Script != nil {
wm.Script = *e.Script
}
if e.Timeout != nil {
wm.Timeout = *e.Timeout
}
if e.AllowedToFail != nil {
wm.AllowedToFail = *e.AllowedToFail
}
case *action.DeactivatedEvent:
wm.State = domain.ActionStateInactive
case *action.ReactivatedEvent:
wm.State = domain.ActionStateActive
case *action.RemovedEvent:
wm.State = domain.ActionStateRemoved
}
}
return wm.WriteModel.Reduce()
}
func (wm *ActionWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(action.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(action.AddedEventType,
action.ChangedEventType,
action.DeactivatedEventType,
action.ReactivatedEventType,
action.RemovedEventType).
Builder()
}
func (wm *ActionWriteModel) NewChangedEvent(
ctx context.Context,
agg *eventstore.Aggregate,
name string,
script string,
timeout time.Duration,
allowedToFail bool,
) (*action.ChangedEvent, error) {
changes := make([]action.ActionChanges, 0)
if wm.Name != name {
changes = append(changes, action.ChangeName(name, wm.Name))
}
if wm.Script != script {
changes = append(changes, action.ChangeScript(script))
}
if wm.Timeout != timeout {
changes = append(changes, action.ChangeTimeout(timeout))
}
if wm.AllowedToFail != allowedToFail {
changes = append(changes, action.ChangeAllowedToFail(allowedToFail))
}
return action.NewChangedEvent(ctx, agg, changes)
}
func ActionAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
return eventstore.AggregateFromWriteModel(wm, action.AggregateType, action.AggregateVersion)
}
func NewActionAggregate(id, resourceOwner string) *eventstore.Aggregate {
return ActionAggregateFromWriteModel(&eventstore.WriteModel{
AggregateID: id,
ResourceOwner: resourceOwner,
})
}
type ActionExistsModel struct {
eventstore.WriteModel
actionIDs []string
checkedIDs []string
}
func NewActionsExistModel(actionIDs []string, resourceOwner string) *ActionExistsModel {
return &ActionExistsModel{
WriteModel: eventstore.WriteModel{
ResourceOwner: resourceOwner,
},
actionIDs: actionIDs,
}
}
func (wm *ActionExistsModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *action.AddedEvent:
wm.checkedIDs = append(wm.checkedIDs, e.Aggregate().ID)
case *action.RemovedEvent:
for i := len(wm.checkedIDs) - 1; i >= 0; i-- {
if wm.checkedIDs[i] == e.Aggregate().ID {
wm.checkedIDs[i] = wm.checkedIDs[len(wm.checkedIDs)-1]
wm.checkedIDs[len(wm.checkedIDs)-1] = ""
wm.checkedIDs = wm.checkedIDs[:len(wm.checkedIDs)-1]
break
}
}
}
}
return wm.WriteModel.Reduce()
}
func (wm *ActionExistsModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(action.AggregateType).
AggregateIDs(wm.actionIDs...).
EventTypes(action.AddedEventType,
action.RemovedEventType).
Builder()
}
type ActionsListByOrgModel struct {
eventstore.WriteModel
Actions map[string]string
}
func NewActionsListByOrgModel(resourceOwner string) *ActionsListByOrgModel {
return &ActionsListByOrgModel{
WriteModel: eventstore.WriteModel{
ResourceOwner: resourceOwner,
},
Actions: make(map[string]string),
}
}
func (wm *ActionsListByOrgModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *action.AddedEvent:
wm.Actions[e.Aggregate().ID] = e.Name
case *action.RemovedEvent:
delete(wm.Actions, e.Aggregate().ID)
}
}
return wm.WriteModel.Reduce()
}
func (wm *ActionsListByOrgModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(action.AggregateType).
EventTypes(action.AddedEventType,
action.RemovedEventType).
Builder()
}

View File

@@ -0,0 +1,791 @@
package command
import (
"context"
"testing"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/id"
"github.com/caos/zitadel/internal/id/mock"
"github.com/caos/zitadel/internal/repository/action"
"github.com/caos/zitadel/internal/repository/org"
"github.com/stretchr/testify/assert"
)
func TestCommands_AddAction(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
idGenerator id.Generator
}
type args struct {
ctx context.Context
addAction *domain.Action
resourceOwner string
}
type res struct {
id string
details *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"no name, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
addAction: &domain.Action{
Script: "test()",
},
resourceOwner: "org1",
},
res{
err: errors.IsErrorInvalidArgument,
},
},
{
"unique constraint failed, error",
fields{
eventstore: eventstoreExpect(t,
expectPushFailed(
errors.ThrowPreconditionFailed(nil, "id", "name already exists"),
[]*repository.Event{
eventFromEventPusher(
action.NewAddedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
"name",
"name() {};",
0,
false,
),
),
},
uniqueConstraintsFromEventConstraint(action.NewAddActionNameUniqueConstraint("name", "org1")),
),
),
idGenerator: mock.ExpectID(t, "id1"),
},
args{
ctx: context.Background(),
addAction: &domain.Action{
Name: "name",
Script: "name() {};",
},
resourceOwner: "org1",
},
res{
err: errors.IsPreconditionFailed,
},
},
{
"push ok",
fields{
eventstore: eventstoreExpect(t,
expectPush(
[]*repository.Event{
eventFromEventPusher(
action.NewAddedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
"name",
"name() {};",
0,
false,
),
),
},
uniqueConstraintsFromEventConstraint(action.NewAddActionNameUniqueConstraint("name", "org1")),
),
),
idGenerator: mock.ExpectID(t, "id1"),
},
args{
ctx: context.Background(),
addAction: &domain.Action{
Name: "name",
Script: "name() {};",
},
resourceOwner: "org1",
},
res{
id: "id1",
details: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
idGenerator: tt.fields.idGenerator,
}
id, details, err := c.AddAction(tt.args.ctx, tt.args.addAction, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.id, id)
assert.Equal(t, tt.res.details, details)
}
})
}
}
func TestCommands_ChangeAction(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
changeAction *domain.Action
resourceOwner string
}
type res struct {
details *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"id missing, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
changeAction: &domain.Action{
Name: "name",
Script: "name() {};",
},
resourceOwner: "org1",
},
res{
err: errors.IsErrorInvalidArgument,
},
},
{
"not found, error",
fields{
eventstore: eventstoreExpect(t,
expectFilter(),
),
},
args{
ctx: context.Background(),
changeAction: &domain.Action{
ObjectRoot: models.ObjectRoot{
AggregateID: "id1",
},
Name: "name",
Script: "name() {};",
},
resourceOwner: "org1",
},
res{
err: errors.IsNotFound,
},
},
{
"no changes, error",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
action.NewAddedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
"name",
"name() {};",
0,
false,
),
),
),
),
},
args{
ctx: context.Background(),
changeAction: &domain.Action{
ObjectRoot: models.ObjectRoot{
AggregateID: "id1",
},
Name: "name",
Script: "name() {};",
},
resourceOwner: "org1",
},
res{
err: errors.IsPreconditionFailed,
},
},
{
"unique constraint failed, error",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
action.NewAddedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
"name",
"name() {};",
0,
false,
),
),
),
expectPushFailed(
errors.ThrowPreconditionFailed(nil, "id", "name already exists"),
[]*repository.Event{
eventFromEventPusher(
func() *action.ChangedEvent {
event, _ := action.NewChangedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
[]action.ActionChanges{
action.ChangeName("name2", "name"),
action.ChangeScript("name2() {};"),
},
)
return event
}(),
),
},
uniqueConstraintsFromEventConstraint(action.NewRemoveActionNameUniqueConstraint("name", "org1")),
uniqueConstraintsFromEventConstraint(action.NewAddActionNameUniqueConstraint("name2", "org1")),
),
),
},
args{
ctx: context.Background(),
changeAction: &domain.Action{
ObjectRoot: models.ObjectRoot{
AggregateID: "id1",
},
Name: "name2",
Script: "name2() {};",
},
resourceOwner: "org1",
},
res{
err: errors.IsPreconditionFailed,
},
},
{
"push ok",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
action.NewAddedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
"name",
"name() {};",
0,
false,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
func() *action.ChangedEvent {
event, _ := action.NewChangedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
[]action.ActionChanges{
action.ChangeName("name2", "name"),
action.ChangeScript("name2() {};"),
},
)
return event
}(),
),
},
uniqueConstraintsFromEventConstraint(action.NewRemoveActionNameUniqueConstraint("name", "org1")),
uniqueConstraintsFromEventConstraint(action.NewAddActionNameUniqueConstraint("name2", "org1")),
),
),
},
args{
ctx: context.Background(),
changeAction: &domain.Action{
ObjectRoot: models.ObjectRoot{
AggregateID: "id1",
},
Name: "name2",
Script: "name2() {};",
},
resourceOwner: "org1",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
}
details, err := c.ChangeAction(tt.args.ctx, tt.args.changeAction, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.details, details)
}
})
}
}
func TestCommands_DeactivateAction(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
actionID string
resourceOwner string
}
type res struct {
details *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"id missing, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
actionID: "",
resourceOwner: "org1",
},
res{
err: errors.IsErrorInvalidArgument,
},
},
{
"not found, error",
fields{
eventstore: eventstoreExpect(t,
expectFilter(),
),
},
args{
ctx: context.Background(),
actionID: "id1",
resourceOwner: "org1",
},
res{
err: errors.IsNotFound,
},
},
{
"not active, error",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
action.NewAddedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
"name",
"name() {};",
0,
false,
),
),
eventFromEventPusher(
action.NewDeactivatedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
),
),
),
),
},
args{
ctx: context.Background(),
actionID: "id1",
resourceOwner: "org1",
},
res{
err: errors.IsPreconditionFailed,
},
},
{
"deactivate ok",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
action.NewAddedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
"name",
"name() {};",
0,
false,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
action.NewDeactivatedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
),
),
},
),
),
},
args{
ctx: context.Background(),
actionID: "id1",
resourceOwner: "org1",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
}
details, err := c.DeactivateAction(tt.args.ctx, tt.args.actionID, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.details, details)
}
})
}
}
func TestCommands_ReactivateAction(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
actionID string
resourceOwner string
}
type res struct {
details *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"id missing, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
actionID: "",
resourceOwner: "org1",
},
res{
err: errors.IsErrorInvalidArgument,
},
},
{
"not found, error",
fields{
eventstore: eventstoreExpect(t,
expectFilter(),
),
},
args{
ctx: context.Background(),
actionID: "id1",
resourceOwner: "org1",
},
res{
err: errors.IsNotFound,
},
},
{
"not inactive, error",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
action.NewAddedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
"name",
"name() {};",
0,
false,
),
),
),
),
},
args{
ctx: context.Background(),
actionID: "id1",
resourceOwner: "org1",
},
res{
err: errors.IsPreconditionFailed,
},
},
{
"reactivate ok",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
action.NewAddedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
"name",
"name() {};",
0,
false,
),
),
eventFromEventPusher(
action.NewDeactivatedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
action.NewReactivatedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
),
),
},
),
),
},
args{
ctx: context.Background(),
actionID: "id1",
resourceOwner: "org1",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
}
details, err := c.ReactivateAction(tt.args.ctx, tt.args.actionID, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.details, details)
}
})
}
}
func TestCommands_DeleteAction(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
id string
resourceOwner string
flowTypes []domain.FlowType
}
type res struct {
details *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"id or resourceOwner emtpy, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
id: "",
resourceOwner: "",
},
res{
err: errors.IsErrorInvalidArgument,
},
},
{
"action not found, error",
fields{
eventstore: eventstoreExpect(t,
expectFilter(),
),
},
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "org1",
},
res{
err: errors.IsNotFound,
},
},
{
"remove ok",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
action.NewAddedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
"name",
"name() {};",
0,
false,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
action.NewRemovedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
"name",
),
),
},
uniqueConstraintsFromEventConstraint(action.NewRemoveActionNameUniqueConstraint("name", "org1")),
),
),
},
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "org1",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
"remove with used action ok",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
action.NewAddedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
"name",
"name() {};",
0,
false,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
action.NewRemovedEvent(context.Background(),
&action.NewAggregate("id1", "org1").Aggregate,
"name",
),
),
eventFromEventPusher(
org.NewTriggerActionsCascadeRemovedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
domain.FlowTypeExternalAuthentication,
"id1",
),
),
},
uniqueConstraintsFromEventConstraint(action.NewRemoveActionNameUniqueConstraint("name", "org1")),
),
),
},
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "org1",
flowTypes: []domain.FlowType{
domain.FlowTypeExternalAuthentication,
},
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
}
details, err := c.DeleteAction(tt.args.ctx, tt.args.id, tt.args.resourceOwner, tt.args.flowTypes...)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.details, details)
}
})
}
}

View File

@@ -45,6 +45,7 @@ func (c *Commands) SetOrgFeatures(ctx context.Context, resourceOwner string, fea
features.CustomTextMessage,
features.CustomTextLogin,
features.LockoutPolicy,
features.Actions,
)
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged")
@@ -176,6 +177,15 @@ func (c *Commands) ensureOrgSettingsToFeatures(ctx context.Context, orgID string
events = append(events, removeOrgUserMetadatas...)
}
}
if !features.Actions {
removeOrgActions, err := c.removeActionsFromOrg(ctx, orgID)
if err != nil {
return nil, err
}
if len(removeOrgActions) > 0 {
events = append(events, removeOrgActions...)
}
}
return events, nil
}

View File

@@ -78,7 +78,8 @@ func (wm *OrgFeaturesWriteModel) NewSetEvent(
metadataUser,
customTextMessage,
customTextLogin,
lockoutPolicy bool,
lockoutPolicy,
actions bool,
) (*org.FeaturesSetEvent, bool) {
changes := make([]features.FeaturesChanges, 0)
@@ -143,6 +144,9 @@ func (wm *OrgFeaturesWriteModel) NewSetEvent(
if wm.LockoutPolicy != lockoutPolicy {
changes = append(changes, features.ChangeLockoutPolicy(lockoutPolicy))
}
if wm.Actions != actions {
changes = append(changes, features.ChangeActions(actions))
}
if len(changes) == 0 {
return nil, false

View File

@@ -275,6 +275,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
),
),
expectFilter(),
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -307,6 +308,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
PrivacyPolicy: false,
MetadataUser: false,
LockoutPolicy: false,
Actions: false,
},
},
res: res{
@@ -472,6 +474,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
),
),
expectFilter(),
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -509,6 +512,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
MetadataUser: false,
PrivacyPolicy: false,
LockoutPolicy: false,
Actions: false,
},
},
res: res{
@@ -681,6 +685,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
),
),
expectFilter(),
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -721,6 +726,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
MetadataUser: false,
PrivacyPolicy: false,
LockoutPolicy: false,
Actions: false,
},
},
res: res{
@@ -900,6 +906,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
),
),
expectFilter(),
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -943,6 +950,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
MetadataUser: false,
PrivacyPolicy: false,
LockoutPolicy: false,
Actions: false,
},
},
res: res{
@@ -1174,6 +1182,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
),
),
expectFilter(),
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -1234,6 +1243,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
MetadataUser: false,
PrivacyPolicy: false,
LockoutPolicy: false,
Actions: false,
},
},
res: res{
@@ -1387,6 +1397,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
),
),
),
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -1422,6 +1433,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) {
PrivacyPolicy: false,
MetadataUser: false,
LockoutPolicy: false,
Actions: false,
},
},
res: res{
@@ -1635,6 +1647,7 @@ func TestCommandSide_RemoveOrgFeatures(t *testing.T) {
),
),
expectFilter(),
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(

View File

@@ -0,0 +1,83 @@
package command
import (
"context"
"reflect"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/repository/org"
)
func (c *Commands) ClearFlow(ctx context.Context, flowType domain.FlowType, resourceOwner string) (*domain.ObjectDetails, error) {
if !flowType.Valid() || resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Dfw2h", "Errors.Flow.FlowTypeMissing")
}
existingFlow, err := c.getOrgFlowWriteModelByType(ctx, flowType, resourceOwner)
if err != nil {
return nil, err
}
if len(existingFlow.Triggers) == 0 {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-DgGh3", "Errors.Flow.Empty")
}
orgAgg := OrgAggregateFromWriteModel(&existingFlow.WriteModel)
pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewFlowClearedEvent(ctx, orgAgg, flowType))
if err != nil {
return nil, err
}
err = AppendAndReduce(existingFlow, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingFlow.WriteModel), nil
}
func (c *Commands) SetTriggerActions(ctx context.Context, flowType domain.FlowType, triggerType domain.TriggerType, actionIDs []string, resourceOwner string) (*domain.ObjectDetails, error) {
if !flowType.Valid() || !triggerType.Valid() || resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Dfhj5", "Errors.Flow.FlowTypeMissing")
}
if !flowType.HasTrigger(triggerType) {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Dfgh6", "Errors.Flow.WrongTriggerType")
}
existingFlow, err := c.getOrgFlowWriteModelByType(ctx, flowType, resourceOwner)
if err != nil {
return nil, err
}
if reflect.DeepEqual(existingFlow.Triggers[triggerType], actionIDs) {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Nfh52", "Errors.Flow.NoChanges")
}
if len(actionIDs) > 0 {
exists, err := c.actionsIDsExist(ctx, actionIDs, resourceOwner)
if err != nil {
return nil, err
}
if !exists {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-dg422", "Errors.Flow.ActionIDsNotExist")
}
}
orgAgg := OrgAggregateFromWriteModel(&existingFlow.WriteModel)
pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewTriggerActionsSetEvent(ctx, orgAgg, flowType, triggerType, actionIDs))
if err != nil {
return nil, err
}
err = AppendAndReduce(existingFlow, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingFlow.WriteModel), nil
}
func (c *Commands) getOrgFlowWriteModelByType(ctx context.Context, flowType domain.FlowType, resourceOwner string) (*OrgFlowWriteModel, error) {
flowWriteModel := NewOrgFlowWriteModel(flowType, resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, flowWriteModel)
if err != nil {
return nil, err
}
return flowWriteModel, nil
}
func (c *Commands) actionsIDsExist(ctx context.Context, ids []string, resourceOwner string) (bool, error) {
actionIDsModel := NewActionsExistModel(ids, resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, actionIDsModel)
return len(actionIDsModel.actionIDs) == len(actionIDsModel.checkedIDs), err
}

View File

@@ -0,0 +1,54 @@
package command
import (
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/org"
)
type OrgFlowWriteModel struct {
FlowWriteModel
}
func NewOrgFlowWriteModel(flowType domain.FlowType, resourceOwner string) *OrgFlowWriteModel {
return &OrgFlowWriteModel{
FlowWriteModel: *NewFlowWriteModel(flowType, resourceOwner),
}
}
func (wm *OrgFlowWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *org.TriggerActionsSetEvent:
if e.FlowType != wm.FlowType {
continue
}
wm.FlowWriteModel.AppendEvents(&e.TriggerActionsSetEvent)
case *org.TriggerActionsCascadeRemovedEvent:
if e.FlowType != wm.FlowType {
continue
}
wm.FlowWriteModel.AppendEvents(&e.TriggerActionsCascadeRemovedEvent)
case *org.FlowClearedEvent:
if e.FlowType != wm.FlowType {
continue
}
wm.FlowWriteModel.AppendEvents(&e.FlowClearedEvent)
}
}
}
func (wm *OrgFlowWriteModel) Reduce() error {
return wm.FlowWriteModel.Reduce()
}
func (wm *OrgFlowWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(org.AggregateType).
EventTypes(org.TriggerActionsSetEventType,
org.TriggerActionsCascadeRemovedEventType,
org.FlowClearedEventType).
Builder()
}

View File

@@ -0,0 +1,286 @@
package command
import (
"context"
"testing"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/action"
"github.com/caos/zitadel/internal/repository/org"
"github.com/stretchr/testify/assert"
)
func TestCommands_ClearFlow(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
flowType domain.FlowType
resourceOwner string
}
type res struct {
details *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"invalid flow type, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
flowType: domain.FlowTypeUnspecified,
resourceOwner: "org1",
},
res{
details: nil,
err: errors.IsErrorInvalidArgument,
},
},
{
"already empty, error",
fields{
eventstore: eventstoreExpect(t,
expectFilter(),
),
},
args{
ctx: context.Background(),
flowType: domain.FlowTypeExternalAuthentication,
resourceOwner: "org1",
},
res{
details: nil,
err: errors.IsPreconditionFailed,
},
},
{
"clear ok",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
org.NewTriggerActionsSetEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
domain.FlowTypeExternalAuthentication,
domain.TriggerTypePostAuthentication,
[]string{"actionID1"},
),
),
),
expectPush(
eventPusherToEvents(
org.NewFlowClearedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
domain.FlowTypeExternalAuthentication,
),
),
),
),
},
args{
ctx: context.Background(),
flowType: domain.FlowTypeExternalAuthentication,
resourceOwner: "org1",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
},
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
}
details, err := c.ClearFlow(tt.args.ctx, tt.args.flowType, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.details, details)
}
})
}
}
func TestCommands_SetTriggerActions(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
flowType domain.FlowType
resourceOwner string
triggerType domain.TriggerType
actionIDs []string
}
type res struct {
details *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"invalid flow type, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
flowType: domain.FlowTypeUnspecified,
triggerType: domain.TriggerTypePostAuthentication,
actionIDs: []string{"actionID1"},
resourceOwner: "org1",
},
res{
details: nil,
err: errors.IsErrorInvalidArgument,
},
},
//TODO: combination not possible at the moment, add when more flow types available
//{
// "impossible flow / trigger type, error",
// fields{
// eventstore: eventstoreExpect(t,),
// },
// args{
// ctx: context.Background(),
// flowType: domain.FlowTypeUnspecified,
// triggerType: domain.TriggerTypePostAuthentication,
// actionIDs: []string{"actionID1"},
// resourceOwner: "org1",
// },
// res{
// details: nil,
// err: errors.IsErrorInvalidArgument,
// },
//},
{
"no changes, error",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
org.NewTriggerActionsSetEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
domain.FlowTypeExternalAuthentication,
domain.TriggerTypePostAuthentication,
[]string{"actionID1"},
),
),
),
),
},
args{
ctx: context.Background(),
flowType: domain.FlowTypeExternalAuthentication,
triggerType: domain.TriggerTypePostAuthentication,
actionIDs: []string{"actionID1"},
resourceOwner: "org1",
},
res{
details: nil,
err: errors.IsPreconditionFailed,
},
},
{
"actionID not exists, error",
fields{
eventstore: eventstoreExpect(t,
expectFilter(),
expectFilter(),
),
},
args{
ctx: context.Background(),
flowType: domain.FlowTypeExternalAuthentication,
triggerType: domain.TriggerTypePostAuthentication,
actionIDs: []string{"actionID1"},
resourceOwner: "org1",
},
res{
details: nil,
err: errors.IsPreconditionFailed,
},
},
{
"set ok",
fields{
eventstore: eventstoreExpect(t,
expectFilter(),
expectFilter(
eventFromEventPusher(
action.NewAddedEvent(context.Background(),
&action.NewAggregate("action1", "org1").Aggregate,
"actionID1",
"function(ctx, api) action {};",
0,
false,
),
),
),
expectPush(
eventPusherToEvents(
org.NewTriggerActionsSetEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
domain.FlowTypeExternalAuthentication,
domain.TriggerTypePostAuthentication,
[]string{"actionID1"},
),
),
),
),
},
args{
ctx: context.Background(),
flowType: domain.FlowTypeExternalAuthentication,
triggerType: domain.TriggerTypePostAuthentication,
actionIDs: []string{"actionID1"},
resourceOwner: "org1",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
},
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
}
details, err := c.SetTriggerActions(tt.args.ctx, tt.args.flowType, tt.args.triggerType, tt.args.actionIDs, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.details, details)
}
})
}
}

View File

@@ -2,9 +2,10 @@ package command
import (
"context"
"github.com/caos/zitadel/internal/eventstore"
"reflect"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/repository/usergrant"
@@ -29,10 +30,6 @@ func (c *Commands) AddUserGrant(ctx context.Context, usergrant *domain.UserGrant
}
func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string) (pusher eventstore.EventPusher, _ *UserGrantWriteModel, err error) {
err = checkExplicitProjectPermission(ctx, userGrant.ProjectGrantID, userGrant.ProjectID)
if err != nil {
return nil, nil, err
}
if !userGrant.IsValid() {
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4M0fs", "Errors.UserGrant.Invalid")
}

View File

@@ -2,6 +2,8 @@ package command
import (
"context"
"testing"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
@@ -15,7 +17,6 @@ import (
"github.com/caos/zitadel/internal/repository/usergrant"
"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
"testing"
)
func TestCommandSide_AddUserGrant(t *testing.T) {
@@ -38,24 +39,6 @@ func TestCommandSide_AddUserGrant(t *testing.T) {
args args
res res
}{
{
name: "invalid permissions, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
userGrant: &domain.UserGrant{
UserID: "user1",
},
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsPermissionDenied,
},
},
{
name: "invalid usergrant, error",
fields: fields{

39
internal/domain/action.go Normal file
View File

@@ -0,0 +1,39 @@
package domain
import (
"time"
"github.com/caos/zitadel/internal/eventstore/v1/models"
)
type Action struct {
models.ObjectRoot
Name string
Script string
Timeout time.Duration
AllowedToFail bool
State ActionState
}
func (a *Action) IsValid() bool {
return a.Name != ""
}
type ActionState int32
const (
ActionStateUnspecified ActionState = iota
ActionStateActive
ActionStateInactive
ActionStateRemoved
actionStateCount
)
func (s ActionState) Valid() bool {
return s >= 0 && s < actionStateCount
}
func (s ActionState) Exists() bool {
return s != ActionStateUnspecified && s != ActionStateRemoved
}

View File

@@ -68,6 +68,7 @@ type ExternalUser struct {
PreferredLanguage language.Tag
Phone string
IsPhoneVerified bool
Metadatas []*Metadata
}
type Prompt int32

View File

@@ -26,6 +26,7 @@ const (
FeatureCustomTextMessage = FeatureCustomText + ".message"
FeatureCustomTextLogin = FeatureCustomText + ".login"
FeatureMetadataUser = FeatureMetadata + ".user"
FeatureActions = "actions"
)
type Features struct {
@@ -53,6 +54,7 @@ type Features struct {
PrivacyPolicy bool
MetadataUser bool
LockoutPolicy bool
Actions bool
}
type FeaturesState int32

52
internal/domain/flow.go Normal file
View File

@@ -0,0 +1,52 @@
package domain
type FlowState int32
const (
FlowStateActive FlowState = iota
FlowStateInactive
flowStateCount
)
func (s FlowState) Valid() bool {
return s >= 0 && s < flowStateCount
}
type FlowType int32
const (
FlowTypeUnspecified FlowType = iota
FlowTypeExternalAuthentication
flowTypeCount
)
func (s FlowType) Valid() bool {
return s > 0 && s < flowTypeCount
}
func (s FlowType) HasTrigger(triggerType TriggerType) bool {
switch triggerType {
case TriggerTypePostAuthentication:
return s == FlowTypeExternalAuthentication
case TriggerTypePreCreation:
return s == FlowTypeExternalAuthentication
case TriggerTypePostCreation:
return s == FlowTypeExternalAuthentication
default:
return false
}
}
type TriggerType int32
const (
TriggerTypeUnspecified TriggerType = iota
TriggerTypePostAuthentication
TriggerTypePreCreation
TriggerTypePostCreation
triggerTypeCount
)
func (s TriggerType) Valid() bool {
return s >= 0 && s < triggerTypeCount
}

View File

@@ -33,6 +33,7 @@ type FeaturesView struct {
CustomTextMessage bool
CustomTextLogin bool
LockoutPolicy bool
Actions bool
}
func (f *FeaturesView) FeatureList() []string {
@@ -82,6 +83,9 @@ func (f *FeaturesView) FeatureList() []string {
if f.LockoutPolicy {
list = append(list, domain.FeatureLockoutPolicy)
}
if f.Actions {
list = append(list, domain.FeatureActions)
}
return list
}

View File

@@ -47,6 +47,7 @@ type FeaturesView struct {
CustomTextMessage bool `json:"customTextMessage" gorm:"column:custom_text_message"`
CustomTextLogin bool `json:"customTextLogin" gorm:"column:custom_text_login"`
LockoutPolicy bool `json:"lockoutPolicy" gorm:"column:lockout_policy"`
Actions bool `json:"actions" gorm:"column:actions"`
}
func FeaturesToModel(features *FeaturesView) *features_model.FeaturesView {
@@ -76,6 +77,7 @@ func FeaturesToModel(features *FeaturesView) *features_model.FeaturesView {
CustomTextMessage: features.CustomTextMessage,
CustomTextLogin: features.CustomTextLogin,
LockoutPolicy: features.LockoutPolicy,
Actions: features.Actions,
}
}

104
internal/query/action.go Normal file
View File

@@ -0,0 +1,104 @@
package query
import (
"context"
"time"
"github.com/Masterminds/squirrel"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
)
var actionsQuery = squirrel.StatementBuilder.Select("creation_date", "change_date", "resource_owner", "sequence", "id", "action_state", "name", "script", "timeout", "allowed_to_fail").
From("zitadel.projections.actions").PlaceholderFormat(squirrel.Dollar)
func (q *Queries) GetAction(ctx context.Context, id string, orgID string) (*Action, error) {
idQuery, _ := newActionIDSearchQuery(id)
actions, err := q.SearchActions(ctx, &ActionSearchQueries{Queries: []SearchQuery{idQuery}})
if err != nil {
return nil, err
}
if len(actions) != 1 {
return nil, errors.ThrowNotFound(nil, "QUERY-dft2g", "Errors.Action.NotFound")
}
return actions[0], err
}
func (q *Queries) SearchActions(ctx context.Context, query *ActionSearchQueries) ([]*Action, error) {
stmt, args, err := query.ToQuery(actionsQuery).ToSql()
if err != nil {
return nil, errors.ThrowInvalidArgument(err, "QUERY-wQ3by", "Errors.orgs.invalid.request")
}
rows, err := q.client.QueryContext(ctx, stmt, args...)
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-M6mYN", "Errors.orgs.internal")
}
actions := []*Action{}
for rows.Next() {
org := new(Action)
rows.Scan(
&org.CreationDate,
&org.ChangeDate,
&org.ResourceOwner,
&org.Sequence,
&org.ID,
&org.State,
&org.Name,
&org.Script,
&org.Timeout,
&org.AllowedToFail,
)
actions = append(actions, org)
}
if err := rows.Err(); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-pA0Wj", "Errors.actions.internal")
}
return actions, nil
}
type Action struct {
ID string `col:"id"`
CreationDate time.Time `col:"creation_date"`
ChangeDate time.Time `col:"change_date"`
ResourceOwner string `col:"resource_owner"`
State domain.ActionState `col:"action_state"`
Sequence uint64 `col:"sequence"`
Name string `col:"name"`
Script string `col:"script"`
Timeout time.Duration `col:"-"`
AllowedToFail bool `col:"-"`
}
type ActionSearchQueries struct {
SearchRequest
Queries []SearchQuery
}
func (q *ActionSearchQueries) ToQuery(query squirrel.SelectBuilder) squirrel.SelectBuilder {
query = q.SearchRequest.ToQuery(query)
for _, q := range q.Queries {
query = q.ToQuery(query)
}
return query
}
func NewActionResourceOwnerQuery(id string) (SearchQuery, error) {
return NewTextQuery("resource_owner", id, TextEquals)
}
func NewActionNameSearchQuery(method TextComparison, value string) (SearchQuery, error) {
return NewTextQuery("name", value, method)
}
func NewActionStateSearchQuery(value domain.ActionState) (SearchQuery, error) {
return NewIntQuery("state", int(value), IntEquals)
}
func newActionIDSearchQuery(id string) (SearchQuery, error) {
return NewTextQuery("id", id, TextEquals)
}

View File

@@ -0,0 +1,173 @@
package query
import (
"context"
"time"
"github.com/Masterminds/squirrel"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
)
func (q *Queries) GetActionsByFlowAndTriggerType(ctx context.Context, flowType domain.FlowType, triggerType domain.TriggerType) ([]*Action, error) {
flowTypeQuery, _ := NewTriggerActionFlowTypeSearchQuery(flowType)
triggerTypeQuery, _ := NewTriggerActionTriggerTypeSearchQuery(triggerType)
return q.SearchActionsFromFlow(ctx, &TriggerActionSearchQueries{Queries: []SearchQuery{flowTypeQuery, triggerTypeQuery}})
}
var triggerActionsQuery = squirrel.StatementBuilder.Select("creation_date", "change_date", "resource_owner", "sequence", "action_id", "name", "script", "trigger_type", "trigger_sequence").
From("zitadel.projections.flows_actions_triggers").PlaceholderFormat(squirrel.Dollar)
func (q *Queries) SearchActionsFromFlow(ctx context.Context, query *TriggerActionSearchQueries) ([]*Action, error) {
stmt, args, err := query.ToQuery(triggerActionsQuery).OrderBy("flow_type", "trigger_type", "trigger_sequence").ToSql()
if err != nil {
return nil, errors.ThrowInvalidArgument(err, "QUERY-wQ3by", "Errors.orgs.invalid.request")
}
rows, err := q.client.QueryContext(ctx, stmt, args...)
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-M6mYN", "Errors.orgs.internal")
}
actions := []*Action{}
for rows.Next() {
action := new(Action)
var triggerType domain.TriggerType
var triggerSequence int
rows.Scan(
&action.CreationDate,
&action.ChangeDate,
&action.ResourceOwner,
&action.Sequence,
//&action.State, //TODO: state in next release
&action.ID,
&action.Name,
&action.Script,
&triggerType,
&triggerSequence,
)
actions = append(actions, action)
}
if err := rows.Err(); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-pA0Wj", "Errors.actions.internal")
}
return actions, nil
}
func (q *Queries) GetFlow(ctx context.Context, flowType domain.FlowType) (*Flow, error) {
flowTypeQuery, _ := NewTriggerActionFlowTypeSearchQuery(flowType)
return q.SearchFlow(ctx, &TriggerActionSearchQueries{Queries: []SearchQuery{flowTypeQuery}})
}
func (q *Queries) SearchFlow(ctx context.Context, query *TriggerActionSearchQueries) (*Flow, error) {
stmt, args, err := query.ToQuery(triggerActionsQuery.OrderBy("flow_type", "trigger_type", "trigger_sequence")).ToSql()
if err != nil {
return nil, errors.ThrowInvalidArgument(err, "QUERY-wQ3by", "Errors.orgs.invalid.request")
}
rows, err := q.client.QueryContext(ctx, stmt, args...)
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-M6mYN", "Errors.orgs.internal")
}
flow := &Flow{
TriggerActions: make(map[domain.TriggerType][]*Action),
}
for rows.Next() {
action := new(Action)
var triggerType domain.TriggerType
var triggerSequence int
rows.Scan(
&action.CreationDate,
&action.ChangeDate,
&action.ResourceOwner,
&action.Sequence,
//&action.State, //TODO: state in next release
&action.ID,
&action.Name,
&action.Script,
&triggerType,
&triggerSequence,
)
flow.TriggerActions[triggerType] = append(flow.TriggerActions[triggerType], action)
}
if err := rows.Err(); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-pA0Wj", "Errors.actions.internal")
}
return flow, nil
}
func (q *Queries) GetFlowTypesOfActionID(ctx context.Context, actionID string) ([]domain.FlowType, error) {
actionIDQuery, _ := NewTriggerActionActionIDSearchQuery(actionID)
query := &TriggerActionSearchQueries{Queries: []SearchQuery{actionIDQuery}}
stmt, args, err := query.ToQuery(
squirrel.StatementBuilder.
Select("flow_type").
From("zitadel.projections.flows_actions_triggers").
PlaceholderFormat(squirrel.Dollar)).ToSql()
if err != nil {
return nil, errors.ThrowInvalidArgument(err, "QUERY-wQ3by", "Errors.orgs.invalid.request")
}
rows, err := q.client.QueryContext(ctx, stmt, args...)
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-M6mYN", "Errors.orgs.internal")
}
flowTypes := make([]domain.FlowType, 0)
for rows.Next() {
var flow_type domain.FlowType
rows.Scan(
&flow_type,
)
flowTypes = append(flowTypes, flow_type)
}
if err := rows.Err(); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-pA0Wj", "Errors.actions.internal")
}
return flowTypes, nil
}
type Flow struct {
ID string `col:"id"`
CreationDate time.Time `col:"creation_date"`
ChangeDate time.Time `col:"change_date"`
ResourceOwner string `col:"resource_owner"`
Sequence uint64 `col:"sequence"`
Type domain.FlowType `col:"flow_type"`
TriggerActions map[domain.TriggerType][]*Action
}
type TriggerActionSearchQueries struct {
SearchRequest
Queries []SearchQuery
}
func (q *TriggerActionSearchQueries) ToQuery(query squirrel.SelectBuilder) squirrel.SelectBuilder {
query = q.SearchRequest.ToQuery(query)
for _, q := range q.Queries {
query = q.ToQuery(query)
}
return query
}
func NewTriggerActionTriggerTypeSearchQuery(value domain.TriggerType) (SearchQuery, error) {
return NewIntQuery("trigger_type", int(value), IntEquals)
}
func NewTriggerActionFlowTypeSearchQuery(value domain.FlowType) (SearchQuery, error) {
return NewIntQuery("flow_type", int(value), IntEquals)
}
func NewTriggerActionActionIDSearchQuery(actionID string) (SearchQuery, error) {
return NewTextQuery("action_id", actionID, TextEquals)
}

View File

@@ -0,0 +1,174 @@
package projection
import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/handler"
"github.com/caos/zitadel/internal/eventstore/handler/crdb"
"github.com/caos/zitadel/internal/repository/action"
)
type ActionProjection struct {
crdb.StatementHandler
}
func NewActionProjection(ctx context.Context, config crdb.StatementHandlerConfig) *ActionProjection {
p := &ActionProjection{}
config.ProjectionName = "projections.actions"
config.Reducers = p.reducers()
p.StatementHandler = crdb.NewStatementHandler(ctx, config)
return p
}
func (p *ActionProjection) reducers() []handler.AggregateReducer {
return []handler.AggregateReducer{
{
Aggregate: action.AggregateType,
EventRedusers: []handler.EventReducer{
{
Event: action.AddedEventType,
Reduce: p.reduceActionAdded,
},
{
Event: action.ChangedEventType,
Reduce: p.reduceActionChanged,
},
{
Event: action.DeactivatedEventType,
Reduce: p.reduceActionDeactivated,
},
{
Event: action.ReactivatedEventType,
Reduce: p.reduceActionReactivated,
},
{
Event: action.RemovedEventType,
Reduce: p.reduceActionRemoved,
},
},
},
}
}
const (
actionIDCol = "id"
actionCreationDateCol = "creation_date"
actionChangeDateCol = "change_date"
actionResourceOwnerCol = "resource_owner"
actionStateCol = "action_state"
actionSequenceCol = "sequence"
actionNameCol = "name"
actionScriptCol = "script"
actionTimeoutCol = "timeout"
actionAllowedToFailCol = "allowed_to_fail"
)
func (p *ActionProjection) reduceActionAdded(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*action.AddedEvent)
if !ok {
logging.LogWithFields("HANDL-zWCk3", "seq", event.Sequence, "expectedType", action.AddedEventType).Error("was not an event")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-uYq4r", "reduce.wrong.event.type")
}
return crdb.NewCreateStatement(
e,
[]handler.Column{
handler.NewCol(actionIDCol, e.Aggregate().ID),
handler.NewCol(actionCreationDateCol, e.CreationDate()),
handler.NewCol(actionChangeDateCol, e.CreationDate()),
handler.NewCol(actionResourceOwnerCol, e.Aggregate().ResourceOwner),
handler.NewCol(actionSequenceCol, e.Sequence()),
handler.NewCol(actionNameCol, e.Name),
handler.NewCol(actionScriptCol, e.Script),
handler.NewCol(actionTimeoutCol, e.Timeout),
handler.NewCol(actionAllowedToFailCol, e.AllowedToFail),
handler.NewCol(actionStateCol, domain.ActionStateActive),
},
), nil
}
func (p *ActionProjection) reduceActionChanged(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*action.ChangedEvent)
if !ok {
logging.LogWithFields("HANDL-q4oq8", "seq", event.Sequence, "expected", action.ChangedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-Bg8oM", "reduce.wrong.event.type")
}
values := []handler.Column{
handler.NewCol(actionChangeDateCol, e.CreationDate()),
handler.NewCol(actionSequenceCol, e.Sequence()),
}
if e.Name != nil {
values = append(values, handler.NewCol(actionNameCol, *e.Name))
}
if e.Script != nil {
values = append(values, handler.NewCol(actionScriptCol, *e.Script))
}
if e.Timeout != nil {
values = append(values, handler.NewCol(actionTimeoutCol, *e.Timeout))
}
if e.AllowedToFail != nil {
values = append(values, handler.NewCol(actionAllowedToFailCol, *e.AllowedToFail))
}
return crdb.NewUpdateStatement(
e,
values,
[]handler.Condition{
handler.NewCond(actionIDCol, e.Aggregate().ID),
},
), nil
}
func (p *ActionProjection) reduceActionDeactivated(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*action.DeactivatedEvent)
if !ok {
logging.LogWithFields("HANDL-1gwdc", "seq", event.Sequence, "expectedType", action.DeactivatedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-BApK4", "reduce.wrong.event.type")
}
return crdb.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(actionChangeDateCol, e.CreationDate()),
handler.NewCol(actionSequenceCol, e.Sequence()),
handler.NewCol(actionStateCol, domain.ActionStateInactive),
},
[]handler.Condition{
handler.NewCond(actionIDCol, e.Aggregate().ID),
},
), nil
}
func (p *ActionProjection) reduceActionReactivated(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*action.ReactivatedEvent)
if !ok {
logging.LogWithFields("HANDL-Vjwiy", "seq", event.Sequence, "expectedType", action.ReactivatedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-o37De", "reduce.wrong.event.type")
}
return crdb.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(actionChangeDateCol, e.CreationDate()),
handler.NewCol(actionSequenceCol, e.Sequence()),
handler.NewCol(actionStateCol, domain.ActionStateActive),
},
[]handler.Condition{
handler.NewCond(actionIDCol, e.Aggregate().ID),
},
), nil
}
func (p *ActionProjection) reduceActionRemoved(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*action.RemovedEvent)
if !ok {
logging.LogWithFields("HANDL-79OhB", "seq", event.Sequence, "expectedType", action.RemovedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-4TbKT", "reduce.wrong.event.type")
}
return crdb.NewDeleteStatement(
e,
[]handler.Condition{
handler.NewCond(actionIDCol, e.Aggregate().ID),
},
), nil
}

View File

@@ -0,0 +1,184 @@
package flow
import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/handler"
"github.com/caos/zitadel/internal/eventstore/handler/crdb"
"github.com/caos/zitadel/internal/repository/action"
"github.com/caos/zitadel/internal/repository/org"
)
type FlowProjection struct {
crdb.StatementHandler
}
func NewFlowProjection(ctx context.Context, config crdb.StatementHandlerConfig) *FlowProjection {
p := &FlowProjection{}
config.ProjectionName = "projections.flows"
config.Reducers = p.reducers()
p.StatementHandler = crdb.NewStatementHandler(ctx, config)
return p
}
func (p *FlowProjection) reducers() []handler.AggregateReducer {
return []handler.AggregateReducer{
{
Aggregate: org.AggregateType,
EventRedusers: []handler.EventReducer{
{
Event: org.TriggerActionsSetEventType,
Reduce: p.reduceTriggerActionsSetEventType,
},
{
Event: org.FlowClearedEventType,
Reduce: p.reduceFlowClearedEventType,
},
},
},
{
Aggregate: action.AggregateType,
EventRedusers: []handler.EventReducer{
{
Event: action.AddedEventType,
Reduce: p.reduceFlowActionAdded,
},
{
Event: action.ChangedEventType,
Reduce: p.reduceFlowActionChanged,
},
{
Event: action.RemovedEventType,
Reduce: p.reduceFlowActionRemoved,
},
},
},
}
}
const (
triggerTableSuffix = "triggers"
flowTypeCol = "flow_type"
flowTriggerTypeCol = "trigger_type"
flowResourceOwnerCol = "resource_owner"
flowActionTriggerSequenceCol = "trigger_sequence"
flowActionIDCol = "action_id"
actionTableSuffix = "actions"
actionIDCol = "id"
actionCreationDateCol = "creation_date"
actionChangeDateCol = "change_date"
actionResourceOwnerCol = "resource_owner"
actionSequenceCol = "sequence"
actionNameCol = "name"
actionScriptCol = "script"
)
func (p *FlowProjection) reduceTriggerActionsSetEventType(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*org.TriggerActionsSetEvent)
if !ok {
logging.LogWithFields("HANDL-zWCk3", "seq", event.Sequence, "expectedType", action.AddedEventType).Error("was not an trigger actions set event")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-uYq4r", "reduce.wrong.event.type")
}
stmts := make([]func(reader eventstore.EventReader) crdb.Exec, len(e.ActionIDs)+1)
stmts[0] = crdb.AddDeleteStatement(
[]handler.Condition{
handler.NewCond(flowTypeCol, e.FlowType),
handler.NewCond(flowTriggerTypeCol, e.TriggerType),
},
crdb.WithTableSuffix(triggerTableSuffix),
)
for i, id := range e.ActionIDs {
stmts[i+1] = crdb.AddCreateStatement(
[]handler.Column{
handler.NewCol(flowResourceOwnerCol, e.Aggregate().ResourceOwner),
handler.NewCol(flowTypeCol, e.FlowType),
handler.NewCol(flowTriggerTypeCol, e.TriggerType),
handler.NewCol(flowActionIDCol, id),
handler.NewCol(flowActionTriggerSequenceCol, i),
},
crdb.WithTableSuffix(triggerTableSuffix),
)
}
return crdb.NewMultiStatement(e, stmts...), nil
}
func (p *FlowProjection) reduceFlowClearedEventType(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*org.FlowClearedEvent)
if !ok {
logging.LogWithFields("HANDL-zWCk3", "seq", event.Sequence, "expectedType", action.AddedEventType).Error("was not an trigger actions set event")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-uYq4r", "reduce.wrong.event.type")
}
return crdb.NewDeleteStatement(
e,
[]handler.Condition{
handler.NewCond(flowTypeCol, e.FlowType),
},
crdb.WithTableSuffix(triggerTableSuffix),
), nil
}
func (p *FlowProjection) reduceFlowActionAdded(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*action.AddedEvent)
if !ok {
logging.LogWithFields("HANDL-zWCk3", "seq", event.Sequence, "expectedType", action.AddedEventType).Error("was not an flow action added event")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-uYq4r", "reduce.wrong.event.type")
}
return crdb.NewCreateStatement(
e,
[]handler.Column{
handler.NewCol(actionIDCol, e.Aggregate().ID),
handler.NewCol(actionCreationDateCol, e.CreationDate()),
handler.NewCol(actionChangeDateCol, e.CreationDate()),
handler.NewCol(actionResourceOwnerCol, e.Aggregate().ResourceOwner),
handler.NewCol(actionSequenceCol, e.Sequence()),
handler.NewCol(actionNameCol, e.Name),
handler.NewCol(actionScriptCol, e.Script),
},
crdb.WithTableSuffix(actionTableSuffix),
), nil
}
func (p *FlowProjection) reduceFlowActionChanged(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*action.ChangedEvent)
if !ok {
logging.LogWithFields("HANDL-q4oq8", "seq", event.Sequence, "expected", action.ChangedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-Bg8oM", "reduce.wrong.event.type")
}
values := []handler.Column{
handler.NewCol(actionChangeDateCol, e.CreationDate()),
handler.NewCol(actionSequenceCol, e.Sequence()),
}
if e.Name != nil {
values = append(values, handler.NewCol(actionNameCol, *e.Name))
}
if e.Script != nil {
values = append(values, handler.NewCol(actionScriptCol, *e.Script))
}
return crdb.NewUpdateStatement(
e,
values,
[]handler.Condition{
handler.NewCond(actionIDCol, e.Aggregate().ID),
},
crdb.WithTableSuffix(actionTableSuffix),
), nil
}
func (p *FlowProjection) reduceFlowActionRemoved(event eventstore.EventReader) (*handler.Statement, error) {
e, ok := event.(*action.RemovedEvent)
if !ok {
logging.LogWithFields("HANDL-79OhB", "seq", event.Sequence, "expectedType", action.RemovedEventType).Error("wrong event type")
return nil, errors.ThrowInvalidArgument(nil, "HANDL-4TbKT", "reduce.wrong.event.type")
}
return crdb.NewDeleteStatement(
e,
[]handler.Condition{
handler.NewCond(actionIDCol, e.Aggregate().ID),
},
crdb.WithTableSuffix(actionTableSuffix),
), nil
}

View File

@@ -6,7 +6,7 @@ import (
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/handler"
"github.com/caos/zitadel/internal/eventstore/handler/crdb"
"github.com/caos/zitadel/internal/query/projection/org/owner"
"github.com/caos/zitadel/internal/query/projection/flow"
)
const (
@@ -37,9 +37,12 @@ func Start(ctx context.Context, es *eventstore.Eventstore, config Config) error
BulkLimit: config.BulkLimit,
}
NewOrgProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["orgs"]))
NewProjectProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["projects"]))
owner.NewOrgOwnerProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["org_owners"]))
// turned off for this release
//NewOrgProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["orgs"]))
//NewProjectProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["projects"]))
//owner.NewOrgOwnerProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["org_owners"]))
NewActionProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["actions"]))
flow.NewFlowProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["flows"]))
return nil
}

View File

@@ -2,6 +2,7 @@ package query
import (
"context"
"database/sql"
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/config/types"
@@ -10,6 +11,7 @@ import (
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/id"
"github.com/caos/zitadel/internal/query/projection"
"github.com/caos/zitadel/internal/repository/action"
iam_repo "github.com/caos/zitadel/internal/repository/iam"
"github.com/caos/zitadel/internal/repository/org"
"github.com/caos/zitadel/internal/repository/project"
@@ -22,6 +24,8 @@ type Queries struct {
eventstore *eventstore.Eventstore
idGenerator id.Generator
secretCrypto crypto.Crypto
client *sql.DB
}
type Config struct {
@@ -29,26 +33,32 @@ type Config struct {
}
func StartQueries(ctx context.Context, es *eventstore.Eventstore, projections projection.Config, defaults sd.SystemDefaults) (repo *Queries, err error) {
sqlClient, err := projections.CRDB.Start()
if err != nil {
return nil, err
}
repo = &Queries{
iamID: defaults.IamID,
eventstore: es,
idGenerator: id.SonyFlakeGenerator,
client: sqlClient,
}
iam_repo.RegisterEventMappers(repo.eventstore)
usr_repo.RegisterEventMappers(repo.eventstore)
org.RegisterEventMappers(repo.eventstore)
project.RegisterEventMappers(repo.eventstore)
action.RegisterEventMappers(repo.eventstore)
repo.secretCrypto, err = crypto.NewAESCrypto(defaults.IDPConfigVerificationKey)
if err != nil {
return nil, err
}
// turned off for this release
// err = projection.Start(ctx, es, projections)
// if err != nil {
// return nil, err
// }
err = projection.Start(ctx, es, projections)
if err != nil {
return nil, err
}
return repo, nil
}

View File

@@ -0,0 +1,149 @@
package query
import (
"errors"
"strings"
sq "github.com/Masterminds/squirrel"
)
type SearchRequest struct {
Offset uint64
Limit uint64
SortingColumn string
Asc bool
}
func (req *SearchRequest) ToQuery(query sq.SelectBuilder) sq.SelectBuilder {
if req.Offset > 0 {
query = query.Offset(req.Offset)
}
if req.Limit > 0 {
query = query.Limit(req.Limit)
}
if req.SortingColumn != "" {
clause := "LOWER(?)"
if !req.Asc {
clause += " DESC"
}
query.OrderByClause(clause, req.SortingColumn)
}
return query
}
const sqlPlaceholder = "?"
type SearchQuery interface {
ToQuery(sq.SelectBuilder) sq.SelectBuilder
}
type TextQuery struct {
Column string
Text string
Compare TextComparison
}
func NewTextQuery(column, value string, compare TextComparison) (*TextQuery, error) {
if compare < 0 || compare >= textMax {
return nil, errors.New("invalid compare")
}
if column == "" {
return nil, errors.New("missing column")
}
return &TextQuery{
Column: column,
Text: value,
Compare: compare,
}, nil
}
func (q *TextQuery) ToQuery(query sq.SelectBuilder) sq.SelectBuilder {
query = query.Where(q.comp())
return query
}
func (s *TextQuery) comp() map[string]interface{} {
switch s.Compare {
case TextEquals:
return sq.Eq{s.Column: s.Text}
case TextEqualsIgnore:
return sq.Eq{"LOWER(" + s.Column + ")": strings.ToLower(s.Text)}
case TextStartsWith:
return sq.Like{s.Column: s.Text + sqlPlaceholder}
case TextStartsWithIgnore:
return sq.Like{"LOWER(" + s.Column + ")": strings.ToLower(s.Text) + sqlPlaceholder}
case TextEndsWith:
return sq.Like{s.Column: sqlPlaceholder + s.Text}
case TextEndsWithIgnore:
return sq.Like{"LOWER(" + s.Column + ")": sqlPlaceholder + strings.ToLower(s.Text)}
case TextContains:
return sq.Like{s.Column: sqlPlaceholder + s.Text + sqlPlaceholder}
case TextContainsIgnore:
return sq.Like{"LOWER(" + s.Column + ")": sqlPlaceholder + strings.ToLower(s.Text) + sqlPlaceholder}
}
return nil
}
type TextComparison int
const (
TextEquals TextComparison = iota
TextEqualsIgnore
TextStartsWith
TextStartsWithIgnore
TextEndsWith
TextEndsWithIgnore
TextContains
TextContainsIgnore
textMax
)
type IntQuery struct {
Column string
Int int
Compare IntComparison
}
func NewIntQuery(column string, value int, compare IntComparison) (*IntQuery, error) {
if compare < 0 || compare >= intMax {
return nil, errors.New("invalid compare")
}
if column == "" {
return nil, errors.New("missing column")
}
return &IntQuery{
Column: column,
Int: value,
Compare: compare,
}, nil
}
func (q *IntQuery) ToQuery(query sq.SelectBuilder) sq.SelectBuilder {
query = query.Where(q.comp())
return query
}
func (s *IntQuery) comp() sq.Sqlizer {
switch s.Compare {
case IntEquals:
return sq.Eq{s.Column: s.Int}
case IntGreater:
return sq.Gt{s.Column: s.Int}
case IntLess:
return sq.Lt{s.Column: s.Int}
}
return nil
}
type IntComparison int
const (
IntEquals IntComparison = iota
IntGreater
IntLess
intMax
)

View File

@@ -0,0 +1,261 @@
package action
import (
"context"
"encoding/json"
"time"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
)
const (
UniqueActionNameType = "action_names"
eventTypePrefix = eventstore.EventType("action.")
AddedEventType = eventTypePrefix + "added"
ChangedEventType = eventTypePrefix + "changed"
DeactivatedEventType = eventTypePrefix + "deactivated"
ReactivatedEventType = eventTypePrefix + "reactivated"
RemovedEventType = eventTypePrefix + "removed"
)
func NewAddActionNameUniqueConstraint(actionName, resourceOwner string) *eventstore.EventUniqueConstraint {
return eventstore.NewAddEventUniqueConstraint(
UniqueActionNameType,
actionName+":"+resourceOwner,
"Errors.Action.AlreadyExists")
}
func NewRemoveActionNameUniqueConstraint(actionName, resourceOwner string) *eventstore.EventUniqueConstraint {
return eventstore.NewRemoveEventUniqueConstraint(
UniqueActionNameType,
actionName+":"+resourceOwner)
}
type AddedEvent struct {
eventstore.BaseEvent `json:"-"`
Name string `json:"name"`
Script string `json:"script,omitempty"`
Timeout time.Duration `json:"timeout,omitempty"`
AllowedToFail bool `json:"allowedToFail"`
}
func (e *AddedEvent) Data() interface{} {
return e
}
func (e *AddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return []*eventstore.EventUniqueConstraint{NewAddActionNameUniqueConstraint(e.Name, e.Aggregate().ResourceOwner)}
}
func NewAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
name,
script string,
timeout time.Duration,
allowedToFail bool,
) *AddedEvent {
return &AddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
AddedEventType,
),
Name: name,
Script: script,
Timeout: timeout,
AllowedToFail: allowedToFail,
}
}
func AddedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
e := &AddedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, e)
if err != nil {
return nil, errors.ThrowInternal(err, "ACTION-4n8vs", "unable to unmarshal action added")
}
return e, nil
}
type ChangedEvent struct {
eventstore.BaseEvent `json:"-"`
Name *string `json:"name,omitempty"`
Script *string `json:"script,omitempty"`
Timeout *time.Duration `json:"timeout,omitempty"`
AllowedToFail *bool `json:"allowedToFail,omitempty"`
oldName string
}
func (e *ChangedEvent) Data() interface{} {
return e
}
func (e *ChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
if e.oldName == "" {
return nil
}
return []*eventstore.EventUniqueConstraint{
NewRemoveActionNameUniqueConstraint(e.oldName, e.Aggregate().ResourceOwner),
NewAddActionNameUniqueConstraint(*e.Name, e.Aggregate().ResourceOwner),
}
}
func NewChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
changes []ActionChanges,
) (*ChangedEvent, error) {
if len(changes) == 0 {
return nil, errors.ThrowPreconditionFailed(nil, "ACTION-dg4t2", "Errors.NoChangesFound")
}
changeEvent := &ChangedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
ChangedEventType,
),
}
for _, change := range changes {
change(changeEvent)
}
return changeEvent, nil
}
type ActionChanges func(event *ChangedEvent)
func ChangeName(name, oldName string) func(event *ChangedEvent) {
return func(e *ChangedEvent) {
e.Name = &name
e.oldName = oldName
}
}
func ChangeScript(script string) func(event *ChangedEvent) {
return func(e *ChangedEvent) {
e.Script = &script
}
}
func ChangeTimeout(timeout time.Duration) func(event *ChangedEvent) {
return func(e *ChangedEvent) {
e.Timeout = &timeout
}
}
func ChangeAllowedToFail(allowedToFail bool) func(event *ChangedEvent) {
return func(e *ChangedEvent) {
e.AllowedToFail = &allowedToFail
}
}
func ChangedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
e := &ChangedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, e)
if err != nil {
return nil, errors.ThrowInternal(err, "ACTION-4n8vs", "unable to unmarshal action changed")
}
return e, nil
}
type DeactivatedEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *DeactivatedEvent) Data() interface{} {
return nil
}
func (e *DeactivatedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewDeactivatedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *DeactivatedEvent {
return &DeactivatedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
DeactivatedEventType,
),
}
}
func DeactivatedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
return &DeactivatedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}
type ReactivatedEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *ReactivatedEvent) Data() interface{} {
return nil
}
func (e *ReactivatedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewReactivatedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *ReactivatedEvent {
return &ReactivatedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
ReactivatedEventType,
),
}
}
func ReactivatedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
return &ReactivatedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}
type RemovedEvent struct {
eventstore.BaseEvent `json:"-"`
name string
}
func (e *RemovedEvent) Data() interface{} {
return e
}
func (e *RemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return []*eventstore.EventUniqueConstraint{NewRemoveActionNameUniqueConstraint(e.name, e.Aggregate().ResourceOwner)}
}
func NewRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
name string,
) *RemovedEvent {
return &RemovedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
RemovedEventType,
),
name: name,
}
}
func RemovedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
return &RemovedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}

View File

@@ -0,0 +1,23 @@
package action
import "github.com/caos/zitadel/internal/eventstore"
const (
AggregateType = "action"
AggregateVersion = "v1"
)
type Aggregate struct {
eventstore.Aggregate
}
func NewAggregate(id, resourceOwner string) *Aggregate {
return &Aggregate{
Aggregate: eventstore.Aggregate{
Type: AggregateType,
Version: AggregateVersion,
ID: id,
ResourceOwner: resourceOwner,
},
}
}

View File

@@ -0,0 +1,11 @@
package action
import "github.com/caos/zitadel/internal/eventstore"
func RegisterEventMappers(es *eventstore.Eventstore) {
es.RegisterFilterEventMapper(AddedEventType, AddedEventMapper).
RegisterFilterEventMapper(ChangedEventType, ChangedEventMapper).
RegisterFilterEventMapper(DeactivatedEventType, DeactivatedEventMapper).
RegisterFilterEventMapper(ReactivatedEventType, ReactivatedEventMapper).
RegisterFilterEventMapper(RemovedEventType, RemovedEventMapper)
}

View File

@@ -40,6 +40,7 @@ type FeaturesSetEvent struct {
CustomTextMessage *bool `json:"customTextMessage,omitempty"`
CustomTextLogin *bool `json:"customTextLogin,omitempty"`
LockoutPolicy *bool `json:"lockoutPolicy,omitempty"`
Actions *bool `json:"actions,omitempty"`
}
func (e *FeaturesSetEvent) Data() interface{} {
@@ -188,6 +189,12 @@ func ChangeLockoutPolicy(lockoutPolicy bool) func(event *FeaturesSetEvent) {
}
}
func ChangeActions(actions bool) func(event *FeaturesSetEvent) {
return func(e *FeaturesSetEvent) {
e.Actions = &actions
}
}
func FeaturesSetEventMapper(event *repository.Event) (eventstore.EventReader, error) {
e := &FeaturesSetEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),

View File

@@ -0,0 +1,139 @@
package flow
import (
"encoding/json"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
)
const (
eventTypePrefix = eventstore.EventType("flow.")
triggerActionsPrefix = eventTypePrefix + "trigger_actions."
TriggerActionsSetEventType = triggerActionsPrefix + "set"
TriggerActionsCascadeRemovedEventType = triggerActionsPrefix + "cascade.removed"
FlowClearedEventType = eventTypePrefix + "cleared"
)
type TriggerActionsSetEvent struct {
eventstore.BaseEvent
FlowType domain.FlowType
TriggerType domain.TriggerType
ActionIDs []string
}
func (e *TriggerActionsSetEvent) Data() interface{} {
return e
}
func (e *TriggerActionsSetEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewTriggerActionsSetEvent(
base *eventstore.BaseEvent,
flowType domain.FlowType,
triggerType domain.TriggerType,
actionIDs []string,
) *TriggerActionsSetEvent {
return &TriggerActionsSetEvent{
BaseEvent: *base,
FlowType: flowType,
TriggerType: triggerType,
ActionIDs: actionIDs,
}
}
func TriggerActionsSetEventMapper(event *repository.Event) (eventstore.EventReader, error) {
e := &TriggerActionsSetEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, e)
if err != nil {
return nil, errors.ThrowInternal(err, "FLOW-4n8vs", "unable to unmarshal trigger actions")
}
return e, nil
}
type TriggerActionsCascadeRemovedEvent struct {
eventstore.BaseEvent
FlowType domain.FlowType
TriggerType domain.TriggerType
ActionID string
}
func (e *TriggerActionsCascadeRemovedEvent) Data() interface{} {
return e
}
func (e *TriggerActionsCascadeRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewTriggerActionsCascadeRemovedEvent(
base *eventstore.BaseEvent,
flowType domain.FlowType,
actionID string,
) *TriggerActionsCascadeRemovedEvent {
return &TriggerActionsCascadeRemovedEvent{
BaseEvent: *base,
FlowType: flowType,
ActionID: actionID,
}
}
func TriggerActionsCascadeRemovedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
e := &TriggerActionsCascadeRemovedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, e)
if err != nil {
return nil, errors.ThrowInternal(err, "FLOW-4n8vs", "unable to unmarshal trigger actions")
}
return e, nil
}
type FlowClearedEvent struct {
eventstore.BaseEvent
FlowType domain.FlowType
}
func (e *FlowClearedEvent) Data() interface{} {
return e
}
func (e *FlowClearedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewFlowClearedEvent(
base *eventstore.BaseEvent,
flowType domain.FlowType,
) *FlowClearedEvent {
return &FlowClearedEvent{
BaseEvent: *base,
FlowType: flowType,
}
}
func FlowClearedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
e := &FlowClearedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, e)
if err != nil {
return nil, errors.ThrowInternal(err, "FLOW-BHfg2", "unable to unmarshal flow cleared")
}
return e, nil
}

View File

@@ -78,5 +78,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(IDPJWTConfigAddedEventType, IDPJWTConfigAddedEventMapper).
RegisterFilterEventMapper(IDPJWTConfigChangedEventType, IDPJWTConfigChangedEventMapper).
RegisterFilterEventMapper(FeaturesSetEventType, FeaturesSetEventMapper).
RegisterFilterEventMapper(FeaturesRemovedEventType, FeaturesRemovedEventMapper)
RegisterFilterEventMapper(FeaturesRemovedEventType, FeaturesRemovedEventMapper).
RegisterFilterEventMapper(TriggerActionsSetEventType, TriggerActionsSetEventMapper).
RegisterFilterEventMapper(TriggerActionsCascadeRemovedEventType, TriggerActionsCascadeRemovedEventMapper).
RegisterFilterEventMapper(FlowClearedEventType, FlowClearedEventMapper)
}

View File

@@ -0,0 +1,106 @@
package org
import (
"context"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/repository/flow"
)
var (
TriggerActionsSetEventType = orgEventTypePrefix + flow.TriggerActionsSetEventType
TriggerActionsCascadeRemovedEventType = orgEventTypePrefix + flow.TriggerActionsCascadeRemovedEventType
FlowClearedEventType = orgEventTypePrefix + flow.FlowClearedEventType
)
type TriggerActionsSetEvent struct {
flow.TriggerActionsSetEvent
}
func NewTriggerActionsSetEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
flowType domain.FlowType,
triggerType domain.TriggerType,
actionIDs []string,
) *TriggerActionsSetEvent {
return &TriggerActionsSetEvent{
TriggerActionsSetEvent: *flow.NewTriggerActionsSetEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
TriggerActionsSetEventType),
flowType,
triggerType,
actionIDs),
}
}
func TriggerActionsSetEventMapper(event *repository.Event) (eventstore.EventReader, error) {
e, err := flow.TriggerActionsSetEventMapper(event)
if err != nil {
return nil, err
}
return &TriggerActionsSetEvent{TriggerActionsSetEvent: *e.(*flow.TriggerActionsSetEvent)}, nil
}
type TriggerActionsCascadeRemovedEvent struct {
flow.TriggerActionsCascadeRemovedEvent
}
func NewTriggerActionsCascadeRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
flowType domain.FlowType,
actionID string,
) *TriggerActionsCascadeRemovedEvent {
return &TriggerActionsCascadeRemovedEvent{
TriggerActionsCascadeRemovedEvent: *flow.NewTriggerActionsCascadeRemovedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
TriggerActionsCascadeRemovedEventType),
flowType,
actionID),
}
}
func TriggerActionsCascadeRemovedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
e, err := flow.TriggerActionsCascadeRemovedEventMapper(event)
if err != nil {
return nil, err
}
return &TriggerActionsCascadeRemovedEvent{TriggerActionsCascadeRemovedEvent: *e.(*flow.TriggerActionsCascadeRemovedEvent)}, nil
}
type FlowClearedEvent struct {
flow.FlowClearedEvent
}
func NewFlowClearedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
flowType domain.FlowType,
) *FlowClearedEvent {
return &FlowClearedEvent{
FlowClearedEvent: *flow.NewFlowClearedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
FlowClearedEventType),
flowType),
}
}
func FlowClearedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
e, err := flow.FlowClearedEventMapper(event)
if err != nil {
return nil, err
}
return &FlowClearedEvent{FlowClearedEvent: *e.(*flow.FlowClearedEvent)}, nil
}

View File

@@ -365,6 +365,17 @@ Errors:
NoData: Meta Daten Liste ist leer
Invalid: Meta Daten sind ungültig
KeyNotExisting: Ein oder mehrere Keys existiert nicht
Action:
Invalid: Action ist ungültig
NotFound: Action wurde nicht gefunden
NotActive: Action ist nicht aktiv
NotInactive: Action ist nicht inaktiv
Flow:
FlowTypeMissing: FlowType fehlt
Empty: Flow ist bereits leer
WrongTriggerType: TriggerType ist ungültig
NoChanges: Keine Änderungen
ActionIDsNotExist: ActionIDs existieren nicht
EventTypes:
user:
added: Benutzer hinzugefügt
@@ -654,6 +665,12 @@ EventTypes:
added: Datenschutzbestimmung und AGB hinzugefügt
changed: Datenschutzbestimmung und AGB geändert
removed: Datenschutzbestimmung und AGB entfernt
flow:
trigger_actions:
set: Aktionen festgelegt
cascade:
removed: Aktionen kaskadiert entfernt
removed: Aktionen entfernt
project:
added: Projekt hinzugefügt
changed: Project geändert
@@ -784,6 +801,12 @@ EventTypes:
removed: Bilder und Schrift von Label Richtlinie entfernt
key_pair:
added: Schlüsselpaar hinzugefügt
action:
added: Aktion hinzugefügt
changed: Aktion geändert
deactivated: Aktion deaktiviert
reactivated: Aktion reaktiviert
removed: Aktion gelöscht
Application:
OIDC:
V1:

View File

@@ -365,6 +365,17 @@ Errors:
NoData: Metadata list is empty
Invalid: Metadata is invalid
KeyNotExisting: One or more keys do not exist
Action:
Invalid: Action is invalid
NotFound: Action not found
NotActive: Action is not active
NotInactive: Action is not inactive
Flow:
FlowTypeMissing: FlowType missing
Empty: Flow is already empty
WrongTriggerType: TriggerType is invalid
NoChanges: No Changes
ActionIDsNotExist: ActionIDs do not exist
EventTypes:
user:
added: User added
@@ -654,6 +665,12 @@ EventTypes:
added: Privacy policy and TOS added
changed: Privacy policy and TOS changed
removed: Privacy policy and TOS removed
flow:
trigger_actions:
set: Action set
cascade:
removed: Actions cascade removed
removed: Actions removed
project:
added: Project added
changed: Project changed
@@ -781,6 +798,12 @@ EventTypes:
removed: Assets removed from Label Policy
key_pair:
added: Key pair added
action:
added: Action added
changed: Action changed
deactivated: Action deactivated
reactivated: Action reactivated
removed: Action removed
Application:
OIDC:
V1:

View File

@@ -0,0 +1,75 @@
package handler
import (
"context"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/zitadel/internal/actions"
"github.com/caos/zitadel/internal/domain"
iam_model "github.com/caos/zitadel/internal/iam/model"
)
func (l *Login) customExternalUserMapping(user *domain.ExternalUser, tokens *oidc.Tokens, req *domain.AuthRequest, config *iam_model.IDPConfigView) (*domain.ExternalUser, error) {
triggerActions, err := l.query.GetActionsByFlowAndTriggerType(context.TODO(), domain.FlowTypeExternalAuthentication, domain.TriggerTypePostAuthentication)
if err != nil {
return nil, err
}
ctx := (&actions.Context{}).SetToken(tokens)
api := (&actions.API{}).SetExternalUser(user).SetMetadata(&user.Metadatas)
for _, a := range triggerActions {
err = actions.Run(ctx, api, a.Script, a.Name, a.Timeout, a.AllowedToFail)
if err != nil {
return nil, err
}
}
return user, err
}
func (l *Login) customExternalUserToLoginUserMapping(user *domain.Human, tokens *oidc.Tokens, req *domain.AuthRequest, config *iam_model.IDPConfigView, metadata []*domain.Metadata) (*domain.Human, []*domain.Metadata, error) {
triggerActions, err := l.query.GetActionsByFlowAndTriggerType(context.TODO(), domain.FlowTypeExternalAuthentication, domain.TriggerTypePreCreation)
if err != nil {
return nil, nil, err
}
ctx := (&actions.Context{}).SetToken(tokens)
api := (&actions.API{}).SetHuman(user).SetMetadata(&metadata)
for _, a := range triggerActions {
err = actions.Run(ctx, api, a.Script, a.Name, a.Timeout, a.AllowedToFail)
if err != nil {
return nil, nil, err
}
}
return user, metadata, err
}
func (l *Login) customGrants(userID string, tokens *oidc.Tokens, req *domain.AuthRequest, config *iam_model.IDPConfigView) ([]*domain.UserGrant, error) {
triggerActions, err := l.query.GetActionsByFlowAndTriggerType(context.TODO(), domain.FlowTypeExternalAuthentication, domain.TriggerTypePostCreation)
if err != nil {
return nil, err
}
ctx := (&actions.Context{}).SetToken(tokens)
actionUserGrants := make([]actions.UserGrant, 0)
api := (&actions.API{}).SetUserGrants(&actionUserGrants)
for _, a := range triggerActions {
err = actions.Run(ctx, api, a.Script, a.Name, a.Timeout, a.AllowedToFail)
if err != nil {
return nil, err
}
}
return actionUserGrantsToDomain(userID, actionUserGrants), err
}
func actionUserGrantsToDomain(userID string, actionUserGrants []actions.UserGrant) []*domain.UserGrant {
if actionUserGrants == nil {
return nil
}
userGrants := make([]*domain.UserGrant, len(actionUserGrants))
for i, grant := range actionUserGrants {
userGrants[i] = &domain.UserGrant{
UserID: userID,
ProjectID: grant.ProjectID,
ProjectGrantID: grant.ProjectGrantID,
RoleKeys: grant.Roles,
}
}
return userGrants
}

View File

@@ -184,7 +184,12 @@ func (l *Login) getRPConfig(w http.ResponseWriter, r *http.Request, authReq *dom
func (l *Login) handleExternalUserAuthenticated(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView, userAgentID string, tokens *oidc.Tokens) {
externalUser := l.mapTokenToLoginUser(tokens, idpConfig)
err := l.authRepo.CheckExternalUserLogin(r.Context(), authReq.ID, userAgentID, externalUser, domain.BrowserInfoFromRequest(r))
externalUser, err := l.customExternalUserMapping(externalUser, tokens, authReq, idpConfig)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
err = l.authRepo.CheckExternalUserLogin(r.Context(), authReq.ID, userAgentID, externalUser, domain.BrowserInfoFromRequest(r))
if err != nil {
if errors.IsNotFound(err) {
err = nil
@@ -201,6 +206,17 @@ func (l *Login) handleExternalUserAuthenticated(w http.ResponseWriter, r *http.R
l.handleAutoRegister(w, r, authReq)
return
}
if len(externalUser.Metadatas) > 0 {
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, userAgentID)
if err != nil {
return
}
_, err = l.command.BulkSetUserMetadata(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, externalUser.Metadatas...)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
}
l.renderNextStep(w, r, authReq)
}
@@ -266,12 +282,29 @@ func (l *Login) handleAutoRegister(w http.ResponseWriter, r *http.Request, authR
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
user, externalIDP := l.mapExternalUserToLoginUser(orgIamPolicy, authReq.LinkingUsers[len(authReq.LinkingUsers)-1], idpConfig)
err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, memberRoles, authReq.ID, userAgentID, resourceOwner, domain.BrowserInfoFromRequest(r))
linkingUser := authReq.LinkingUsers[len(authReq.LinkingUsers)-1]
user, externalIDP, metadata := l.mapExternalUserToLoginUser(orgIamPolicy, linkingUser, idpConfig)
user, metadata, err = l.customExternalUserToLoginUserMapping(user, nil, authReq, idpConfig, metadata)
err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, memberRoles, authReq.ID, userAgentID, resourceOwner, metadata, domain.BrowserInfoFromRequest(r))
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, err)
return
}
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
userGrants, err := l.customGrants(authReq.UserID, nil, authReq, idpConfig)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
err = l.appendUserGrants(r.Context(), userGrants, resourceOwner)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
l.renderNextStep(w, r, authReq)
}
@@ -305,7 +338,7 @@ func (l *Login) mapTokenToLoginUser(tokens *oidc.Tokens, idpConfig *iam_model.ID
}
return externalUser
}
func (l *Login) mapExternalUserToLoginUser(orgIamPolicy *iam_model.OrgIAMPolicyView, linkingUser *domain.ExternalUser, idpConfig *iam_model.IDPConfigView) (*domain.Human, *domain.ExternalIDP) {
func (l *Login) mapExternalUserToLoginUser(orgIamPolicy *iam_model.OrgIAMPolicyView, linkingUser *domain.ExternalUser, idpConfig *iam_model.IDPConfigView) (*domain.Human, *domain.ExternalIDP, []*domain.Metadata) {
username := linkingUser.PreferredUsername
switch idpConfig.OIDCUsernameMapping {
case iam_model.OIDCMappingFieldEmail:
@@ -360,5 +393,5 @@ func (l *Login) mapExternalUserToLoginUser(orgIamPolicy *iam_model.OrgIAMPolicyV
ExternalUserID: linkingUser.ExternalUserID,
DisplayName: displayName,
}
return human, externalIDP
return human, externalIDP, linkingUser.Metadatas
}

View File

@@ -118,6 +118,10 @@ func (l *Login) handleExternalUserRegister(w http.ResponseWriter, r *http.Reques
return
}
user, externalIDP := l.mapTokenToLoginHumanAndExternalIDP(orgIamPolicy, tokens, idpConfig)
if err != nil {
l.renderRegisterOption(w, r, authReq, err)
return
}
if !idpConfig.AutoRegister {
l.renderExternalRegisterOverview(w, r, authReq, orgIamPolicy, user, externalIDP, nil)
return

View File

@@ -8,6 +8,7 @@ import (
"strings"
"time"
"github.com/caos/logging"
"github.com/caos/oidc/pkg/client/rp"
"github.com/caos/oidc/pkg/oidc"
http_util "github.com/caos/zitadel/internal/api/http"
@@ -74,28 +75,24 @@ func (l *Login) handleJWTExtraction(w http.ResponseWriter, r *http.Request, auth
}
tokens := &oidc.Tokens{IDToken: token, IDTokenClaims: tokenClaims}
externalUser := l.mapTokenToLoginUser(tokens, idpConfig)
externalUser, err = l.customExternalUserMapping(externalUser, tokens, authReq, idpConfig)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
metadata := externalUser.Metadatas
err = l.authRepo.CheckExternalUserLogin(r.Context(), authReq.ID, authReq.AgentID, externalUser, domain.BrowserInfoFromRequest(r))
if err != nil {
if errors.IsNotFound(err) {
err = nil
}
if !idpConfig.AutoRegister {
l.renderExternalNotFoundOption(w, r, authReq, err)
return
}
l.jwtExtractionUserNotFound(w, r, authReq, idpConfig, tokens, err)
return
}
if len(metadata) > 0 {
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
resourceOwner := l.getOrgID(authReq)
orgIamPolicy, err := l.getOrgIamPolicy(r, resourceOwner)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
user, externalIDP := l.mapExternalUserToLoginUser(orgIamPolicy, authReq.LinkingUsers[len(authReq.LinkingUsers)-1], idpConfig)
err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, nil, authReq.ID, authReq.AgentID, resourceOwner, domain.BrowserInfoFromRequest(r))
_, err = l.command.BulkSetUserMetadata(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, metadata...)
if err != nil {
l.renderError(w, r, authReq, err)
return
@@ -109,6 +106,72 @@ func (l *Login) handleJWTExtraction(w http.ResponseWriter, r *http.Request, auth
http.Redirect(w, r, redirect, http.StatusFound)
}
func (l *Login) jwtExtractionUserNotFound(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView, tokens *oidc.Tokens, err error) {
if errors.IsNotFound(err) {
err = nil
}
if !idpConfig.AutoRegister {
l.renderExternalNotFoundOption(w, r, authReq, err)
return
}
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
resourceOwner := l.getOrgID(authReq)
orgIamPolicy, err := l.getOrgIamPolicy(r, resourceOwner)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
user, externalIDP, metadata := l.mapExternalUserToLoginUser(orgIamPolicy, authReq.LinkingUsers[len(authReq.LinkingUsers)-1], idpConfig)
user, metadata, err = l.customExternalUserToLoginUserMapping(user, tokens, authReq, idpConfig, metadata)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, nil, authReq.ID, authReq.AgentID, resourceOwner, metadata, domain.BrowserInfoFromRequest(r))
if err != nil {
l.renderError(w, r, authReq, err)
return
}
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
userGrants, err := l.customGrants(authReq.UserID, tokens, authReq, idpConfig)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
err = l.appendUserGrants(r.Context(), userGrants, resourceOwner)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
redirect, err := l.redirectToJWTCallback(authReq)
if err != nil {
l.renderError(w, r, nil, err)
return
}
http.Redirect(w, r, redirect, http.StatusFound)
}
func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserGrant, resourceOwner string) error {
if len(userGrants) == 0 {
return nil
}
for _, grant := range userGrants {
_, err := l.command.AddUserGrant(setContext(ctx, resourceOwner), grant, resourceOwner)
if err != nil {
return err
}
}
return nil
}
func (l *Login) redirectToJWTCallback(authReq *domain.AuthRequest) (string, error) {
redirect, err := url.Parse(l.baseURL + EndpointJWTCallback)
if err != nil {
@@ -160,6 +223,7 @@ func (l *Login) handleJWTCallback(w http.ResponseWriter, r *http.Request) {
}
func validateToken(ctx context.Context, token string, config *iam_model.IDPConfigView) (oidc.IDTokenClaims, error) {
logging.Log("LOGIN-ADf42").Debug("begin token validation")
offset := 3 * time.Second
maxAge := time.Hour
claims := oidc.EmptyIDTokenClaims()
@@ -172,6 +236,7 @@ func validateToken(ctx context.Context, token string, config *iam_model.IDPConfi
return nil, err
}
logging.Log("LOGIN-Dfg22").Debug("begin signature validation")
keySet := rp.NewRemoteKeySet(http.DefaultClient, config.JWTKeysEndpoint)
if err = oidc.CheckSignature(ctx, token, payload, claims, nil, keySet); err != nil {
return nil, err