From ed80a8bb1e181b35f80907f068811471f3ef8ad6 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 27 Sep 2021 13:43:49 +0200 Subject: [PATCH] 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 --- build/zitadel/generate-grpc.sh | 4 + cmd/zitadel/authz.yaml | 24 + cmd/zitadel/main.go | 5 + docs/docs/apis/proto/action.md | 188 +++++ docs/docs/apis/proto/admin.md | 2 + docs/docs/apis/proto/management.md | 338 ++++++++ go.mod | 3 + go.sum | 20 +- internal/actions/actions.go | 112 +++ internal/actions/api.go | 105 +++ internal/actions/context.go | 33 + internal/actions/provided.go | 55 ++ internal/api/grpc/action/action.go | 126 +++ internal/api/grpc/admin/features.go | 2 + internal/api/grpc/features/features.go | 1 + internal/api/grpc/management/actions.go | 94 +++ .../api/grpc/management/actions_converter.go | 64 ++ internal/api/grpc/management/auth_checks.go | 32 + internal/api/grpc/management/flow.go | 50 ++ internal/api/grpc/management/user_grant.go | 7 +- internal/api/grpc/object/converter.go | 24 + internal/api/grpc/server/gateway.go | 2 +- internal/auth/repository/auth_request.go | 3 +- .../eventsourcing/eventstore/auth_request.go | 9 +- .../eventstore/token_verifier.go | 6 + internal/command/command.go | 2 + internal/command/features_model.go | 4 + internal/command/flow_model.go | 52 ++ internal/command/iam_features.go | 1 + internal/command/iam_features_model.go | 6 +- internal/command/main_test.go | 2 + internal/command/org_action.go | 200 +++++ internal/command/org_action_model.go | 194 +++++ internal/command/org_action_test.go | 791 ++++++++++++++++++ internal/command/org_features.go | 10 + internal/command/org_features_model.go | 6 +- internal/command/org_features_test.go | 13 + internal/command/org_flow.go | 83 ++ internal/command/org_flow_model.go | 54 ++ internal/command/org_flow_test.go | 286 +++++++ internal/command/user_grant.go | 7 +- internal/command/user_grant_test.go | 21 +- internal/domain/action.go | 39 + internal/domain/auth_request.go | 1 + internal/domain/features.go | 2 + internal/domain/flow.go | 52 ++ internal/features/model/features_view.go | 4 + .../repository/view/model/features.go | 2 + internal/query/action.go | 104 +++ internal/query/action_flow.go | 173 ++++ internal/query/projection/action.go | 174 ++++ internal/query/projection/flow/flow.go | 184 ++++ internal/query/projection/projection.go | 11 +- internal/query/query.go | 20 +- internal/query/search_query.go | 149 ++++ internal/repository/action/action.go | 261 ++++++ internal/repository/action/aggregate.go | 23 + internal/repository/action/eventstore.go | 11 + internal/repository/features/features.go | 7 + internal/repository/flow/flow.go | 139 +++ internal/repository/org/eventstore.go | 5 +- internal/repository/org/flow.go | 106 +++ internal/static/i18n/de.yaml | 23 + internal/static/i18n/en.yaml | 23 + internal/ui/login/handler/custom_action.go | 75 ++ .../login/handler/external_login_handler.go | 43 +- .../handler/external_register_handler.go | 4 + internal/ui/login/handler/jwt_handler.go | 95 ++- migrations/cockroach/V1.72__actions.sql | 63 ++ proto/zitadel/action.proto | 155 ++++ proto/zitadel/admin.proto | 2 + proto/zitadel/features.proto | 3 +- proto/zitadel/management.proto | 267 ++++++ 73 files changed, 5197 insertions(+), 64 deletions(-) create mode 100644 docs/docs/apis/proto/action.md create mode 100644 internal/actions/actions.go create mode 100644 internal/actions/api.go create mode 100644 internal/actions/context.go create mode 100644 internal/actions/provided.go create mode 100644 internal/api/grpc/action/action.go create mode 100644 internal/api/grpc/management/actions.go create mode 100644 internal/api/grpc/management/actions_converter.go create mode 100644 internal/api/grpc/management/auth_checks.go create mode 100644 internal/api/grpc/management/flow.go create mode 100644 internal/command/flow_model.go create mode 100644 internal/command/org_action.go create mode 100644 internal/command/org_action_model.go create mode 100644 internal/command/org_action_test.go create mode 100644 internal/command/org_flow.go create mode 100644 internal/command/org_flow_model.go create mode 100644 internal/command/org_flow_test.go create mode 100644 internal/domain/action.go create mode 100644 internal/domain/flow.go create mode 100644 internal/query/action.go create mode 100644 internal/query/action_flow.go create mode 100644 internal/query/projection/action.go create mode 100644 internal/query/projection/flow/flow.go create mode 100644 internal/query/search_query.go create mode 100644 internal/repository/action/action.go create mode 100644 internal/repository/action/aggregate.go create mode 100644 internal/repository/action/eventstore.go create mode 100644 internal/repository/flow/flow.go create mode 100644 internal/repository/org/flow.go create mode 100644 internal/ui/login/handler/custom_action.go create mode 100644 migrations/cockroach/V1.72__actions.sql create mode 100644 proto/zitadel/action.proto diff --git a/build/zitadel/generate-grpc.sh b/build/zitadel/generate-grpc.sh index ae4d8c041f..8009ff98fa 100755 --- a/build/zitadel/generate-grpc.sh +++ b/build/zitadel/generate-grpc.sh @@ -92,6 +92,10 @@ protoc \ -I=/proto/include \ --doc_out=${DOCS_PATH} --doc_opt=${PROTO_PATH}/docs/zitadel-md.tmpl,admin.md \ ${PROTO_PATH}/admin.proto +protoc \ + -I=/proto/include \ + --doc_out=${DOCS_PATH} --doc_opt=${PROTO_PATH}/docs/zitadel-md.tmpl,action.md \ + ${PROTO_PATH}/action.proto protoc \ -I=/proto/include \ --doc_out=${DOCS_PATH} --doc_opt=${PROTO_PATH}/docs/zitadel-md.tmpl,app.md \ diff --git a/cmd/zitadel/authz.yaml b/cmd/zitadel/authz.yaml index 4a22bad21c..20fd693a3e 100644 --- a/cmd/zitadel/authz.yaml +++ b/cmd/zitadel/authz.yaml @@ -15,6 +15,12 @@ InternalAuthZ: - "iam.idp.read" - "iam.idp.write" - "iam.idp.delete" + - "iam.action.read" + - "iam.action.write" + - "iam.action.delete" + - "iam.flow.read" + - "iam.flow.write" + - "iam.flow.delete" - "org.read" - "org.global.read" - "org.create" @@ -25,6 +31,12 @@ InternalAuthZ: - "org.idp.read" - "org.idp.write" - "org.idp.delete" + - "org.action.read" + - "org.action.write" + - "org.action.delete" + - "org.flow.read" + - "org.flow.write" + - "org.flow.delete" - "user.read" - "user.global.read" - "user.write" @@ -63,9 +75,13 @@ InternalAuthZ: - "iam.policy.read" - "iam.member.read" - "iam.idp.read" + - "iam.action.read" + - "iam.flow.read" - "org.read" - "org.member.read" - "org.idp.read" + - "org.action.read" + - "org.flow.read" - "user.read" - "user.global.read" - "user.grant.read" @@ -90,6 +106,12 @@ InternalAuthZ: - "org.idp.read" - "org.idp.write" - "org.idp.delete" + - "org.action.read" + - "org.action.write" + - "org.action.delete" + - "org.flow.read" + - "org.flow.write" + - "org.flow.delete" - "user.read" - "user.global.read" - "user.write" @@ -125,6 +147,8 @@ InternalAuthZ: - "org.read" - "org.member.read" - "org.idp.read" + - "org.action.read" + - "org.flow.read" - "user.read" - "user.global.read" - "user.grant.read" diff --git a/cmd/zitadel/main.go b/cmd/zitadel/main.go index 673da2ab0b..4c4aacf444 100644 --- a/cmd/zitadel/main.go +++ b/cmd/zitadel/main.go @@ -145,6 +145,11 @@ func startZitadel(configPaths []string) { err := config.Read(conf, configPaths...) logging.Log("ZITAD-EDz31").OnError(err).Fatal("cannot read config") + logging.LogWithFields("MAIN-dsfg2", + "HTTP_PROXY", os.Getenv("HTTP_PROXY") != "", + "HTTPS_PROXY", os.Getenv("HTTPS_PROXY") != "", + "NO_PROXY", os.Getenv("NO_PROXY")).Info("http proxy settings") + ctx := context.Background() esQueries, err := eventstore.StartWithUser(conf.EventstoreBase, conf.Queries.Eventstore) if err != nil { diff --git a/docs/docs/apis/proto/action.md b/docs/docs/apis/proto/action.md new file mode 100644 index 0000000000..40e39d7307 --- /dev/null +++ b/docs/docs/apis/proto/action.md @@ -0,0 +1,188 @@ +--- +title: zitadel/action.proto +--- +> This document reflects the state from API 1.0 (available from 20.04.2021) + + + + +## Messages + + +### Action + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| id | string | - | | +| details | zitadel.v1.ObjectDetails | - | | +| state | ActionState | - | | +| name | string | - | | +| script | string | - | | +| timeout | google.protobuf.Duration | - | | +| allowed_to_fail | bool | - | | + + + + +### ActionIDQuery + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| id | string | - | string.max_len: 200
| + + + + +### ActionNameQuery + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| name | string | - | string.max_len: 200
| +| method | zitadel.v1.TextQueryMethod | - | enum.defined_only: true
| + + + + +### ActionStateQuery +ActionStateQuery is always equals + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| state | ActionState | - | enum.defined_only: true
| + + + + +### Flow + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| type | FlowType | - | | +| details | zitadel.v1.ObjectDetails | - | | +| state | FlowState | - | | +| trigger_actions | repeated TriggerAction | - | | + + + + +### FlowStateQuery +FlowStateQuery is always equals + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| state | FlowState | - | enum.defined_only: true
| + + + + +### FlowTypeQuery +FlowTypeQuery is always equals + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| state | FlowType | - | enum.defined_only: true
| + + + + +### TriggerAction + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| trigger_type | TriggerType | - | | +| actions | repeated Action | - | | + + + + + + +## Enums + + +### ActionFieldName {#actionfieldname} + + +| Name | Number | Description | +| ---- | ------ | ----------- | +| ACTION_FIELD_NAME_UNSPECIFIED | 0 | - | +| ACTION_FIELD_NAME_NAME | 1 | - | +| ACTION_FIELD_NAME_ID | 2 | - | +| ACTION_FIELD_NAME_STATE | 3 | - | + + + + +### ActionState {#actionstate} + + +| Name | Number | Description | +| ---- | ------ | ----------- | +| ACTION_STATE_UNSPECIFIED | 0 | - | +| ACTION_STATE_INACTIVE | 1 | - | +| ACTION_STATE_ACTIVE | 2 | - | + + + + +### FlowFieldName {#flowfieldname} + + +| Name | Number | Description | +| ---- | ------ | ----------- | +| FLOW_FIELD_NAME_UNSPECIFIED | 0 | - | +| FLOW_FIELD_NAME_TYPE | 1 | - | +| FLOW_FIELD_NAME_STATE | 2 | - | + + + + +### FlowState {#flowstate} + + +| Name | Number | Description | +| ---- | ------ | ----------- | +| FLOW_STATE_UNSPECIFIED | 0 | - | +| FLOW_STATE_INACTIVE | 1 | - | +| FLOW_STATE_ACTIVE | 2 | - | + + + + +### FlowType {#flowtype} + + +| Name | Number | Description | +| ---- | ------ | ----------- | +| FLOW_TYPE_UNSPECIFIED | 0 | - | +| FLOW_TYPE_EXTERNAL_AUTHENTICATION | 1 | - | + + + + +### TriggerType {#triggertype} + + +| Name | Number | Description | +| ---- | ------ | ----------- | +| TRIGGER_TYPE_UNSPECIFIED | 0 | - | +| TRIGGER_TYPE_POST_AUTHENTICATION | 1 | - | +| TRIGGER_TYPE_PRE_CREATION | 2 | - | +| TRIGGER_TYPE_POST_CREATION | 3 | - | + + + + diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index 8d794931fc..3faedaa518 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -2563,6 +2563,7 @@ This is an empty request | custom_text_message | bool | - | | | custom_text_login | bool | - | | | lockout_policy | bool | - | | +| actions | bool | - | | @@ -2752,6 +2753,7 @@ This is an empty request | custom_text_message | bool | - | | | custom_text_login | bool | - | | | lockout_policy | bool | - | | +| actions | bool | - | | diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md index a4f2a22e61..88f2026164 100644 --- a/docs/docs/apis/proto/management.md +++ b/docs/docs/apis/proto/management.md @@ -2683,6 +2683,102 @@ Change JWT identity provider configuration of the organisation PUT: /idps/{idp_id}/jwt_config +### ListActions + +> **rpc** ListActions([ListActionsRequest](#listactionsrequest)) +[ListActionsResponse](#listactionsresponse) + + + + + + POST: /actions/_search + + +### GetAction + +> **rpc** GetAction([GetActionRequest](#getactionrequest)) +[GetActionResponse](#getactionresponse) + + + + + + GET: /actions/{id} + + +### CreateAction + +> **rpc** CreateAction([CreateActionRequest](#createactionrequest)) +[CreateActionResponse](#createactionresponse) + + + + + + POST: /actions + + +### UpdateAction + +> **rpc** UpdateAction([UpdateActionRequest](#updateactionrequest)) +[UpdateActionResponse](#updateactionresponse) + + + + + + PUT: /actions/{id} + + +### DeleteAction + +> **rpc** DeleteAction([DeleteActionRequest](#deleteactionrequest)) +[DeleteActionResponse](#deleteactionresponse) + + + + + + DELETE: /actions/{id} + + +### GetFlow + +> **rpc** GetFlow([GetFlowRequest](#getflowrequest)) +[GetFlowResponse](#getflowresponse) + + + + + + GET: /flows/{type} + + +### ClearFlow + +> **rpc** ClearFlow([ClearFlowRequest](#clearflowrequest)) +[ClearFlowResponse](#clearflowresponse) + + + + + + POST: /flows/{type}/_clear + + +### SetTriggerActions + +> **rpc** SetTriggerActions([SetTriggerActionsRequest](#settriggeractionsrequest)) +[SetTriggerActionsResponse](#settriggeractionsresponse) + + + + + + POST: /flows/{flow_type}/trigger/{trigger_type} + + @@ -2691,6 +2787,19 @@ Change JWT identity provider configuration of the organisation ## Messages +### ActionQuery + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.action_id_query | zitadel.action.v1.ActionIDQuery | - | | +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.action_name_query | zitadel.action.v1.ActionNameQuery | - | | +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.action_state_query | zitadel.action.v1.ActionStateQuery | - | | + + + + ### ActivateCustomLabelPolicyRequest This is an empty request @@ -3532,6 +3641,76 @@ This is an empty request +### ClearFlowRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| type | zitadel.action.v1.FlowType | - | | + + + + +### ClearFlowResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + +### CreateActionRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| name | string | - | string.min_len: 1
string.max_len: 200
| +| script | string | - | string.min_len: 1
string.max_len: 2000
| +| timeout | google.protobuf.Duration | - | duration.lte.seconds: 20
duration.lte.nanos: 0
duration.gte.seconds: 0
duration.gte.nanos: 0
| +| allowed_to_fail | bool | - | | + + + + +### CreateActionResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | +| id | string | - | | + + + + +### DeactivateActionRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| id | string | - | | + + + + +### DeactivateActionResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### DeactivateAppRequest @@ -3684,6 +3863,23 @@ This is an empty request +### DeleteActionRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| id | string | - | | + + + + +### DeleteActionResponse + + + + + ### GenerateOrgDomainValidationRequest @@ -3708,6 +3904,28 @@ This is an empty request +### GetActionRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| id | string | - | | + + + + +### GetActionResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| action | zitadel.action.v1.Action | - | | + + + + ### GetAppByIDRequest @@ -4182,6 +4400,28 @@ This is an empty request +### GetFlowRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| type | zitadel.action.v1.FlowType | - | | + + + + +### GetFlowResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| flow | zitadel.action.v1.Flow | - | | + + + + ### GetGrantedProjectByIDRequest @@ -4818,6 +5058,32 @@ This is an empty response +### ListActionsRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| query | zitadel.v1.ListQuery | list limitations and ordering | | +| sorting_column | zitadel.action.v1.ActionFieldName | the field the result is sorted | | +| queries | repeated ActionQuery | criteria the client is looking for | | + + + + +### ListActionsResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ListDetails | - | | +| sorting_column | zitadel.action.v1.ActionFieldName | - | | +| result | repeated zitadel.action.v1.Action | - | | + + + + ### ListAppChangesRequest @@ -5545,6 +5811,28 @@ This is an empty request +### ReactivateActionRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| id | string | - | | + + + + +### ReactivateActionResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### ReactivateAppRequest @@ -7013,6 +7301,30 @@ This is an empty request +### SetTriggerActionsRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| flow_type | zitadel.action.v1.FlowType | - | | +| trigger_type | zitadel.action.v1.TriggerType | - | | +| action_ids | repeated string | - | | + + + + +### SetTriggerActionsResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### SetUserMetadataRequest @@ -7084,6 +7396,32 @@ This is an empty request +### UpdateActionRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| id | string | - | string.min_len: 1
string.max_len: 200
| +| name | string | - | string.min_len: 1
string.max_len: 200
| +| script | string | - | string.min_len: 1
string.max_len: 2000
| +| timeout | google.protobuf.Duration | - | duration.lte.seconds: 20
duration.lte.nanos: 0
duration.gte.seconds: 0
duration.gte.nanos: 0
| +| allowed_to_fail | bool | - | | + + + + +### UpdateActionResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### UpdateAppRequest diff --git a/go.mod b/go.mod index 297edf0376..949e291357 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/sprig v2.22.0+incompatible + github.com/Masterminds/squirrel v1.5.0 github.com/VictoriaMetrics/fastcache v1.7.0 github.com/ajstarks/svgo v0.0.0-20210406150507-75cfd577ce75 github.com/allegro/bigcache v1.2.1 @@ -19,6 +20,8 @@ require ( github.com/caos/oidc v0.15.10 github.com/caos/orbos v1.5.14-0.20210803090517-905668247c09 github.com/cockroachdb/cockroach-go/v2 v2.1.1 + github.com/dop251/goja v0.0.0-20210817151038-07a7fd9355b4 + github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7 github.com/duo-labs/webauthn v0.0.0-20210727191636-9f1b88ef44cc github.com/envoyproxy/protoc-gen-validate v0.6.1 github.com/getsentry/sentry-go v0.11.0 diff --git a/go.sum b/go.sum index 65a21073f3..83c7ef6d16 100644 --- a/go.sum +++ b/go.sum @@ -94,6 +94,8 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/squirrel v1.5.0 h1:JukIZisrUXadA9pl3rMkjhiamxiB0cXiu+HGp/Y8cY8= +github.com/Masterminds/squirrel v1.5.0/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= @@ -231,12 +233,18 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dop251/goja v0.0.0-20210817151038-07a7fd9355b4 h1:c6+6EmiSboC79bkEtZGQhpeEWP1lzHuHpr1tPdISdYo= +github.com/dop251/goja v0.0.0-20210817151038-07a7fd9355b4/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7 h1:tYwu/z8Y0NkkzGEh3z21mSWggMg4LwLRFucLS7TjARg= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/duo-labs/webauthn v0.0.0-20210727191636-9f1b88ef44cc h1:mLNknBMRNrYNf16wFFUyhSAe1tISZN7oAfal4CZ2OxY= github.com/duo-labs/webauthn v0.0.0-20210727191636-9f1b88ef44cc/go.mod h1:/X2OJiJxjQ7alqWZqX9EtBTmZc+4qQ0LvZ1k5wP67RM= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -379,6 +387,8 @@ github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85n github.com/go-openapi/validate v0.19.8/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -690,6 +700,8 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= @@ -701,6 +713,10 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/landoop/tableprinter v0.0.0-20200805134727-ea32388e35c1/go.mod h1:f0X1c0za3TbET/rl5ThtCSel0+G3/yZ8iuU9BxnyVK0= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -816,7 +832,6 @@ github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nicksnyder/go-i18n/v2 v2.1.2 h1:QHYxcUJnGHBaq7XbvgunmZ2Pn0focXFqTD61CkH146c= github.com/nicksnyder/go-i18n/v2 v2.1.2/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -1621,8 +1636,9 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= diff --git a/internal/actions/actions.go b/internal/actions/actions.go new file mode 100644 index 0000000000..b2d2a230bb --- /dev/null +++ b/internal/actions/actions.go @@ -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) + }) +} diff --git a/internal/actions/api.go b/internal/actions/api.go new file mode 100644 index 0000000000..6204ef38e6 --- /dev/null +++ b/internal/actions/api.go @@ -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 +} diff --git a/internal/actions/context.go b/internal/actions/context.go new file mode 100644 index 0000000000..eaf7af6ee5 --- /dev/null +++ b/internal/actions/context.go @@ -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 +} diff --git a/internal/actions/provided.go b/internal/actions/provided.go new file mode 100644 index 0000000000..2409385132 --- /dev/null +++ b/internal/actions/provided.go @@ -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 +} diff --git a/internal/api/grpc/action/action.go b/internal/api/grpc/action/action.go new file mode 100644 index 0000000000..ebb93c83e3 --- /dev/null +++ b/internal/api/grpc/action/action.go @@ -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 + } +} diff --git a/internal/api/grpc/admin/features.go b/internal/api/grpc/admin/features.go index 92f666f056..25487de2da 100644 --- a/internal/api/grpc/admin/features.go +++ b/internal/api/grpc/admin/features.go @@ -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, } } diff --git a/internal/api/grpc/features/features.go b/internal/api/grpc/features/features.go index c0cd69acf9..f6045cb2cf 100644 --- a/internal/api/grpc/features/features.go +++ b/internal/api/grpc/features/features.go @@ -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, } } diff --git a/internal/api/grpc/management/actions.go b/internal/api/grpc/management/actions.go new file mode 100644 index 0000000000..3136fdb8ae --- /dev/null +++ b/internal/api/grpc/management/actions.go @@ -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 +} diff --git a/internal/api/grpc/management/actions_converter.go b/internal/api/grpc/management/actions_converter.go new file mode 100644 index 0000000000..22dbd838a0 --- /dev/null +++ b/internal/api/grpc/management/actions_converter.go @@ -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 +} diff --git a/internal/api/grpc/management/auth_checks.go b/internal/api/grpc/management/auth_checks.go new file mode 100644 index 0000000000..76fd846641 --- /dev/null +++ b/internal/api/grpc/management/auth_checks.go @@ -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 +} diff --git a/internal/api/grpc/management/flow.go b/internal/api/grpc/management/flow.go new file mode 100644 index 0000000000..22fb627be8 --- /dev/null +++ b/internal/api/grpc/management/flow.go @@ -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 +} diff --git a/internal/api/grpc/management/user_grant.go b/internal/api/grpc/management/user_grant.go index 06dfbe8302..3b0978ce11 100644 --- a/internal/api/grpc/management/user_grant.go +++ b/internal/api/grpc/management/user_grant.go @@ -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 } diff --git a/internal/api/grpc/object/converter.go b/internal/api/grpc/object/converter.go index f708e086a7..ab907da26d 100644 --- a/internal/api/grpc/object/converter.go +++ b/internal/api/grpc/object/converter.go @@ -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 + } +} diff --git a/internal/api/grpc/server/gateway.go b/internal/api/grpc/server/gateway.go index 2a54f065ae..f66f8e527d 100644 --- a/internal/api/grpc/server/gateway.go +++ b/internal/api/grpc/server/gateway.go @@ -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) } diff --git a/internal/auth/repository/auth_request.go b/internal/auth/repository/auth_request.go index 6c0f28350c..a395964513 100644 --- a/internal/auth/repository/auth_request.go +++ b/internal/auth/repository/auth_request.go @@ -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 } diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index eb0d057daa..0241911db7 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -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) } diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index ea937b4ca4..57210efaa8 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -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 diff --git a/internal/command/command.go b/internal/command/command.go index 55fb4f2e89..aeb62967f6 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -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 { diff --git a/internal/command/features_model.go b/internal/command/features_model.go index f7813bf872..d33c27c326 100644 --- a/internal/command/features_model.go +++ b/internal/command/features_model.go @@ -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 } diff --git a/internal/command/flow_model.go b/internal/command/flow_model.go new file mode 100644 index 0000000000..2dab620f57 --- /dev/null +++ b/internal/command/flow_model.go @@ -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 + } + } +} diff --git a/internal/command/iam_features.go b/internal/command/iam_features.go index 0c33d5b585..96dde49120 100644 --- a/internal/command/iam_features.go +++ b/internal/command/iam_features.go @@ -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") diff --git a/internal/command/iam_features_model.go b/internal/command/iam_features_model.go index 372008ab40..a88f8fa01c 100644 --- a/internal/command/iam_features_model.go +++ b/internal/command/iam_features_model.go @@ -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 } diff --git a/internal/command/main_test.go b/internal/command/main_test.go index 9bd3b87d0e..8a2a837f47 100644 --- a/internal/command/main_test.go +++ b/internal/command/main_test.go @@ -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 } diff --git a/internal/command/org_action.go b/internal/command/org_action.go new file mode 100644 index 0000000000..886cfe28ca --- /dev/null +++ b/internal/command/org_action.go @@ -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 +} diff --git a/internal/command/org_action_model.go b/internal/command/org_action_model.go new file mode 100644 index 0000000000..07b3cac527 --- /dev/null +++ b/internal/command/org_action_model.go @@ -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() +} diff --git a/internal/command/org_action_test.go b/internal/command/org_action_test.go new file mode 100644 index 0000000000..2d1558b017 --- /dev/null +++ b/internal/command/org_action_test.go @@ -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) + } + }) + } +} diff --git a/internal/command/org_features.go b/internal/command/org_features.go index 6a62fbd562..0266a74bf3 100644 --- a/internal/command/org_features.go +++ b/internal/command/org_features.go @@ -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 } diff --git a/internal/command/org_features_model.go b/internal/command/org_features_model.go index 5011743901..b2fa2e805b 100644 --- a/internal/command/org_features_model.go +++ b/internal/command/org_features_model.go @@ -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 diff --git a/internal/command/org_features_test.go b/internal/command/org_features_test.go index 839ee41b28..da01a76f50 100644 --- a/internal/command/org_features_test.go +++ b/internal/command/org_features_test.go @@ -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( diff --git a/internal/command/org_flow.go b/internal/command/org_flow.go new file mode 100644 index 0000000000..aa3f68b14c --- /dev/null +++ b/internal/command/org_flow.go @@ -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 +} diff --git a/internal/command/org_flow_model.go b/internal/command/org_flow_model.go new file mode 100644 index 0000000000..947a6e5ac6 --- /dev/null +++ b/internal/command/org_flow_model.go @@ -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() +} diff --git a/internal/command/org_flow_test.go b/internal/command/org_flow_test.go new file mode 100644 index 0000000000..9ead320fab --- /dev/null +++ b/internal/command/org_flow_test.go @@ -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) + } + }) + } +} diff --git a/internal/command/user_grant.go b/internal/command/user_grant.go index e02fe9e3e6..6f6cf7d8d9 100644 --- a/internal/command/user_grant.go +++ b/internal/command/user_grant.go @@ -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") } diff --git a/internal/command/user_grant_test.go b/internal/command/user_grant_test.go index 6afb871805..ed4c227e69 100644 --- a/internal/command/user_grant_test.go +++ b/internal/command/user_grant_test.go @@ -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{ diff --git a/internal/domain/action.go b/internal/domain/action.go new file mode 100644 index 0000000000..8bfd80b90d --- /dev/null +++ b/internal/domain/action.go @@ -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 +} diff --git a/internal/domain/auth_request.go b/internal/domain/auth_request.go index 77f56d07e7..5d8fc6cdbb 100644 --- a/internal/domain/auth_request.go +++ b/internal/domain/auth_request.go @@ -68,6 +68,7 @@ type ExternalUser struct { PreferredLanguage language.Tag Phone string IsPhoneVerified bool + Metadatas []*Metadata } type Prompt int32 diff --git a/internal/domain/features.go b/internal/domain/features.go index a87e1db3d0..68a85c5124 100644 --- a/internal/domain/features.go +++ b/internal/domain/features.go @@ -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 diff --git a/internal/domain/flow.go b/internal/domain/flow.go new file mode 100644 index 0000000000..7e68def74a --- /dev/null +++ b/internal/domain/flow.go @@ -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 +} diff --git a/internal/features/model/features_view.go b/internal/features/model/features_view.go index e5f83203fc..3aed63a022 100644 --- a/internal/features/model/features_view.go +++ b/internal/features/model/features_view.go @@ -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 } diff --git a/internal/features/repository/view/model/features.go b/internal/features/repository/view/model/features.go index 108c2462a6..13f2463bbe 100644 --- a/internal/features/repository/view/model/features.go +++ b/internal/features/repository/view/model/features.go @@ -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, } } diff --git a/internal/query/action.go b/internal/query/action.go new file mode 100644 index 0000000000..191f966b04 --- /dev/null +++ b/internal/query/action.go @@ -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) +} diff --git a/internal/query/action_flow.go b/internal/query/action_flow.go new file mode 100644 index 0000000000..a391627c8d --- /dev/null +++ b/internal/query/action_flow.go @@ -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) +} diff --git a/internal/query/projection/action.go b/internal/query/projection/action.go new file mode 100644 index 0000000000..6d299c2a51 --- /dev/null +++ b/internal/query/projection/action.go @@ -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 +} diff --git a/internal/query/projection/flow/flow.go b/internal/query/projection/flow/flow.go new file mode 100644 index 0000000000..0cbae08e7b --- /dev/null +++ b/internal/query/projection/flow/flow.go @@ -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 +} diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 2bf21d7c10..4668a48bfd 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -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 } diff --git a/internal/query/query.go b/internal/query/query.go index e69311b633..dfb3303437 100644 --- a/internal/query/query.go +++ b/internal/query/query.go @@ -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 } diff --git a/internal/query/search_query.go b/internal/query/search_query.go new file mode 100644 index 0000000000..e380783fbc --- /dev/null +++ b/internal/query/search_query.go @@ -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 +) diff --git a/internal/repository/action/action.go b/internal/repository/action/action.go new file mode 100644 index 0000000000..eff0c6cbd3 --- /dev/null +++ b/internal/repository/action/action.go @@ -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 +} diff --git a/internal/repository/action/aggregate.go b/internal/repository/action/aggregate.go new file mode 100644 index 0000000000..909295a2ca --- /dev/null +++ b/internal/repository/action/aggregate.go @@ -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, + }, + } +} diff --git a/internal/repository/action/eventstore.go b/internal/repository/action/eventstore.go new file mode 100644 index 0000000000..98733a244f --- /dev/null +++ b/internal/repository/action/eventstore.go @@ -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) +} diff --git a/internal/repository/features/features.go b/internal/repository/features/features.go index 4fc64bc94f..3521b49cf0 100644 --- a/internal/repository/features/features.go +++ b/internal/repository/features/features.go @@ -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), diff --git a/internal/repository/flow/flow.go b/internal/repository/flow/flow.go new file mode 100644 index 0000000000..a84de9413c --- /dev/null +++ b/internal/repository/flow/flow.go @@ -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 +} diff --git a/internal/repository/org/eventstore.go b/internal/repository/org/eventstore.go index 068906edea..a010464d2f 100644 --- a/internal/repository/org/eventstore.go +++ b/internal/repository/org/eventstore.go @@ -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) } diff --git a/internal/repository/org/flow.go b/internal/repository/org/flow.go new file mode 100644 index 0000000000..d13e4d4bfd --- /dev/null +++ b/internal/repository/org/flow.go @@ -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 +} diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 548f8fd9c0..a233679342 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -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: diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index cf22967d37..9f642c5067 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -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: diff --git a/internal/ui/login/handler/custom_action.go b/internal/ui/login/handler/custom_action.go new file mode 100644 index 0000000000..cdb06014f3 --- /dev/null +++ b/internal/ui/login/handler/custom_action.go @@ -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 +} diff --git a/internal/ui/login/handler/external_login_handler.go b/internal/ui/login/handler/external_login_handler.go index dc6cbdaccf..014f41a595 100644 --- a/internal/ui/login/handler/external_login_handler.go +++ b/internal/ui/login/handler/external_login_handler.go @@ -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 } diff --git a/internal/ui/login/handler/external_register_handler.go b/internal/ui/login/handler/external_register_handler.go index 578b85598e..55fd792c19 100644 --- a/internal/ui/login/handler/external_register_handler.go +++ b/internal/ui/login/handler/external_register_handler.go @@ -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 diff --git a/internal/ui/login/handler/jwt_handler.go b/internal/ui/login/handler/jwt_handler.go index f0db930f36..8faf1e7548 100644 --- a/internal/ui/login/handler/jwt_handler.go +++ b/internal/ui/login/handler/jwt_handler.go @@ -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 diff --git a/migrations/cockroach/V1.72__actions.sql b/migrations/cockroach/V1.72__actions.sql new file mode 100644 index 0000000000..d8512b2954 --- /dev/null +++ b/migrations/cockroach/V1.72__actions.sql @@ -0,0 +1,63 @@ +CREATE TABLE zitadel.projections.actions ( + id TEXT, + creation_date TIMESTAMPTZ, + change_date TIMESTAMPTZ, + resource_owner TEXT, + action_state SMALLINT, + sequence BIGINT, + + name TEXT, + script TEXT, + timeout BIGINT, + allowed_to_fail BOOLEAN, + + PRIMARY KEY (id) +); + +CREATE TABLE zitadel.projections.flows_actions ( + id TEXT, + creation_date TIMESTAMPTZ, + change_date TIMESTAMPTZ, + resource_owner TEXT, + sequence BIGINT, + + name TEXT, + script TEXT, + timeout BIGINT, + allowed_to_fail BOOLEAN, + + PRIMARY KEY (id) +); + +CREATE TABLE zitadel.projections.flows_triggers ( + flow_type SMALLINT, + trigger_type SMALLINT, + resource_owner TEXT, + action_id TEXT, + trigger_sequence SMALLINT, + + PRIMARY KEY (flow_type, trigger_type, resource_owner, action_id), + CONSTRAINT fk_action FOREIGN KEY (action_id) REFERENCES zitadel.projections.flows_actions (id) ON DELETE CASCADE +); + +CREATE VIEW zitadel.projections.flows_actions_triggers AS ( + SELECT a.id AS action_id, + a.name, + a.creation_date, + a.resource_owner, + a.sequence, + a.change_date, + a.script, + a.timeout, + a.allowed_to_fail, + t.flow_type, + t.trigger_type, + t.trigger_sequence + FROM zitadel.projections.flows_triggers t + JOIN zitadel.projections.flows_actions a ON t.action_id = a.id + ); + +ALTER TABLE auth.features ADD COLUMN actions BOOLEAN; +ALTER TABLE authz.features ADD COLUMN actions BOOLEAN; +ALTER TABLE adminapi.features ADD COLUMN actions BOOLEAN; +ALTER TABLE management.features ADD COLUMN actions BOOLEAN; diff --git a/proto/zitadel/action.proto b/proto/zitadel/action.proto new file mode 100644 index 0000000000..94498af0b8 --- /dev/null +++ b/proto/zitadel/action.proto @@ -0,0 +1,155 @@ +syntax = "proto3"; + +import "zitadel/object.proto"; +import "validate/validate.proto"; +import "google/protobuf/duration.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; + +package zitadel.action.v1; + +option go_package ="github.com/caos/zitadel/pkg/grpc/action"; + +message Action { + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; + zitadel.v1.ObjectDetails details = 2; + ActionState state = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the state of the action"; + } + ]; + string name = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"log context\""; + } + ]; + string script = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"function log(context, calls){console.log(context)}\""; + } + ]; + google.protobuf.Duration timeout = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "after which time the action will be terminated if not finished"; + } + ]; + bool allowed_to_fail = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "when true, the next action will be called even if this action fails"; + } + ]; +} + +enum ActionState { + ACTION_STATE_UNSPECIFIED = 0; + ACTION_STATE_INACTIVE = 1; + ACTION_STATE_ACTIVE = 2; +} + +message ActionIDQuery { + string id = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; +} + +message ActionNameQuery { + string name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"log\""; + } + ]; + zitadel.v1.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which text equality method is used"; + } + ]; +} + +//ActionStateQuery is always equals +message ActionStateQuery { + ActionState state = 1 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the action"; + } + ]; +} + +enum ActionFieldName { + ACTION_FIELD_NAME_UNSPECIFIED = 0; + ACTION_FIELD_NAME_NAME = 1; + ACTION_FIELD_NAME_ID = 2; + ACTION_FIELD_NAME_STATE = 3; +} + +message Flow { + FlowType type = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"the type of the flow\""; + } + ]; + zitadel.v1.ObjectDetails details = 2; + FlowState state = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the state of the flow"; + } + ]; + repeated TriggerAction trigger_actions = 4; +} + +enum FlowType { + FLOW_TYPE_UNSPECIFIED = 0; + FLOW_TYPE_EXTERNAL_AUTHENTICATION = 1; +} + +enum FlowState { + FLOW_STATE_UNSPECIFIED = 0; + FLOW_STATE_INACTIVE = 1; + FLOW_STATE_ACTIVE = 2; +} + +enum TriggerType { + TRIGGER_TYPE_UNSPECIFIED = 0; + TRIGGER_TYPE_POST_AUTHENTICATION = 1; + TRIGGER_TYPE_PRE_CREATION = 2; + TRIGGER_TYPE_POST_CREATION = 3; +} + +message TriggerAction { + TriggerType trigger_type = 1; + repeated Action actions = 2; +} + +enum FlowFieldName { + FLOW_FIELD_NAME_UNSPECIFIED = 0; + FLOW_FIELD_NAME_TYPE = 1; + FLOW_FIELD_NAME_STATE = 2; +} + +//FlowTypeQuery is always equals +message FlowTypeQuery { + FlowType state = 1 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "type of the flow"; + } + ]; +} + +//FlowStateQuery is always equals +message FlowStateQuery { + FlowState state = 1 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the flow"; + } + ]; +} diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 6dada623c4..1dc87fe1f5 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -2813,6 +2813,7 @@ message SetDefaultFeaturesRequest { bool custom_text_message = 20; bool custom_text_login = 21; bool lockout_policy = 22; + bool actions = 23; } message SetDefaultFeaturesResponse { @@ -2852,6 +2853,7 @@ message SetOrgFeaturesRequest { bool custom_text_message = 21; bool custom_text_login = 22; bool lockout_policy = 23; + bool actions = 24; } message SetOrgFeaturesResponse { diff --git a/proto/zitadel/features.proto b/proto/zitadel/features.proto index 02ba262ce7..660c8f7db7 100644 --- a/proto/zitadel/features.proto +++ b/proto/zitadel/features.proto @@ -30,6 +30,7 @@ message Features { bool custom_text_message = 19; bool custom_text_login = 20; bool lockout_policy = 21; + bool actions = 22; } message FeatureTier { @@ -45,4 +46,4 @@ enum FeaturesState { FEATURES_STATE_ACTION_REQUIRED = 1; FEATURES_STATE_CANCELED = 2; FEATURES_STATE_GRANDFATHERED = 3; -} \ No newline at end of file +} diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 10ae0ad9eb..ea917092d4 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -15,6 +15,7 @@ import "zitadel/change.proto"; import "zitadel/auth_n_key.proto"; import "zitadel/features.proto"; import "zitadel/metadata.proto"; +import "zitadel/action.proto"; import "google/api/annotations.proto"; import "google/protobuf/timestamp.proto"; @@ -2710,6 +2711,124 @@ service ManagementService { feature: "login_policy.idp" }; } + + rpc ListActions(ListActionsRequest) returns (ListActionsResponse) { + option (google.api.http) = { + post: "/actions/_search" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.action.read" + feature: "actions" + }; + } + + rpc GetAction(GetActionRequest) returns (GetActionResponse) { + option (google.api.http) = { + get: "/actions/{id}" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.action.read" + feature: "actions" + }; + } + + rpc CreateAction(CreateActionRequest) returns (CreateActionResponse) { + option (google.api.http) = { + post: "/actions" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.action.write" + feature: "actions" + }; + } + + rpc UpdateAction(UpdateActionRequest) returns (UpdateActionResponse) { + option (google.api.http) = { + put: "/actions/{id}" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.action.write" + feature: "actions" + }; + } + +//TODO: enable in next release +// rpc DeactivateAction(DeactivateActionRequest) returns (DeactivateActionResponse) { +// option (google.api.http) = { +// post: "/actions/{id}/_deactivate" +// body: "*" +// }; +// +// option (zitadel.v1.auth_option) = { +// permission: "org.action.write" +// feature: "actions" +// }; +// } +// +// rpc ReactivateAction(ReactivateActionRequest) returns (ReactivateActionResponse) { +// option (google.api.http) = { +// post: "/actions/{id}/_reactivate" +// body: "*" +// }; +// +// option (zitadel.v1.auth_option) = { +// permission: "org.action.write" +// feature: "actions" +// }; +// } + + rpc DeleteAction(DeleteActionRequest) returns (DeleteActionResponse) { + option (google.api.http) = { + delete: "/actions/{id}" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.action.delete" + feature: "actions" + }; + } + + rpc GetFlow(GetFlowRequest) returns (GetFlowResponse) { + option (google.api.http) = { + get: "/flows/{type}" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.flow.read" + feature: "actions" + }; + } + + rpc ClearFlow(ClearFlowRequest) returns (ClearFlowResponse) { + option (google.api.http) = { + post: "/flows/{type}/_clear" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.flow.delete" + feature: "actions" + }; + } + + rpc SetTriggerActions(SetTriggerActionsRequest) returns (SetTriggerActionsResponse) { + option (google.api.http) = { + post: "/flows/{flow_type}/trigger/{trigger_type}" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.flow.write" + feature: "actions" + }; + } } //This is an empty request @@ -5117,3 +5236,151 @@ message UpdateOrgIDPJWTConfigRequest { message UpdateOrgIDPJWTConfigResponse { zitadel.v1.ObjectDetails details = 1; } + +message ListActionsRequest { + //list limitations and ordering + zitadel.v1.ListQuery query = 1; + //the field the result is sorted + zitadel.action.v1.ActionFieldName sorting_column = 2; + //criteria the client is looking for + repeated ActionQuery queries = 3; +} + +message ActionQuery { + oneof query { + option (validate.required) = true; + + zitadel.action.v1.ActionIDQuery action_id_query = 1; + zitadel.action.v1.ActionNameQuery action_name_query = 2; + zitadel.action.v1.ActionStateQuery action_state_query = 3; + } +} + +message ListActionsResponse { + zitadel.v1.ListDetails details = 1; + zitadel.action.v1.ActionFieldName sorting_column = 2; + repeated zitadel.action.v1.Action result = 3; +} + +message CreateActionRequest { + string name = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"log context\""; + } + ]; + string script = 2 [ + (validate.rules).string = {min_len: 1, max_len: 2000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"function log(context, calls){console.log(context)}\""; + } + ]; + google.protobuf.Duration timeout = 3 [ + (validate.rules).duration = {gte: {}, lte: {seconds: 20}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "after which time the action will be terminated if not finished"; + } + ]; + bool allowed_to_fail = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "when true, the next action will be called even if this action fails"; + } + ]; +} + +message CreateActionResponse { + zitadel.v1.ObjectDetails details = 1; + string id = 2; +} + +message GetActionRequest { + string id = 1; +} + +message GetActionResponse { + zitadel.action.v1.Action action = 1; +} + +message UpdateActionRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; + string name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"log context\""; + } + ]; + string script = 3 [ + (validate.rules).string = {min_len: 1, max_len: 2000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"function log(context, calls){console.log(context)}\""; + } + ]; + google.protobuf.Duration timeout = 4 [ + (validate.rules).duration = {gte: {}, lte: {seconds: 20}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "after which time the action will be terminated if not finished"; + } + ]; + bool allowed_to_fail = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "when true, the next action will be called even if this action fails"; + } + ]; +} + +message UpdateActionResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message DeleteActionRequest { + string id = 1; +} + +message DeleteActionResponse {} + +message DeactivateActionRequest { + string id = 1; +} + +message DeactivateActionResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message ReactivateActionRequest { + string id = 1; +} + +message ReactivateActionResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message GetFlowRequest { + zitadel.action.v1.FlowType type = 1; +} + +message GetFlowResponse { + zitadel.action.v1.Flow flow = 1; +} + +message ClearFlowRequest { + zitadel.action.v1.FlowType type = 1; +} + +message ClearFlowResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message SetTriggerActionsRequest { + zitadel.action.v1.FlowType flow_type = 1; + zitadel.action.v1.TriggerType trigger_type = 2; + repeated string action_ids = 3; +} + +message SetTriggerActionsResponse { + zitadel.v1.ObjectDetails details = 1; +}