feat: User metadata (#2025)

* feat: user meta data events

* feat: user meta data set tests

* feat: user meta data tests

* feat: user meta data in protos

* feat: user meta data command api

* feat: user meta data query side

* feat: proto correct order, fix handlers

* feat: proto correct order

* feat: fixes of pr comments

* feat: fixes of pr comments

* feat: value as byte array

* feat: metadata feature

* Update internal/auth/repository/eventsourcing/handler/meta_data.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* Update internal/command/user_meta_data.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* Update proto/zitadel/metadata.proto

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* Update proto/zitadel/metadata.proto

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* fix: rename metadata files and table

* fix: rename meta data to metadat in protos

* Update internal/domain/metadata.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* fix: rename vars

* fix: rebiuld docs

* Update internal/iam/repository/view/metadata_view.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

Co-authored-by: Silvan <silvan.reusser@gmail.com>
This commit is contained in:
Fabi 2021-08-09 08:01:20 +02:00 committed by GitHub
parent ae50f57c2c
commit 7451ed58f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 3725 additions and 4 deletions

View File

@ -67,8 +67,10 @@ protoc \
-I=/proto/include \
--grpc-gateway_out ${GOPATH}/src \
--grpc-gateway_opt logtostderr=true \
--grpc-gateway_opt allow_delete_body=true \
--openapiv2_out ${OPENAPI_PATH} \
--openapiv2_opt logtostderr=true \
--openapiv2_opt allow_delete_body=true \
--authoption_out=${GRPC_PATH}/auth \
--validate_out=lang=go:${GOPATH}/src \
${PROTO_PATH}/auth.proto
@ -114,6 +116,10 @@ protoc \
-I=/proto/include \
--doc_out=${DOCS_PATH} --doc_opt=${PROTO_PATH}/docs/zitadel-md.tmpl,message.md \
${PROTO_PATH}/message.proto
protoc \
-I=/proto/include \
--doc_out=${DOCS_PATH} --doc_opt=${PROTO_PATH}/docs/zitadel-md.tmpl,metadata.md \
${PROTO_PATH}/metadata.proto
protoc \
-I=/proto/include \
--doc_out=${DOCS_PATH} --doc_opt=${PROTO_PATH}/docs/zitadel-md.tmpl,object.md \
@ -134,9 +140,14 @@ protoc \
-I=/proto/include \
--doc_out=${DOCS_PATH} --doc_opt=${PROTO_PATH}/docs/zitadel-md.tmpl,project.md \
${PROTO_PATH}/project.proto
protoc \
-I=/proto/include \
--doc_out=${DOCS_PATH} --doc_opt=${PROTO_PATH}/docs/zitadel-md.tmpl,text.md \
${PROTO_PATH}/text.proto
protoc \
-I=/proto/include \
--doc_out=${DOCS_PATH} --doc_opt=${PROTO_PATH}/docs/zitadel-md.tmpl,user.md \
${PROTO_PATH}/user.proto
echo "done generating grpc"

View File

@ -67,6 +67,78 @@ Returns the user sessions of the authorized user of the current useragent
POST: /users/me/sessions/_search
### SetMyMetadata
> **rpc** SetMyMetadata([SetMyMetadataRequest](#setmymetadatarequest))
[SetMyMetadataResponse](#setmymetadataresponse)
Sets a user metadata by key to the authorized user
POST: /users/me/metadata/{key}
### BulkSetMyMetadata
> **rpc** BulkSetMyMetadata([BulkSetMyMetadataRequest](#bulksetmymetadatarequest))
[BulkSetMyMetadataResponse](#bulksetmymetadataresponse)
Set a list of user metadata to the authorized user
POST: /users/me/metadata/_bulk
### ListMyMetadata
> **rpc** ListMyMetadata([ListMyMetadataRequest](#listmymetadatarequest))
[ListMyMetadataResponse](#listmymetadataresponse)
Returns the user metadata of the authorized user
POST: /users/me/metadata/_search
### GetMyMetadata
> **rpc** GetMyMetadata([GetMyMetadataRequest](#getmymetadatarequest))
[GetMyMetadataResponse](#getmymetadataresponse)
Returns the user metadata by key of the authorized user
GET: /users/me/metadata/{key}
### RemoveMyMetadata
> **rpc** RemoveMyMetadata([RemoveMyMetadataRequest](#removemymetadatarequest))
[RemoveMyMetadataResponse](#removemymetadataresponse)
Removes a user metadata by key to the authorized user
DELETE: /users/me/metadata/{key}
### BulkRemoveMyMetadata
> **rpc** BulkRemoveMyMetadata([BulkRemoveMyMetadataRequest](#bulkremovemymetadatarequest))
[BulkRemoveMyMetadataResponse](#bulkremovemymetadataresponse)
Set a list of user metadata to the authorized user
DELETE: /users/me/metadata/_bulk
### ListMyRefreshTokens
> **rpc** ListMyRefreshTokens([ListMyRefreshTokensRequest](#listmyrefreshtokensrequest))
@ -628,6 +700,62 @@ This is an empty request
### BulkRemoveMyMetadataRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| keys | repeated string | - | repeated.items.string.min_len: 1<br /> repeated.items.string.max_len: 200<br /> |
### BulkRemoveMyMetadataResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### BulkSetMyMetadataRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| metadata | repeated BulkSetMyMetadataRequest.Metadata | - | |
### BulkSetMyMetadataRequest.Metadata
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| key | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| value | bytes | - | bytes.min_len: 1<br /> bytes.max_len: 500000<br /> |
### BulkSetMyMetadataResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### GetMyEmailRequest
This is an empty request
@ -646,6 +774,28 @@ This is an empty request
### GetMyMetadataRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| key | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### GetMyMetadataResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| metadata | zitadel.metadata.v1.Metadata | - | |
### GetMyPasswordComplexityPolicyRequest
This is an empty request
@ -811,6 +961,30 @@ This is an empty request
### ListMyMetadataRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| query | zitadel.v1.ListQuery | - | |
| queries | repeated zitadel.metadata.v1.MetadataQuery | - | |
### ListMyMetadataResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ListDetails | - | |
| result | repeated zitadel.metadata.v1.Metadata | - | |
### ListMyPasswordlessRequest
This is an empty request
@ -1063,6 +1237,28 @@ This is an empty request
### RemoveMyMetadataRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| key | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### RemoveMyMetadataResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### RemoveMyPasswordlessRequest
@ -1209,6 +1405,29 @@ This is an empty request
### SetMyMetadataRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| key | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| value | bytes | - | bytes.min_len: 1<br /> bytes.max_len: 500000<br /> |
### SetMyMetadataResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### SetMyPhoneRequest

View File

@ -236,6 +236,78 @@ Changes the username
GET: /users/{user_id}/username
### SetUserMetadata
> **rpc** SetUserMetadata([SetUserMetadataRequest](#setusermetadatarequest))
[SetUserMetadataResponse](#setusermetadataresponse)
Sets a user metadata by key
POST: /users/{id}/metadata/{key}
### BulkSetUserMetadata
> **rpc** BulkSetUserMetadata([BulkSetUserMetadataRequest](#bulksetusermetadatarequest))
[BulkSetUserMetadataResponse](#bulksetusermetadataresponse)
Set a list of user metadata
POST: /users/{id}/metadata/_bulk
### ListUserMetadata
> **rpc** ListUserMetadata([ListUserMetadataRequest](#listusermetadatarequest))
[ListUserMetadataResponse](#listusermetadataresponse)
Returns the user metadata
POST: /users/{id}/metadata/_search
### GetUserMetadata
> **rpc** GetUserMetadata([GetUserMetadataRequest](#getusermetadatarequest))
[GetUserMetadataResponse](#getusermetadataresponse)
Returns the user metadata by key
GET: /users/{id}/metadata/{key}
### RemoveUserMetadata
> **rpc** RemoveUserMetadata([RemoveUserMetadataRequest](#removeusermetadatarequest))
[RemoveUserMetadataResponse](#removeusermetadataresponse)
Removes a user metadata by key
DELETE: /users/{id}/metadata/{key}
### BulkRemoveUserMetadata
> **rpc** BulkRemoveUserMetadata([BulkRemoveUserMetadataRequest](#bulkremoveusermetadatarequest))
[BulkRemoveUserMetadataResponse](#bulkremoveusermetadataresponse)
Set a list of user metadata
DELETE: /users/{id}/metadata/_bulk
### GetHumanProfile
> **rpc** GetHumanProfile([GetHumanProfileRequest](#gethumanprofilerequest))
@ -3347,6 +3419,64 @@ This is an empty request
### BulkRemoveUserMetadataRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| keys | repeated string | - | repeated.items.string.min_len: 1<br /> repeated.items.string.max_len: 200<br /> |
### BulkRemoveUserMetadataResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### BulkSetUserMetadataRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| metadata | repeated BulkSetUserMetadataRequest.Metadata | - | |
### BulkSetUserMetadataRequest.Metadata
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| key | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| value | bytes | - | bytes.min_len: 1<br /> bytes.max_len: 500000<br /> |
### BulkSetUserMetadataResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### DeactivateAppRequest
@ -4480,6 +4610,29 @@ This is an empty request
### GetUserMetadataRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| key | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### GetUserMetadataResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| metadata | zitadel.metadata.v1.Metadata | - | |
### HealthzRequest
This is an empty request
@ -5264,6 +5417,31 @@ This is an empty request
### ListUserMetadataRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| query | zitadel.v1.ListQuery | - | |
| queries | repeated zitadel.metadata.v1.MetadataQuery | - | |
### ListUserMetadataResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ListDetails | - | |
| result | repeated zitadel.metadata.v1.Metadata | - | |
### ListUsersRequest
@ -6068,6 +6246,29 @@ This is an empty response
### RemoveUserMetadataRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| key | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### RemoveUserMetadataResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### RemoveUserRequest
@ -6756,6 +6957,31 @@ This is an empty request
### SetUserMetadataRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| key | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| value | bytes | - | bytes.min_len: 1<br /> bytes.max_len: 500000<br /> |
### SetUserMetadataResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| id | string | - | |
| details | zitadel.v1.ObjectDetails | - | |
### UnlockUserRequest

View File

@ -0,0 +1,49 @@
---
title: zitadel/metadata.proto
---
> This document reflects the state from API 1.0 (available from 20.04.2021)
## Messages
### Metadata
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
| key | string | - | |
| value | bytes | - | |
### MetadataKeyQuery
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| key | string | - | string.max_len: 200<br /> |
| method | zitadel.v1.TextQueryMethod | - | enum.defined_only: true<br /> |
### MetadataQuery
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.key_query | MetadataKeyQuery | - | |

View File

@ -0,0 +1,601 @@
---
title: zitadel/text.proto
---
> This document reflects the state from API 1.0 (available from 20.04.2021)
## Messages
### EmailVerificationDoneScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
| cancel_button_text | string | - | string.max_len: 100<br /> |
| login_button_text | string | - | string.max_len: 100<br /> |
### EmailVerificationScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| code_label | string | - | string.max_len: 200<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
| resend_button_text | string | - | string.max_len: 100<br /> |
### ExternalUserNotFoundScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| link_button_text | string | - | string.max_len: 100<br /> |
| auto_register_button_text | string | - | string.max_len: 100<br /> |
### FooterText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| tos | string | - | string.max_len: 200<br /> |
| privacy_policy | string | - | string.max_len: 200<br /> |
| help | string | - | string.max_len: 200<br /> |
| help_link | string | - | string.max_len: 500<br /> |
### InitMFADoneScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| cancel_button_text | string | - | string.max_len: 100<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
### InitMFAOTPScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| description_otp | string | - | string.max_len: 500<br /> |
| secret_label | string | - | string.max_len: 200<br /> |
| code_label | string | - | string.max_len: 200<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
| cancel_button_text | string | - | string.max_len: 100<br /> |
### InitMFAPromptScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| otp_option | string | - | string.max_len: 200<br /> |
| u2f_option | string | - | string.max_len: 200<br /> |
| skip_button_text | string | - | string.max_len: 100<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
### InitMFAU2FScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| token_name_label | string | - | string.max_len: 200<br /> |
| not_supported | string | - | string.max_len: 500<br /> |
| register_token_button_text | string | - | string.max_len: 100<br /> |
| error_retry | string | - | string.max_len: 500<br /> |
### InitPasswordDoneScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
| cancel_button_text | string | - | string.max_len: 100<br /> |
### InitPasswordScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| code_label | string | - | string.max_len: 200<br /> |
| new_password_label | string | - | string.max_len: 200<br /> |
| new_password_confirm_label | string | - | string.max_len: 200<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
| resend_button_text | string | - | string.max_len: 100<br /> |
### InitializeUserDoneScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| cancel_button_text | string | - | string.max_len: 100<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
### InitializeUserScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| code_label | string | - | string.max_len: 200<br /> |
| new_password_label | string | - | string.max_len: 200<br /> |
| new_password_confirm_label | string | - | string.max_len: 200<br /> |
| resend_button_text | string | - | string.max_len: 100<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
### LinkingUserDoneScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| cancel_button_text | string | - | string.max_len: 100<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
### LoginCustomText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
| select_account_text | SelectAccountScreenText | - | |
| login_text | LoginScreenText | - | |
| password_text | PasswordScreenText | - | |
| username_change_text | UsernameChangeScreenText | - | |
| username_change_done_text | UsernameChangeDoneScreenText | - | |
| init_password_text | InitPasswordScreenText | - | |
| init_password_done_text | InitPasswordDoneScreenText | - | |
| email_verification_text | EmailVerificationScreenText | - | |
| email_verification_done_text | EmailVerificationDoneScreenText | - | |
| initialize_user_text | InitializeUserScreenText | - | |
| initialize_done_text | InitializeUserDoneScreenText | - | |
| init_mfa_prompt_text | InitMFAPromptScreenText | - | |
| init_mfa_otp_text | InitMFAOTPScreenText | - | |
| init_mfa_u2f_text | InitMFAU2FScreenText | - | |
| init_mfa_done_text | InitMFADoneScreenText | - | |
| mfa_providers_text | MFAProvidersText | - | |
| verify_mfa_otp_text | VerifyMFAOTPScreenText | - | |
| verify_mfa_u2f_text | VerifyMFAU2FScreenText | - | |
| passwordless_text | PasswordlessScreenText | - | |
| password_change_text | PasswordChangeScreenText | - | |
| password_change_done_text | PasswordChangeDoneScreenText | - | |
| password_reset_done_text | PasswordResetDoneScreenText | - | |
| registration_option_text | RegistrationOptionScreenText | - | |
| registration_user_text | RegistrationUserScreenText | - | |
| registration_org_text | RegistrationOrgScreenText | - | |
| linking_user_done_text | LinkingUserDoneScreenText | - | |
| external_user_not_found_text | ExternalUserNotFoundScreenText | - | |
| success_login_text | SuccessLoginScreenText | - | |
| logout_text | LogoutDoneScreenText | - | |
| footer_text | FooterText | - | |
| passwordless_prompt_text | PasswordlessPromptScreenText | - | |
| passwordless_registration_text | PasswordlessRegistrationScreenText | - | |
| passwordless_registration_done_text | PasswordlessRegistrationDoneScreenText | - | |
### LoginScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| title_linking_process | string | - | string.max_len: 200<br /> |
| description_linking_process | string | - | string.max_len: 500<br /> |
| user_must_be_member_of_org | string | - | string.max_len: 500<br /> |
| login_name_label | string | - | string.max_len: 200<br /> |
| register_button_text | string | - | string.max_len: 100<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
| external_user_description | string | - | string.max_len: 500<br /> |
| user_name_placeholder | string | - | string.max_len: 200<br /> |
| login_name_placeholder | string | - | string.max_len: 200<br /> |
### LogoutDoneScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| login_button_text | string | - | string.max_len: 200<br /> |
### MFAProvidersText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| choose_other | string | - | string.max_len: 500<br /> |
| otp | string | - | string.max_len: 200<br /> |
| u2f | string | - | string.max_len: 200<br /> |
### MessageCustomText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
| title | string | - | |
| pre_header | string | - | |
| subject | string | - | |
| greeting | string | - | |
| text | string | - | |
| button_text | string | - | |
| footer_text | string | - | |
### PasswordChangeDoneScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
### PasswordChangeScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| old_password_label | string | - | string.max_len: 200<br /> |
| new_password_label | string | - | string.max_len: 200<br /> |
| new_password_confirm_label | string | - | string.max_len: 200<br /> |
| cancel_button_text | string | - | string.max_len: 100<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
### PasswordResetDoneScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
### PasswordScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| password_label | string | - | string.max_len: 200<br /> |
| reset_link_text | string | - | string.max_len: 100<br /> |
| back_button_text | string | - | string.max_len: 100<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
| min_length | string | - | string.max_len: 100<br /> |
| has_uppercase | string | - | string.max_len: 100<br /> |
| has_lowercase | string | - | string.max_len: 100<br /> |
| has_number | string | - | string.max_len: 100<br /> |
| has_symbol | string | - | string.max_len: 100<br /> |
| confirmation | string | - | string.max_len: 100<br /> |
### PasswordlessPromptScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| description_init | string | - | string.max_len: 500<br /> |
| passwordless_button_text | string | - | string.max_len: 100<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
| skip_button_text | string | - | string.max_len: 100<br /> |
### PasswordlessRegistrationDoneScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
### PasswordlessRegistrationScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| token_name_label | string | - | string.max_len: 200<br /> |
| not_supported | string | - | string.max_len: 500<br /> |
| register_token_button_text | string | - | string.max_len: 100<br /> |
| error_retry | string | - | string.max_len: 500<br /> |
### PasswordlessScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| login_with_pw_button_text | string | - | string.max_len: 100<br /> |
| validate_token_button_text | string | - | string.max_len: 200<br /> |
| not_supported | string | - | string.max_len: 500<br /> |
| error_retry | string | - | string.max_len: 500<br /> |
### RegistrationOptionScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| user_name_button_text | string | - | string.max_len: 200<br /> |
| external_login_description | string | - | string.max_len: 500<br /> |
### RegistrationOrgScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| orgname_label | string | - | string.max_len: 200<br /> |
| firstname_label | string | - | string.max_len: 200<br /> |
| lastname_label | string | - | string.max_len: 200<br /> |
| username_label | string | - | string.max_len: 200<br /> |
| email_label | string | - | string.max_len: 200<br /> |
| password_label | string | - | string.max_len: 200<br /> |
| password_confirm_label | string | - | string.max_len: 200<br /> |
| tos_and_privacy_label | string | - | string.max_len: 200<br /> |
| tos_confirm | string | - | string.max_len: 200<br /> |
| tos_link_text | string | - | string.max_len: 200<br /> |
| privacy_link_text | string | - | string.max_len: 200<br /> |
| save_button_text | string | - | string.max_len: 200<br /> |
| tos_confirm_and | string | - | string.max_len: 200<br /> |
### RegistrationUserScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| description_org_register | string | - | string.max_len: 500<br /> |
| firstname_label | string | - | string.max_len: 200<br /> |
| lastname_label | string | - | string.max_len: 200<br /> |
| email_label | string | - | string.max_len: 200<br /> |
| username_label | string | - | string.max_len: 200<br /> |
| language_label | string | - | string.max_len: 200<br /> |
| gender_label | string | - | string.max_len: 200<br /> |
| password_label | string | - | string.max_len: 200<br /> |
| password_confirm_label | string | - | string.max_len: 200<br /> |
| tos_and_privacy_label | string | - | string.max_len: 200<br /> |
| tos_confirm | string | - | string.max_len: 200<br /> |
| tos_link_text | string | - | string.max_len: 200<br /> |
| privacy_link_text | string | - | string.max_len: 200<br /> |
| next_button_text | string | - | string.max_len: 200<br /> |
| back_button_text | string | - | string.max_len: 200<br /> |
| tos_confirm_and | string | - | string.max_len: 200<br /> |
### SelectAccountScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| title_linking_process | string | - | string.max_len: 200<br /> |
| description_linking_process | string | - | string.max_len: 500<br /> |
| other_user | string | - | string.max_len: 500<br /> |
| session_state_active | string | - | string.max_len: 100<br /> |
| session_state_inactive | string | - | string.max_len: 100<br /> |
| user_must_be_member_of_org | string | - | string.max_len: 500<br /> |
### SuccessLoginScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| auto_redirect_description | string | Text to describe that auto redirect should happen after successful login | string.max_len: 500<br /> |
| redirected_description | string | Text to describe that the window can be closed after redirect | string.max_len: 100<br /> |
| next_button_text | string | - | string.max_len: 200<br /> |
### UsernameChangeDoneScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
### UsernameChangeScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| username_label | string | - | string.max_len: 200<br /> |
| cancel_button_text | string | - | string.max_len: 100<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
### VerifyMFAOTPScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| code_label | string | - | string.max_len: 200<br /> |
| next_button_text | string | - | string.max_len: 100<br /> |
### VerifyMFAU2FScreenText
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| title | string | - | string.max_len: 200<br /> |
| description | string | - | string.max_len: 500<br /> |
| validate_token_text | string | - | string.max_len: 500<br /> |
| not_supported | string | - | string.max_len: 500<br /> |
| error_retry | string | - | string.max_len: 500<br /> |

View File

@ -0,0 +1,29 @@
package auth
import (
"github.com/caos/zitadel/internal/api/grpc/metadata"
"github.com/caos/zitadel/internal/api/grpc/object"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/pkg/grpc/auth"
)
func BulkSetMetadataToDomain(req *auth.BulkSetMyMetadataRequest) []*domain.Metadata {
metadata := make([]*domain.Metadata, len(req.Metadata))
for i, data := range req.Metadata {
metadata[i] = &domain.Metadata{
Key: data.Key,
Value: data.Value,
}
}
return metadata
}
func ListUserMetadataToDomain(req *auth.ListMyMetadataRequest) *domain.MetadataSearchRequest {
offset, limit, asc := object.ListQueryToModel(req.Query)
return &domain.MetadataSearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
Queries: metadata.MetadataQueriesToModel(req.Queries),
}
}

View File

@ -6,9 +6,12 @@ import (
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/api/grpc/change"
"github.com/caos/zitadel/internal/api/grpc/metadata"
"github.com/caos/zitadel/internal/api/grpc/object"
obj_grpc "github.com/caos/zitadel/internal/api/grpc/object"
"github.com/caos/zitadel/internal/api/grpc/org"
user_grpc "github.com/caos/zitadel/internal/api/grpc/user"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore/v1/models"
grant_model "github.com/caos/zitadel/internal/usergrant/model"
auth_pb "github.com/caos/zitadel/pkg/grpc/auth"
@ -37,6 +40,79 @@ func (s *Server) ListMyUserChanges(ctx context.Context, req *auth_pb.ListMyUserC
}, nil
}
func (s *Server) ListMyMetadata(ctx context.Context, req *auth_pb.ListMyMetadataRequest) (*auth_pb.ListMyMetadataResponse, error) {
res, err := s.repo.SearchMyMetadata(ctx, ListUserMetadataToDomain(req))
if err != nil {
return nil, err
}
return &auth_pb.ListMyMetadataResponse{
Result: metadata.MetadataListToPb(res.Result),
Details: obj_grpc.ToListDetails(
res.TotalResult,
res.Sequence,
res.Timestamp,
),
}, nil
}
func (s *Server) GetMyMetadata(ctx context.Context, req *auth_pb.GetMyMetadataRequest) (*auth_pb.GetMyMetadataResponse, error) {
data, err := s.repo.GetMyMetadataByKey(ctx, req.Key)
if err != nil {
return nil, err
}
return &auth_pb.GetMyMetadataResponse{
Metadata: metadata.DomainMetadataToPb(data),
}, nil
}
func (s *Server) SetMyMetadata(ctx context.Context, req *auth_pb.SetMyMetadataRequest) (*auth_pb.SetMyMetadataResponse, error) {
ctxData := authz.GetCtxData(ctx)
result, err := s.command.SetUserMetadata(ctx, &domain.Metadata{Key: req.Key, Value: req.Value}, ctxData.UserID, ctxData.ResourceOwner)
if err != nil {
return nil, err
}
return &auth_pb.SetMyMetadataResponse{
Details: obj_grpc.AddToDetailsPb(
result.Sequence,
result.ChangeDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) BulkSetMyMetadata(ctx context.Context, req *auth_pb.BulkSetMyMetadataRequest) (*auth_pb.BulkSetMyMetadataResponse, error) {
ctxData := authz.GetCtxData(ctx)
result, err := s.command.BulkSetUserMetadata(ctx, ctxData.UserID, ctxData.ResourceOwner, BulkSetMetadataToDomain(req)...)
if err != nil {
return nil, err
}
return &auth_pb.BulkSetMyMetadataResponse{
Details: obj_grpc.DomainToChangeDetailsPb(result),
}, nil
}
func (s *Server) RemoveMyMetadata(ctx context.Context, req *auth_pb.RemoveMyMetadataRequest) (*auth_pb.RemoveMyMetadataResponse, error) {
ctxData := authz.GetCtxData(ctx)
result, err := s.command.RemoveUserMetadata(ctx, req.Key, ctxData.UserID, ctxData.ResourceOwner)
if err != nil {
return nil, err
}
return &auth_pb.RemoveMyMetadataResponse{
Details: obj_grpc.DomainToChangeDetailsPb(result),
}, nil
}
func (s *Server) BulkRemoveMyMetadata(ctx context.Context, req *auth_pb.BulkRemoveMyMetadataRequest) (*auth_pb.BulkRemoveMyMetadataResponse, error) {
ctxData := authz.GetCtxData(ctx)
result, err := s.command.BulkRemoveUserMetadata(ctx, ctxData.UserID, ctxData.ResourceOwner, req.Keys...)
if err != nil {
return nil, err
}
return &auth_pb.BulkRemoveMyMetadataResponse{
Details: obj_grpc.DomainToChangeDetailsPb(result),
}, nil
}
func (s *Server) ListMyUserSessions(ctx context.Context, req *auth_pb.ListMyUserSessionsRequest) (*auth_pb.ListMyUserSessionsResponse, error) {
userSessions, err := s.repo.GetMyUserSessions(ctx)
if err != nil {

View File

@ -9,10 +9,12 @@ import (
"github.com/caos/zitadel/internal/api/grpc/authn"
change_grpc "github.com/caos/zitadel/internal/api/grpc/change"
idp_grpc "github.com/caos/zitadel/internal/api/grpc/idp"
"github.com/caos/zitadel/internal/api/grpc/metadata"
"github.com/caos/zitadel/internal/api/grpc/object"
obj_grpc "github.com/caos/zitadel/internal/api/grpc/object"
"github.com/caos/zitadel/internal/api/grpc/user"
user_grpc "github.com/caos/zitadel/internal/api/grpc/user"
"github.com/caos/zitadel/internal/domain"
grant_model "github.com/caos/zitadel/internal/usergrant/model"
mgmt_pb "github.com/caos/zitadel/pkg/grpc/management"
)
@ -78,6 +80,79 @@ func (s *Server) IsUserUnique(ctx context.Context, req *mgmt_pb.IsUserUniqueRequ
}, nil
}
func (s *Server) ListUserMetadata(ctx context.Context, req *mgmt_pb.ListUserMetadataRequest) (*mgmt_pb.ListUserMetadataResponse, error) {
res, err := s.user.SearchMetadata(ctx, req.Id, authz.GetCtxData(ctx).OrgID, ListUserMetadataToDomain(req))
if err != nil {
return nil, err
}
return &mgmt_pb.ListUserMetadataResponse{
Result: metadata.MetadataListToPb(res.Result),
Details: obj_grpc.ToListDetails(
res.TotalResult,
res.Sequence,
res.Timestamp,
),
}, nil
}
func (s *Server) GetUserMetadata(ctx context.Context, req *mgmt_pb.GetUserMetadataRequest) (*mgmt_pb.GetUserMetadataResponse, error) {
data, err := s.user.GetMetadataByKey(ctx, req.Id, authz.GetCtxData(ctx).OrgID, req.Key)
if err != nil {
return nil, err
}
return &mgmt_pb.GetUserMetadataResponse{
Metadata: metadata.DomainMetadataToPb(data),
}, nil
}
func (s *Server) SetUserMetadata(ctx context.Context, req *mgmt_pb.SetUserMetadataRequest) (*mgmt_pb.SetUserMetadataResponse, error) {
ctxData := authz.GetCtxData(ctx)
result, err := s.command.SetUserMetadata(ctx, &domain.Metadata{Key: req.Key, Value: req.Value}, req.Id, ctxData.ResourceOwner)
if err != nil {
return nil, err
}
return &mgmt_pb.SetUserMetadataResponse{
Details: obj_grpc.AddToDetailsPb(
result.Sequence,
result.ChangeDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) BulkSetUserMetadata(ctx context.Context, req *mgmt_pb.BulkSetUserMetadataRequest) (*mgmt_pb.BulkSetUserMetadataResponse, error) {
ctxData := authz.GetCtxData(ctx)
result, err := s.command.BulkSetUserMetadata(ctx, req.Id, ctxData.ResourceOwner, BulkSetMetadataToDomain(req)...)
if err != nil {
return nil, err
}
return &mgmt_pb.BulkSetUserMetadataResponse{
Details: obj_grpc.DomainToChangeDetailsPb(result),
}, nil
}
func (s *Server) RemoveUserMetadata(ctx context.Context, req *mgmt_pb.RemoveUserMetadataRequest) (*mgmt_pb.RemoveUserMetadataResponse, error) {
ctxData := authz.GetCtxData(ctx)
result, err := s.command.RemoveUserMetadata(ctx, req.Key, req.Id, ctxData.ResourceOwner)
if err != nil {
return nil, err
}
return &mgmt_pb.RemoveUserMetadataResponse{
Details: obj_grpc.DomainToChangeDetailsPb(result),
}, nil
}
func (s *Server) BulkRemoveUserMetadata(ctx context.Context, req *mgmt_pb.BulkRemoveUserMetadataRequest) (*mgmt_pb.BulkRemoveUserMetadataResponse, error) {
ctxData := authz.GetCtxData(ctx)
result, err := s.command.BulkRemoveUserMetadata(ctx, req.Id, ctxData.ResourceOwner, req.Keys...)
if err != nil {
return nil, err
}
return &mgmt_pb.BulkRemoveUserMetadataResponse{
Details: obj_grpc.DomainToChangeDetailsPb(result),
}, nil
}
func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) {
human, err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, AddHumanUserRequestToDomain(req))
if err != nil {

View File

@ -10,6 +10,7 @@ import (
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/api/grpc/authn"
"github.com/caos/zitadel/internal/api/grpc/metadata"
"github.com/caos/zitadel/internal/api/grpc/object"
user_grpc "github.com/caos/zitadel/internal/api/grpc/user"
"github.com/caos/zitadel/internal/domain"
@ -38,6 +39,27 @@ func ListUsersRequestToModel(ctx context.Context, req *mgmt_pb.ListUsersRequest)
}
}
func BulkSetMetadataToDomain(req *mgmt_pb.BulkSetUserMetadataRequest) []*domain.Metadata {
metadata := make([]*domain.Metadata, len(req.Metadata))
for i, data := range req.Metadata {
metadata[i] = &domain.Metadata{
Key: data.Key,
Value: data.Value,
}
}
return metadata
}
func ListUserMetadataToDomain(req *mgmt_pb.ListUserMetadataRequest) *domain.MetadataSearchRequest {
offset, limit, asc := object.ListQueryToModel(req.Query)
return &domain.MetadataSearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
Queries: metadata.MetadataQueriesToModel(req.Queries),
}
}
func AddHumanUserRequestToDomain(req *mgmt_pb.AddHumanUserRequest) *domain.Human {
h := &domain.Human{
Username: req.UserName,

View File

@ -0,0 +1,53 @@
package metadata
import (
"github.com/caos/zitadel/internal/api/grpc/object"
"github.com/caos/zitadel/internal/domain"
meta_pb "github.com/caos/zitadel/pkg/grpc/metadata"
)
func MetadataListToPb(dataList []*domain.Metadata) []*meta_pb.Metadata {
mds := make([]*meta_pb.Metadata, len(dataList))
for i, data := range dataList {
mds[i] = DomainMetadataToPb(data)
}
return mds
}
func DomainMetadataToPb(data *domain.Metadata) *meta_pb.Metadata {
return &meta_pb.Metadata{
Key: data.Key,
Value: data.Value,
Details: object.ToViewDetailsPb(
data.Sequence,
data.CreationDate,
data.ChangeDate,
data.ResourceOwner,
),
}
}
func MetadataQueriesToModel(queries []*meta_pb.MetadataQuery) []*domain.MetadataSearchQuery {
q := make([]*domain.MetadataSearchQuery, len(queries))
for i, query := range queries {
q[i] = MetadataQueryToModel(query)
}
return q
}
func MetadataQueryToModel(query *meta_pb.MetadataQuery) *domain.MetadataSearchQuery {
switch q := query.Query.(type) {
case *meta_pb.MetadataQuery_KeyQuery:
return MetadataKeyQueryToModel(q.KeyQuery)
default:
return nil
}
}
func MetadataKeyQueryToModel(q *meta_pb.MetadataKeyQuery) *domain.MetadataSearchQuery {
return &domain.MetadataSearchQuery{
Key: domain.MetadataSearchKeyKey,
Method: object.TextMethodToModel(q.Method),
Value: q.Key,
}
}

View File

@ -14,6 +14,7 @@ import (
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1"
"github.com/caos/zitadel/internal/eventstore/v1/models"
iam_model "github.com/caos/zitadel/internal/iam/repository/view/model"
key_model "github.com/caos/zitadel/internal/key/model"
key_view_model "github.com/caos/zitadel/internal/key/repository/view/model"
"github.com/caos/zitadel/internal/telemetry/tracing"
@ -296,3 +297,39 @@ func (r *UserRepo) getUserEvents(ctx context.Context, userID string, sequence ui
}
return r.Eventstore.FilterEvents(ctx, query)
}
func (repo *UserRepo) GetMyMetadataByKey(ctx context.Context, key string) (*domain.Metadata, error) {
ctxData := authz.GetCtxData(ctx)
data, err := repo.View.MetadataByKeyAndResourceOwner(ctxData.UserID, ctxData.ResourceOwner, key)
if err != nil {
return nil, err
}
return iam_model.MetadataViewToDomain(data), nil
}
func (repo *UserRepo) SearchMyMetadata(ctx context.Context, req *domain.MetadataSearchRequest) (*domain.MetadataSearchResponse, error) {
ctxData := authz.GetCtxData(ctx)
err := req.EnsureLimit(repo.SearchLimit)
if err != nil {
return nil, err
}
sequence, sequenceErr := repo.View.GetLatestUserSequence()
logging.Log("EVENT-N9fsd").OnError(sequenceErr).Warn("could not read latest user sequence")
req.AppendAggregateIDQuery(ctxData.UserID)
req.AppendResourceOwnerQuery(ctxData.ResourceOwner)
metadata, count, err := repo.View.SearchMetadata(req)
if err != nil {
return nil, err
}
result := &domain.MetadataSearchResponse{
Offset: req.Offset,
Limit: req.Limit,
TotalResult: count,
Result: iam_model.MetadataViewsToDomain(metadata),
}
if sequenceErr == nil {
result.Sequence = sequence.CurrentSequence
result.Timestamp = sequence.LastSuccessfulSpoolerRun
}
return result, nil
}

View File

@ -72,6 +72,7 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es
newRefreshToken(handler{view, bulkLimit, configs.cycleDuration("RefreshToken"), errorCount, es}),
newPrivacyPolicy(handler{view, bulkLimit, configs.cycleDuration("PrivacyPolicy"), errorCount, es}),
newCustomText(handler{view, bulkLimit, configs.cycleDuration("CustomTexts"), errorCount, es}),
newMetadata(handler{view, bulkLimit, configs.cycleDuration("Metadata"), errorCount, es}),
}
}

View File

@ -0,0 +1,124 @@
package handler
import (
"github.com/caos/logging"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1"
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/eventstore/v1/query"
"github.com/caos/zitadel/internal/eventstore/v1/spooler"
iam_model "github.com/caos/zitadel/internal/iam/repository/view/model"
usr_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
)
type Metadata struct {
handler
subscription *v1.Subscription
}
func newMetadata(handler handler) *Metadata {
h := &Metadata{
handler: handler,
}
h.subscribe()
return h
}
func (m *Metadata) subscribe() {
m.subscription = m.es.Subscribe(m.AggregateTypes()...)
go func() {
for event := range m.subscription.Events {
query.ReduceEvent(m, event)
}
}()
}
const (
metadataTable = "auth.metadata"
)
func (m *Metadata) ViewModel() string {
return metadataTable
}
func (m *Metadata) Subscription() *v1.Subscription {
return m.subscription
}
func (_ *Metadata) AggregateTypes() []es_models.AggregateType {
return []es_models.AggregateType{usr_model.UserAggregate}
}
func (p *Metadata) CurrentSequence() (uint64, error) {
sequence, err := p.view.GetLatestMetadataSequence()
if err != nil {
return 0, err
}
return sequence.CurrentSequence, nil
}
func (m *Metadata) EventQuery() (*es_models.SearchQuery, error) {
sequence, err := m.view.GetLatestMetadataSequence()
if err != nil {
return nil, err
}
return es_models.NewSearchQuery().
AggregateTypeFilter(m.AggregateTypes()...).
LatestSequenceFilter(sequence.CurrentSequence), nil
}
func (m *Metadata) Reduce(event *es_models.Event) (err error) {
switch event.AggregateType {
case usr_model.UserAggregate:
err = m.processMetadata(event)
}
return err
}
func (m *Metadata) processMetadata(event *es_models.Event) (err error) {
metadata := new(iam_model.MetadataView)
switch event.Type {
case usr_model.UserMetadataSet:
err = metadata.SetData(event)
if err != nil {
return err
}
metadata, err = m.view.MetadataByKey(event.AggregateID, metadata.Key)
if err != nil && !caos_errs.IsNotFound(err) {
return err
}
if caos_errs.IsNotFound(err) {
err = nil
metadata = new(iam_model.MetadataView)
metadata.CreationDate = event.CreationDate
}
err = metadata.AppendEvent(event)
case usr_model.UserMetadataRemoved:
data := new(iam_model.MetadataView)
err = data.SetData(event)
if err != nil {
return err
}
return m.view.DeleteMetadata(event.AggregateID, data.Key, event)
case usr_model.UserRemoved:
return m.view.DeleteMetadataByAggregateID(event.AggregateID, event)
default:
return m.view.ProcessedMetadataSequence(event)
}
if err != nil {
return err
}
return m.view.PutMetadata(metadata, event)
}
func (m *Metadata) OnError(event *es_models.Event, err error) error {
logging.LogWithFields("SPOOL-miJJs", "id", event.AggregateID).WithError(err).Warn("something went wrong in custom text handler")
return spooler.HandleError(event, err, m.view.GetLatestMetadataFailedEvent, m.view.ProcessedMetadataFailedEvent, m.view.ProcessedMetadataSequence, m.errorCountUntilSkip)
}
func (m *Metadata) OnSuccess() error {
return spooler.HandleSuccess(m.view.UpdateMetadataSpoolerRunTimestamp)
}

View File

@ -0,0 +1,73 @@
package view
import (
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/iam/repository/view"
"github.com/caos/zitadel/internal/iam/repository/view/model"
global_view "github.com/caos/zitadel/internal/view/repository"
)
const (
metadataTable = "auth.metadata"
)
func (v *View) MetadataByKey(aggregateID, key string) (*model.MetadataView, error) {
return view.MetadataByKey(v.Db, metadataTable, aggregateID, key)
}
func (v *View) MetadataListByAggregateID(aggregateID string) ([]*model.MetadataView, error) {
return view.GetMetadataList(v.Db, metadataTable, aggregateID)
}
func (v *View) MetadataByKeyAndResourceOwner(aggregateID, resourceOwner, key string) (*model.MetadataView, error) {
return view.MetadataByKeyAndResourceOwner(v.Db, metadataTable, aggregateID, resourceOwner, key)
}
func (v *View) SearchMetadata(request *domain.MetadataSearchRequest) ([]*model.MetadataView, uint64, error) {
return view.SearchMetadata(v.Db, metadataTable, request)
}
func (v *View) PutMetadata(template *model.MetadataView, event *models.Event) error {
err := view.PutMetadata(v.Db, metadataTable, template)
if err != nil {
return err
}
return v.ProcessedMetadataSequence(event)
}
func (v *View) DeleteMetadata(aggregateID, key string, event *models.Event) error {
err := view.DeleteMetadata(v.Db, metadataTable, aggregateID, key)
if err != nil && !errors.IsNotFound(err) {
return err
}
return v.ProcessedMetadataSequence(event)
}
func (v *View) DeleteMetadataByAggregateID(aggregateID string, event *models.Event) error {
err := view.DeleteMetadataByAggregateID(v.Db, metadataTable, aggregateID)
if err != nil && !errors.IsNotFound(err) {
return err
}
return v.ProcessedMetadataSequence(event)
}
func (v *View) GetLatestMetadataSequence() (*global_view.CurrentSequence, error) {
return v.latestSequence(metadataTable)
}
func (v *View) ProcessedMetadataSequence(event *models.Event) error {
return v.saveCurrentSequence(metadataTable, event)
}
func (v *View) UpdateMetadataSpoolerRunTimestamp() error {
return v.updateSpoolerRunSequence(metadataTable)
}
func (v *View) GetLatestMetadataFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
return v.latestFailedEvent(metadataTable, sequence)
}
func (v *View) ProcessedMetadataFailedEvent(failedEvent *global_view.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}

View File

@ -4,6 +4,7 @@ import (
"context"
"time"
"github.com/caos/zitadel/internal/domain"
key_model "github.com/caos/zitadel/internal/key/model"
"github.com/caos/zitadel/internal/user/model"
@ -42,4 +43,7 @@ type myUserRepo interface {
MyUserChanges(ctx context.Context, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.UserChanges, error)
SearchMyUserMemberships(ctx context.Context, request *model.UserMembershipSearchRequest) (*model.UserMembershipSearchResponse, error)
GetMyMetadataByKey(ctx context.Context, key string) (*domain.Metadata, error)
SearchMyMetadata(ctx context.Context, req *domain.MetadataSearchRequest) (*domain.MetadataSearchResponse, error)
}

View File

@ -0,0 +1,49 @@
package command
import (
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/metadata"
)
type MetadataWriteModel struct {
eventstore.WriteModel
Key string
Value []byte
State domain.MetadataState
}
func (wm *MetadataWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *metadata.SetEvent:
if wm.Key != e.Key {
continue
}
wm.Value = e.Value
wm.State = domain.MetadataStateActive
case *metadata.RemovedEvent:
wm.State = domain.MetadataStateRemoved
}
}
return wm.WriteModel.Reduce()
}
type MetadataListWriteModel struct {
eventstore.WriteModel
metadataList map[string][]byte
}
func (wm *MetadataListWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *metadata.SetEvent:
wm.metadataList[e.Key] = e.Value
case *metadata.RemovedEvent:
delete(wm.metadataList, e.Key)
}
}
return wm.WriteModel.Reduce()
}

View File

@ -153,3 +153,12 @@ func writeModelToPasswordlessInitCode(initCodeModel *HumanPasswordlessInitCodeWr
State: initCodeModel.State,
}
}
func writeModelToUserMetadata(wm *UserMetadataWriteModel) *domain.Metadata {
return &domain.Metadata{
ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
Key: wm.Key,
Value: wm.Value,
State: wm.State,
}
}

View File

@ -0,0 +1,177 @@
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/user"
)
func (c *Commands) SetUserMetadata(ctx context.Context, metadata *domain.Metadata, userID, resourceOwner string) (_ *domain.Metadata, err error) {
err = c.checkUserExists(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
setMetadata := NewUserMetadataWriteModel(userID, resourceOwner, metadata.Key)
userAgg := UserAggregateFromWriteModel(&setMetadata.WriteModel)
event, err := c.setUserMetadata(ctx, userAgg, metadata)
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.PushEvents(ctx, event)
if err != nil {
return nil, err
}
err = AppendAndReduce(setMetadata, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToUserMetadata(setMetadata), nil
}
func (c *Commands) BulkSetUserMetadata(ctx context.Context, userID, resourceOwner string, metadatas ...*domain.Metadata) (_ *domain.ObjectDetails, err error) {
if len(metadatas) == 0 {
return nil, caos_errs.ThrowPreconditionFailed(nil, "META-9mm2d", "Errors.Metadata.NoData")
}
err = c.checkUserExists(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
events := make([]eventstore.EventPusher, len(metadatas))
setMetadata := NewUserMetadataListWriteModel(userID, resourceOwner)
userAgg := UserAggregateFromWriteModel(&setMetadata.WriteModel)
for i, data := range metadatas {
event, err := c.setUserMetadata(ctx, userAgg, data)
if err != nil {
return nil, err
}
events[i] = event
}
pushedEvents, err := c.eventstore.PushEvents(ctx, events...)
if err != nil {
return nil, err
}
err = AppendAndReduce(setMetadata, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&setMetadata.WriteModel), nil
}
func (c *Commands) setUserMetadata(ctx context.Context, userAgg *eventstore.Aggregate, metadata *domain.Metadata) (pusher eventstore.EventPusher, err error) {
if !metadata.IsValid() {
return nil, caos_errs.ThrowInvalidArgument(nil, "META-2m00f", "Errors.Metadata.Invalid")
}
return user.NewMetadataSetEvent(
ctx,
userAgg,
metadata.Key,
metadata.Value,
), nil
}
func (c *Commands) RemoveUserMetadata(ctx context.Context, metadataKey, userID, resourceOwner string) (_ *domain.ObjectDetails, err error) {
if metadataKey == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "META-2n0fs", "Errors.Metadata.Invalid")
}
err = c.checkUserExists(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
removeMetadata, err := c.getUserMetadataModelByID(ctx, userID, resourceOwner, metadataKey)
if err != nil {
return nil, err
}
if !removeMetadata.State.Exists() {
return nil, caos_errs.ThrowNotFound(nil, "META-ncnw3", "Errors.Metadata.NotFound")
}
userAgg := UserAggregateFromWriteModel(&removeMetadata.WriteModel)
event, err := c.removeUserMetadata(ctx, userAgg, metadataKey)
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.PushEvents(ctx, event)
if err != nil {
return nil, err
}
err = AppendAndReduce(removeMetadata, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&removeMetadata.WriteModel), nil
}
func (c *Commands) BulkRemoveUserMetadata(ctx context.Context, userID, resourceOwner string, metadataKeys ...string) (_ *domain.ObjectDetails, err error) {
if len(metadataKeys) == 0 {
return nil, caos_errs.ThrowPreconditionFailed(nil, "META-9mm2d", "Errors.Metadata.NoData")
}
err = c.checkUserExists(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
events := make([]eventstore.EventPusher, len(metadataKeys))
removeMetadata, err := c.getUserMetadataListModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
userAgg := UserAggregateFromWriteModel(&removeMetadata.WriteModel)
for i, key := range metadataKeys {
if key == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-m29ds", "Errors.Metadata.Invalid")
}
if _, found := removeMetadata.metadataList[key]; !found {
return nil, caos_errs.ThrowNotFound(nil, "META-2nnds", "Errors.Metadata.KeyNotExisting")
}
event, err := c.removeUserMetadata(ctx, userAgg, key)
if err != nil {
return nil, err
}
events[i] = event
}
pushedEvents, err := c.eventstore.PushEvents(ctx, events...)
if err != nil {
return nil, err
}
err = AppendAndReduce(removeMetadata, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&removeMetadata.WriteModel), nil
}
func (c *Commands) removeUserMetadata(ctx context.Context, userAgg *eventstore.Aggregate, metadataKey string) (pusher eventstore.EventPusher, err error) {
pusher = user.NewMetadataRemovedEvent(
ctx,
userAgg,
metadataKey,
)
return pusher, nil
}
func (c *Commands) getUserMetadataModelByID(ctx context.Context, userID, resourceOwner, key string) (*UserMetadataWriteModel, error) {
userMetadataWriteModel := NewUserMetadataWriteModel(userID, resourceOwner, key)
err := c.eventstore.FilterToQueryReducer(ctx, userMetadataWriteModel)
if err != nil {
return nil, err
}
return userMetadataWriteModel, nil
}
func (c *Commands) getUserMetadataListModelByID(ctx context.Context, userID, resourceOwner string) (*UserMetadataListWriteModel, error) {
userMetadataWriteModel := NewUserMetadataListWriteModel(userID, resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, userMetadataWriteModel)
if err != nil {
return nil, err
}
return userMetadataWriteModel, nil
}

View File

@ -0,0 +1,92 @@
package command
import (
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/user"
)
type UserMetadataWriteModel struct {
MetadataWriteModel
}
func NewUserMetadataWriteModel(userID, resourceOwner, key string) *UserMetadataWriteModel {
return &UserMetadataWriteModel{
MetadataWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
Key: key,
},
}
}
func (wm *UserMetadataWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.MetadataSetEvent:
wm.MetadataWriteModel.AppendEvents(&e.SetEvent)
case *user.MetadataRemovedEvent:
wm.MetadataWriteModel.AppendEvents(&e.RemovedEvent)
}
}
}
func (wm *UserMetadataWriteModel) Reduce() error {
return wm.MetadataWriteModel.Reduce()
}
func (wm *UserMetadataWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateIDs(wm.MetadataWriteModel.AggregateID).
AggregateTypes(user.AggregateType).
EventTypes(
user.MetadataSetType,
user.MetadataRemovedType).
Builder()
}
type UserMetadataListWriteModel struct {
MetadataListWriteModel
}
func NewUserMetadataListWriteModel(userID, resourceOwner string) *UserMetadataListWriteModel {
return &UserMetadataListWriteModel{
MetadataListWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
metadataList: make(map[string][]byte),
},
}
}
func (wm *UserMetadataListWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.MetadataSetEvent:
wm.MetadataListWriteModel.AppendEvents(&e.SetEvent)
case *user.MetadataRemovedEvent:
wm.MetadataListWriteModel.AppendEvents(&e.RemovedEvent)
}
}
}
func (wm *UserMetadataListWriteModel) Reduce() error {
return wm.MetadataListWriteModel.Reduce()
}
func (wm *UserMetadataListWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateIDs(wm.MetadataListWriteModel.AggregateID).
AggregateTypes(user.AggregateType).
EventTypes(
user.MetadataSetType,
user.MetadataRemovedType).
Builder()
}

View File

@ -0,0 +1,739 @@
package command
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
"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/eventstore/repository"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/repository/user"
)
func TestCommandSide_SetMetadata(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type (
args struct {
ctx context.Context
orgID string
userID string
metadata *domain.Metadata
}
)
type res struct {
want *domain.Metadata
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "user not existing, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadata: &domain.Metadata{
Key: "key",
Value: []byte("value"),
},
},
res: res{
err: caos_errs.IsPreconditionFailed,
},
},
{
name: "invalid metadata, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadata: &domain.Metadata{
Key: "key",
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
name: "add metadata, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
[]byte("value"),
),
),
},
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadata: &domain.Metadata{
Key: "key",
Value: []byte("value"),
},
},
res: res{
want: &domain.Metadata{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
},
Key: "key",
Value: []byte("value"),
State: domain.MetadataStateActive,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.SetUserMetadata(tt.args.ctx, tt.args.metadata, tt.args.userID, tt.args.orgID)
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.want, got)
}
})
}
}
func TestCommandSide_BulkSetMetadata(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type (
args struct {
ctx context.Context
orgID string
userID string
metadataList []*domain.Metadata
}
)
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "empty meta data list, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
},
res: res{
err: caos_errs.IsPreconditionFailed,
},
},
{
name: "user not existing, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadataList: []*domain.Metadata{
{Key: "key", Value: []byte("value")},
{Key: "key1", Value: []byte("value1")},
},
},
res: res{
err: caos_errs.IsPreconditionFailed,
},
},
{
name: "invalid metadata, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadataList: []*domain.Metadata{
{Key: "key"},
{Key: "key1"},
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
name: "add metadata, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
[]byte("value"),
),
),
eventFromEventPusher(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key1",
[]byte("value1"),
),
),
},
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadataList: []*domain.Metadata{
{Key: "key", Value: []byte("value")},
{Key: "key1", Value: []byte("value1")},
},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.BulkSetUserMetadata(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.metadataList...)
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.want, got)
}
})
}
}
func TestCommandSide_UserRemoveMetadata(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type (
args struct {
ctx context.Context
orgID string
userID string
metadataKey string
}
)
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "user not existing, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadataKey: "key",
},
res: res{
err: caos_errs.IsPreconditionFailed,
},
},
{
name: "invalid metadata, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadataKey: "",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
name: "meta data not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
),
expectFilter(),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadataKey: "key",
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "remove metadata, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
),
expectFilter(
eventFromEventPusher(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
[]byte("value"),
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
user.NewMetadataRemovedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
),
),
},
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadataKey: "key",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.RemoveUserMetadata(tt.args.ctx, tt.args.metadataKey, tt.args.userID, tt.args.orgID)
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.want, got)
}
})
}
}
func TestCommandSide_BulkRemoveMetadata(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type (
args struct {
ctx context.Context
orgID string
userID string
metadataList []string
}
)
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "empty meta data list, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
},
res: res{
err: caos_errs.IsPreconditionFailed,
},
},
{
name: "user not existing, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadataList: []string{"key", "key1"},
},
res: res{
err: caos_errs.IsPreconditionFailed,
},
},
{
name: "remove metadata keys not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
),
expectFilter(
eventFromEventPusher(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
[]byte("value"),
),
),
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadataList: []string{"key", "key1"},
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "invalid metadata, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
),
expectFilter(
eventFromEventPusher(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
[]byte("value"),
),
),
eventFromEventPusher(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key1",
[]byte("value1"),
),
),
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadataList: []string{""},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
name: "remove metadata, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
),
expectFilter(
eventFromEventPusher(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
[]byte("value"),
),
),
eventFromEventPusher(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key1",
[]byte("value1"),
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
user.NewMetadataRemovedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
),
),
eventFromEventPusher(
user.NewMetadataRemovedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key1",
),
),
},
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadataList: []string{"key", "key1"},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.BulkRemoveUserMetadata(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.metadataList...)
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.want, got)
}
})
}
}

View File

@ -0,0 +1,83 @@
package domain
import (
"time"
caos_errors "github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
)
type Metadata struct {
es_models.ObjectRoot
State MetadataState
Key string
Value []byte
}
type MetadataState int32
const (
MetadataStateUnspecified MetadataState = iota
MetadataStateActive
MetadataStateRemoved
)
func (m *Metadata) IsValid() bool {
return m.Key != "" && len(m.Value) > 0
}
func (s MetadataState) Exists() bool {
return s != MetadataStateUnspecified && s != MetadataStateRemoved
}
type MetadataSearchRequest struct {
Offset uint64
Limit uint64
SortingColumn MetadataSearchKey
Asc bool
Queries []*MetadataSearchQuery
}
type MetadataSearchKey int32
const (
MetadataSearchKeyUnspecified MetadataSearchKey = iota
MetadataSearchKeyAggregateID
MetadataSearchKeyResourceOwner
MetadataSearchKeyKey
MetadataSearchKeyValue
)
type MetadataSearchQuery struct {
Key MetadataSearchKey
Method SearchMethod
Value interface{}
}
type MetadataSearchResponse struct {
Offset uint64
Limit uint64
TotalResult uint64
Result []*Metadata
Sequence uint64
Timestamp time.Time
}
func (r *MetadataSearchRequest) EnsureLimit(limit uint64) error {
if r.Limit > limit {
return caos_errors.ThrowInvalidArgument(nil, "SEARCH-0ds32", "Errors.Limit.ExceedsDefault")
}
if r.Limit == 0 {
r.Limit = limit
}
return nil
}
func (r *MetadataSearchRequest) AppendAggregateIDQuery(aggregateID string) {
r.Queries = append(r.Queries, &MetadataSearchQuery{Key: MetadataSearchKeyAggregateID, Method: SearchMethodEquals, Value: aggregateID})
}
func (r *MetadataSearchRequest) AppendResourceOwnerQuery(resourceOwner string) {
r.Queries = append(r.Queries, &MetadataSearchQuery{Key: MetadataSearchKeyResourceOwner, Method: SearchMethodEquals, Value: resourceOwner})
}

View File

@ -0,0 +1,80 @@
package view
import (
"github.com/jinzhu/gorm"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/iam/repository/view/model"
"github.com/caos/zitadel/internal/view/repository"
)
func GetMetadataList(db *gorm.DB, table string, aggregateID string) ([]*model.MetadataView, error) {
metadatas := make([]*model.MetadataView, 0)
queries := []*domain.MetadataSearchQuery{
{
Key: domain.MetadataSearchKeyAggregateID,
Value: aggregateID,
Method: domain.SearchMethodEquals,
},
}
query := repository.PrepareSearchQuery(table, model.MetadataSearchRequest{Queries: queries})
_, err := query(db, &metadatas)
if err != nil {
return nil, err
}
return metadatas, nil
}
func MetadataByKey(db *gorm.DB, table, aggregateID, key string) (*model.MetadataView, error) {
metadata := new(model.MetadataView)
aggregateIDQuery := &model.MetadataSearchQuery{Key: domain.MetadataSearchKeyAggregateID, Value: aggregateID, Method: domain.SearchMethodEquals}
keyQuery := &model.MetadataSearchQuery{Key: domain.MetadataSearchKeyKey, Value: key, Method: domain.SearchMethodEquals}
query := repository.PrepareGetByQuery(table, aggregateIDQuery, keyQuery)
err := query(db, metadata)
if caos_errs.IsNotFound(err) {
return nil, caos_errs.ThrowNotFound(nil, "VIEW-29kkd", "Errors.Metadata.NotExisting")
}
return metadata, err
}
func MetadataByKeyAndResourceOwner(db *gorm.DB, table, aggregateID, resourceOwner, key string) (*model.MetadataView, error) {
metadata := new(model.MetadataView)
aggregateIDQuery := &model.MetadataSearchQuery{Key: domain.MetadataSearchKeyAggregateID, Value: aggregateID, Method: domain.SearchMethodEquals}
resourceOwnerQuery := &model.MetadataSearchQuery{Key: domain.MetadataSearchKeyResourceOwner, Value: resourceOwner, Method: domain.SearchMethodEquals}
keyQuery := &model.MetadataSearchQuery{Key: domain.MetadataSearchKeyKey, Value: key, Method: domain.SearchMethodEquals}
query := repository.PrepareGetByQuery(table, aggregateIDQuery, resourceOwnerQuery, keyQuery)
err := query(db, metadata)
if caos_errs.IsNotFound(err) {
return nil, caos_errs.ThrowNotFound(nil, "VIEW-29kkd", "Errors.Metadata.NotExisting")
}
return metadata, err
}
func SearchMetadata(db *gorm.DB, table string, req *domain.MetadataSearchRequest) ([]*model.MetadataView, uint64, error) {
metadata := make([]*model.MetadataView, 0)
query := repository.PrepareSearchQuery(table, model.MetadataSearchRequest{Limit: req.Limit, Offset: req.Offset, Queries: req.Queries})
count, err := query(db, &metadata)
if err != nil {
return nil, 0, err
}
return metadata, count, nil
}
func PutMetadata(db *gorm.DB, table string, customText *model.MetadataView) error {
save := repository.PrepareSave(table)
return save(db, customText)
}
func DeleteMetadata(db *gorm.DB, table, aggregateID, key string) error {
aggregateIDQuery := repository.Key{Key: model.MetadataSearchKey(domain.MetadataSearchKeyAggregateID), Value: aggregateID}
keyQuery := repository.Key{Key: model.MetadataSearchKey(domain.MetadataSearchKeyKey), Value: key}
delete := repository.PrepareDeleteByKeys(table, aggregateIDQuery, keyQuery)
return delete(db)
}
func DeleteMetadataByAggregateID(db *gorm.DB, table, aggregateID string) error {
aggregateIDQuery := repository.Key{Key: model.MetadataSearchKey(domain.MetadataSearchKeyAggregateID), Value: aggregateID}
delete := repository.PrepareDeleteByKeys(table, aggregateIDQuery)
return delete(db)
}

View File

@ -30,10 +30,10 @@ type CustomTextView struct {
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
Template string `json:"Template" gorm:"column:template;primary_key"`
Language string `json:"Language" gorm:"column:language;primary_key"`
Key string `json:"Key" gorm:"column:key;primary_key"`
Text string `json:"Text" gorm:"column:text"`
Template string `json:"template" gorm:"column:template;primary_key"`
Language string `json:"language" gorm:"column:language;primary_key"`
Key string `json:"key" gorm:"column:key;primary_key"`
Text string `json:"text" gorm:"column:text"`
Sequence uint64 `json:"-" gorm:"column:sequence"`
}

View File

@ -0,0 +1,79 @@
package model
import (
"encoding/json"
"time"
"github.com/caos/zitadel/internal/domain"
usr_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
"github.com/caos/logging"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1/models"
)
const (
MetadataKeyAggregateID = "aggregate_id"
MetadataKeyResourceOwner = "resource_owner"
MetadataKeyKey = "key"
MetadataKeyValue = "value"
)
type MetadataView struct {
AggregateID string `json:"-" gorm:"column:aggregate_id;primary_key"`
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
Key string `json:"key" gorm:"column:key;primary_key"`
Value []byte `json:"value" gorm:"column:value"`
Sequence uint64 `json:"-" gorm:"column:sequence"`
}
func MetadataViewsToDomain(texts []*MetadataView) []*domain.Metadata {
result := make([]*domain.Metadata, len(texts))
for i, text := range texts {
result[i] = MetadataViewToDomain(text)
}
return result
}
func MetadataViewToDomain(data *MetadataView) *domain.Metadata {
return &domain.Metadata{
ObjectRoot: models.ObjectRoot{
AggregateID: data.AggregateID,
Sequence: data.Sequence,
CreationDate: data.CreationDate,
ChangeDate: data.ChangeDate,
},
Key: data.Key,
Value: data.Value,
}
}
func (md *MetadataView) AppendEvent(event *models.Event) (err error) {
md.Sequence = event.Sequence
switch event.Type {
case usr_model.UserMetadataSet:
md.setRootData(event)
err = md.SetData(event)
}
return err
}
func (md *MetadataView) setRootData(event *models.Event) {
md.AggregateID = event.AggregateID
md.ResourceOwner = event.ResourceOwner
md.ChangeDate = event.CreationDate
md.Sequence = event.Sequence
}
func (md *MetadataView) SetData(event *models.Event) error {
if err := json.Unmarshal(event.Data, md); err != nil {
logging.Log("MODEL-3n9fs").WithError(err).Error("could not unmarshal event data")
return caos_errs.ThrowInternal(err, "MODEL-5CVaR", "Could not unmarshal data")
}
return nil
}

View File

@ -0,0 +1,64 @@
package model
import (
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/view/repository"
)
type MetadataSearchRequest domain.MetadataSearchRequest
type MetadataSearchQuery domain.MetadataSearchQuery
type MetadataSearchKey domain.MetadataSearchKey
func (req MetadataSearchRequest) GetLimit() uint64 {
return req.Limit
}
func (req MetadataSearchRequest) GetOffset() uint64 {
return req.Offset
}
func (req MetadataSearchRequest) GetSortingColumn() repository.ColumnKey {
if req.SortingColumn == domain.MetadataSearchKeyUnspecified {
return nil
}
return MetadataSearchKey(req.SortingColumn)
}
func (req MetadataSearchRequest) GetAsc() bool {
return req.Asc
}
func (req MetadataSearchRequest) GetQueries() []repository.SearchQuery {
result := make([]repository.SearchQuery, len(req.Queries))
for i, q := range req.Queries {
result[i] = MetadataSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method}
}
return result
}
func (req MetadataSearchQuery) GetKey() repository.ColumnKey {
return MetadataSearchKey(req.Key)
}
func (req MetadataSearchQuery) GetMethod() domain.SearchMethod {
return req.Method
}
func (req MetadataSearchQuery) GetValue() interface{} {
return req.Value
}
func (key MetadataSearchKey) ToColumnName() string {
switch domain.MetadataSearchKey(key) {
case domain.MetadataSearchKeyAggregateID:
return MetadataKeyAggregateID
case domain.MetadataSearchKeyResourceOwner:
return MetadataKeyResourceOwner
case domain.MetadataSearchKeyKey:
return MetadataKeyKey
case domain.MetadataSearchKeyValue:
return MetadataKeyValue
default:
return ""
}
}

View File

@ -9,6 +9,7 @@ import (
"github.com/caos/zitadel/internal/domain"
v1 "github.com/caos/zitadel/internal/eventstore/v1"
"github.com/caos/zitadel/internal/eventstore/v1/models"
iam_model "github.com/caos/zitadel/internal/iam/repository/view/model"
usr_view "github.com/caos/zitadel/internal/user/repository/view"
"github.com/caos/logging"
@ -128,6 +129,40 @@ func (repo *UserRepo) IsUserUnique(ctx context.Context, userName, email string)
return repo.View.IsUserUnique(userName, email)
}
func (repo *UserRepo) GetMetadataByKey(ctx context.Context, userID, resourceOwner, key string) (*domain.Metadata, error) {
data, err := repo.View.MetadataByKeyAndResourceOwner(userID, resourceOwner, key)
if err != nil {
return nil, err
}
return iam_model.MetadataViewToDomain(data), nil
}
func (repo *UserRepo) SearchMetadata(ctx context.Context, userID, resourceOwner string, req *domain.MetadataSearchRequest) (*domain.MetadataSearchResponse, error) {
err := req.EnsureLimit(repo.SearchLimit)
if err != nil {
return nil, err
}
sequence, sequenceErr := repo.View.GetLatestUserSequence()
logging.Log("EVENT-m0ds3").OnError(sequenceErr).Warn("could not read latest user sequence")
req.AppendAggregateIDQuery(userID)
req.AppendResourceOwnerQuery(resourceOwner)
metadata, count, err := repo.View.SearchMetadata(req)
if err != nil {
return nil, err
}
result := &domain.MetadataSearchResponse{
Offset: req.Offset,
Limit: req.Limit,
TotalResult: count,
Result: iam_model.MetadataViewsToDomain(metadata),
}
if sequenceErr == nil {
result.Sequence = sequence.CurrentSequence
result.Timestamp = sequence.LastSuccessfulSpoolerRun
}
return result, nil
}
func (repo *UserRepo) UserMFAs(ctx context.Context, userID string) ([]*usr_model.MultiFactor, error) {
user, err := repo.UserByID(ctx, userID)
if err != nil {

View File

@ -85,6 +85,8 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es
handler{view, bulkLimit, configs.cycleDuration("PrivacyPolicy"), errorCount, es}),
newCustomText(
handler{view, bulkLimit, configs.cycleDuration("CustomText"), errorCount, es}),
newMetadata(
handler{view, bulkLimit, configs.cycleDuration("Metadata"), errorCount, es}),
}
}

View File

@ -0,0 +1,124 @@
package handler
import (
"github.com/caos/logging"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1"
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/eventstore/v1/query"
"github.com/caos/zitadel/internal/eventstore/v1/spooler"
iam_model "github.com/caos/zitadel/internal/iam/repository/view/model"
usr_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
)
type Metadata struct {
handler
subscription *v1.Subscription
}
func newMetadata(handler handler) *Metadata {
h := &Metadata{
handler: handler,
}
h.subscribe()
return h
}
func (m *Metadata) subscribe() {
m.subscription = m.es.Subscribe(m.AggregateTypes()...)
go func() {
for event := range m.subscription.Events {
query.ReduceEvent(m, event)
}
}()
}
const (
metadataTable = "management.metadata"
)
func (m *Metadata) ViewModel() string {
return metadataTable
}
func (m *Metadata) Subscription() *v1.Subscription {
return m.subscription
}
func (_ *Metadata) AggregateTypes() []es_models.AggregateType {
return []es_models.AggregateType{usr_model.UserAggregate}
}
func (p *Metadata) CurrentSequence() (uint64, error) {
sequence, err := p.view.GetLatestMetadataSequence()
if err != nil {
return 0, err
}
return sequence.CurrentSequence, nil
}
func (m *Metadata) EventQuery() (*es_models.SearchQuery, error) {
sequence, err := m.view.GetLatestMetadataSequence()
if err != nil {
return nil, err
}
return es_models.NewSearchQuery().
AggregateTypeFilter(m.AggregateTypes()...).
LatestSequenceFilter(sequence.CurrentSequence), nil
}
func (m *Metadata) Reduce(event *es_models.Event) (err error) {
switch event.AggregateType {
case usr_model.UserAggregate:
err = m.processMetadata(event)
}
return err
}
func (m *Metadata) processMetadata(event *es_models.Event) (err error) {
metadata := new(iam_model.MetadataView)
switch event.Type {
case usr_model.UserMetadataSet:
err = metadata.SetData(event)
if err != nil {
return err
}
metadata, err = m.view.MetadataByKey(event.AggregateID, metadata.Key)
if err != nil && !caos_errs.IsNotFound(err) {
return err
}
if caos_errs.IsNotFound(err) {
err = nil
metadata = new(iam_model.MetadataView)
metadata.CreationDate = event.CreationDate
}
err = metadata.AppendEvent(event)
case usr_model.UserMetadataRemoved:
data := new(iam_model.MetadataView)
err = data.SetData(event)
if err != nil {
return err
}
return m.view.DeleteMetadata(event.AggregateID, data.Key, event)
case usr_model.UserRemoved:
return m.view.DeleteMetadataByAggregateID(event.AggregateID, event)
default:
return m.view.ProcessedMetadataSequence(event)
}
if err != nil {
return err
}
return m.view.PutMetadata(metadata, event)
}
func (m *Metadata) OnError(event *es_models.Event, err error) error {
logging.LogWithFields("SPOOL-3m912", "id", event.AggregateID).WithError(err).Warn("something went wrong in custom text handler")
return spooler.HandleError(event, err, m.view.GetLatestMetadataFailedEvent, m.view.ProcessedMetadataFailedEvent, m.view.ProcessedMetadataSequence, m.errorCountUntilSkip)
}
func (o *Metadata) OnSuccess() error {
return spooler.HandleSuccess(o.view.UpdateMetadataSpoolerRunTimestamp)
}

View File

@ -0,0 +1,73 @@
package view
import (
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/iam/repository/view"
"github.com/caos/zitadel/internal/iam/repository/view/model"
global_view "github.com/caos/zitadel/internal/view/repository"
)
const (
metadataTable = "management.metadata"
)
func (v *View) MetadataByKey(aggregateID, key string) (*model.MetadataView, error) {
return view.MetadataByKey(v.Db, metadataTable, aggregateID, key)
}
func (v *View) MetadataByKeyAndResourceOwner(aggregateID, resourceOwner, key string) (*model.MetadataView, error) {
return view.MetadataByKeyAndResourceOwner(v.Db, metadataTable, aggregateID, resourceOwner, key)
}
func (v *View) MetadataListByAggregateID(aggregateID string) ([]*model.MetadataView, error) {
return view.GetMetadataList(v.Db, metadataTable, aggregateID)
}
func (v *View) SearchMetadata(request *domain.MetadataSearchRequest) ([]*model.MetadataView, uint64, error) {
return view.SearchMetadata(v.Db, metadataTable, request)
}
func (v *View) PutMetadata(template *model.MetadataView, event *models.Event) error {
err := view.PutMetadata(v.Db, metadataTable, template)
if err != nil {
return err
}
return v.ProcessedMetadataSequence(event)
}
func (v *View) DeleteMetadata(aggregateID, key string, event *models.Event) error {
err := view.DeleteMetadata(v.Db, metadataTable, aggregateID, key)
if err != nil && !errors.IsNotFound(err) {
return err
}
return v.ProcessedMetadataSequence(event)
}
func (v *View) DeleteMetadataByAggregateID(aggregateID string, event *models.Event) error {
err := view.DeleteMetadataByAggregateID(v.Db, metadataTable, aggregateID)
if err != nil && !errors.IsNotFound(err) {
return err
}
return v.ProcessedMetadataSequence(event)
}
func (v *View) GetLatestMetadataSequence() (*global_view.CurrentSequence, error) {
return v.latestSequence(metadataTable)
}
func (v *View) ProcessedMetadataSequence(event *models.Event) error {
return v.saveCurrentSequence(metadataTable, event)
}
func (v *View) UpdateMetadataSpoolerRunTimestamp() error {
return v.updateSpoolerRunSequence(metadataTable)
}
func (v *View) GetLatestMetadataFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
return v.latestFailedEvent(metadataTable, sequence)
}
func (v *View) ProcessedMetadataFailedEvent(failedEvent *global_view.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}

View File

@ -4,6 +4,7 @@ import (
"context"
"time"
"github.com/caos/zitadel/internal/domain"
key_model "github.com/caos/zitadel/internal/key/model"
"github.com/caos/zitadel/internal/user/model"
)
@ -16,6 +17,9 @@ type UserRepository interface {
GetUserByLoginNameGlobal(ctx context.Context, email string) (*model.UserView, error)
IsUserUnique(ctx context.Context, userName, email string) (bool, error)
GetMetadataByKey(ctx context.Context, userID, resourceOwner, key string) (*domain.Metadata, error)
SearchMetadata(ctx context.Context, userID, resourceOwner string, req *domain.MetadataSearchRequest) (*domain.MetadataSearchResponse, error)
UserChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.UserChanges, error)
ProfileByID(ctx context.Context, userID string) (*model.Profile, error)

View File

@ -0,0 +1,92 @@
package metadata
import (
"encoding/json"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
)
const (
SetEventType = "metadata.set"
RemovedEventType = "metadata.removed"
)
type SetEvent struct {
eventstore.BaseEvent `json:"-"`
Key string `json:"key"`
Value []byte `json:"value"`
}
func (e *SetEvent) Data() interface{} {
return e
}
func (e *SetEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewSetEvent(
base *eventstore.BaseEvent,
key string,
value []byte,
) *SetEvent {
return &SetEvent{
BaseEvent: *base,
Key: key,
Value: value,
}
}
func SetEventMapper(event *repository.Event) (eventstore.EventReader, error) {
e := &SetEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, e)
if err != nil {
return nil, errors.ThrowInternal(err, "META-3n9fs", "unable to unmarshal metadata set")
}
return e, nil
}
type RemovedEvent struct {
eventstore.BaseEvent `json:"-"`
Key string `json:"key"`
}
func (e *RemovedEvent) Data() interface{} {
return e
}
func (e *RemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewRemovedEvent(
base *eventstore.BaseEvent,
key string,
) *RemovedEvent {
return &RemovedEvent{
BaseEvent: *base,
Key: key,
}
}
func RemovedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
e := &RemovedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, e)
if err != nil {
return nil, errors.ThrowInternal(err, "META-2m99f", "unable to unmarshal metadata removed")
}
return e, nil
}

View File

@ -45,6 +45,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(UserDomainClaimedType, DomainClaimedEventMapper).
RegisterFilterEventMapper(UserDomainClaimedSentType, DomainClaimedSentEventMapper).
RegisterFilterEventMapper(UserUserNameChangedType, UsernameChangedEventMapper).
RegisterFilterEventMapper(MetadataSetType, MetadataSetEventMapper).
RegisterFilterEventMapper(MetadataRemovedType, MetadataRemovedEventMapper).
RegisterFilterEventMapper(HumanAddedType, HumanAddedEventMapper).
RegisterFilterEventMapper(HumanRegisteredType, HumanRegisteredEventMapper).
RegisterFilterEventMapper(HumanInitialCodeAddedType, HumanInitialCodeAddedEventMapper).

View File

@ -0,0 +1,63 @@
package user
import (
"context"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/repository/metadata"
)
const (
MetadataSetType = userEventTypePrefix + metadata.SetEventType
MetadataRemovedType = userEventTypePrefix + metadata.RemovedEventType
)
type MetadataSetEvent struct {
metadata.SetEvent
}
func NewMetadataSetEvent(ctx context.Context, aggregate *eventstore.Aggregate, key string, value []byte) *MetadataSetEvent {
return &MetadataSetEvent{
SetEvent: *metadata.NewSetEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
MetadataSetType),
key,
value),
}
}
func MetadataSetEventMapper(event *repository.Event) (eventstore.EventReader, error) {
e, err := metadata.SetEventMapper(event)
if err != nil {
return nil, err
}
return &MetadataSetEvent{SetEvent: *e.(*metadata.SetEvent)}, nil
}
type MetadataRemovedEvent struct {
metadata.RemovedEvent
}
func NewMetadataRemovedEvent(ctx context.Context, aggregate *eventstore.Aggregate, key string) *MetadataRemovedEvent {
return &MetadataRemovedEvent{
RemovedEvent: *metadata.NewRemovedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
MetadataRemovedType),
key),
}
}
func MetadataRemovedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
e, err := metadata.RemovedEventMapper(event)
if err != nil {
return nil, err
}
return &MetadataRemovedEvent{RemovedEvent: *e.(*metadata.RemovedEvent)}, nil
}

View File

@ -359,6 +359,11 @@ Errors:
ReadError: Übersetzungsdatei konnte nicht gelesen werden
MergeError: Übersetzungsdatei konnte nicht mit benutzerdefinierten Übersetzungen zusammengeführt werden
NotFound: Übersetzungsdatei existiert nicht
MetaData:
NotFound: Meta Daten konnten nicht gefunden werden
NoData: Meta Daten Liste ist leer
Invalid: Meta Daten sind ungültig
KeyNotExisting: Ein oder mehrere Keys existiert nicht
EventTypes:
user:
added: Benutzer hinzugefügt

View File

@ -359,6 +359,11 @@ Errors:
ReadError: Error in reading translation file
MergeError: Translation file could not be merged with custom translations
NotFound: Translation file doesn't exist
MetaData:
NotFound: Metadata not found
NoData: Metadata list is empty
Invalid: Metadata is invalid
KeyNotExisting: One or more keys do not exist
EventTypes:
user:
added: User added

View File

@ -69,6 +69,9 @@ const (
DomainClaimed models.EventType = "user.domain.claimed"
DomainClaimedSent models.EventType = "user.domain.claimed.sent"
UserMetadataSet models.EventType = "user.metadata.set"
UserMetadataRemoved models.EventType = "user.metadata.removed"
)
// the following consts are for user(v2).human

View File

@ -0,0 +1,27 @@
CREATE TABLE auth.metadata (
aggregate_id TEXT,
key TEXT,
value BYTES,
resource_owner TEXT,
creation_date TIMESTAMPTZ,
change_date TIMESTAMPTZ,
sequence BIGINT,
PRIMARY KEY (aggregate_id, key)
);
CREATE TABLE management.metadata (
aggregate_id TEXT,
key TEXT,
value BYTES,
resource_owner TEXT,
creation_date TIMESTAMPTZ,
change_date TIMESTAMPTZ,
sequence BIGINT,
PRIMARY KEY (aggregate_id, key)
);

View File

@ -7,6 +7,7 @@ import "zitadel/object.proto";
import "zitadel/options.proto";
import "zitadel/policy.proto";
import "zitadel/idp.proto";
import "zitadel/metadata.proto";
import "validate/validate.proto";
import "google/api/annotations.proto";
import "google/protobuf/duration.proto";
@ -76,6 +77,7 @@ service AuthService {
rpc ListMyUserChanges(ListMyUserChangesRequest) returns (ListMyUserChangesResponse) {
option (google.api.http) = {
post: "/users/me/changes/_search"
body: "*"
};
option (zitadel.v1.auth_option) = {
@ -87,6 +89,77 @@ service AuthService {
rpc ListMyUserSessions(ListMyUserSessionsRequest) returns (ListMyUserSessionsResponse) {
option (google.api.http) = {
post: "/users/me/sessions/_search"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "authenticated"
};
}
// Sets a user metadata by key to the authorized user
rpc SetMyMetadata(SetMyMetadataRequest) returns (SetMyMetadataResponse) {
option (google.api.http) = {
post: "/users/me/metadata/{key}"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "authenticated"
};
}
// Set a list of user metadata to the authorized user
rpc BulkSetMyMetadata(BulkSetMyMetadataRequest) returns (BulkSetMyMetadataResponse) {
option (google.api.http) = {
post: "/users/me/metadata/_bulk"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "authenticated"
};
}
// Returns the user metadata of the authorized user
rpc ListMyMetadata(ListMyMetadataRequest) returns (ListMyMetadataResponse) {
option (google.api.http) = {
post: "/users/me/metadata/_search"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "authenticated"
};
}
// Returns the user metadata by key of the authorized user
rpc GetMyMetadata(GetMyMetadataRequest) returns (GetMyMetadataResponse) {
option (google.api.http) = {
get: "/users/me/metadata/{key}"
};
option (zitadel.v1.auth_option) = {
permission: "authenticated"
};
}
// Removes a user metadata by key to the authorized user
rpc RemoveMyMetadata(RemoveMyMetadataRequest) returns (RemoveMyMetadataResponse) {
option (google.api.http) = {
delete: "/users/me/metadata/{key}"
};
option (zitadel.v1.auth_option) = {
permission: "authenticated"
};
}
// Set a list of user metadata to the authorized user
rpc BulkRemoveMyMetadata(BulkRemoveMyMetadataRequest) returns (BulkRemoveMyMetadataResponse) {
option (google.api.http) = {
delete: "/users/me/metadata/_bulk"
body: "*"
};
option (zitadel.v1.auth_option) = {
@ -98,6 +171,7 @@ service AuthService {
rpc ListMyRefreshTokens(ListMyRefreshTokensRequest) returns (ListMyRefreshTokensResponse) {
option (google.api.http) = {
post: "/users/me/tokens/refresh/_search"
body: "*"
};
option (zitadel.v1.auth_option) = {
@ -120,6 +194,7 @@ service AuthService {
rpc RevokeAllMyRefreshTokens(RevokeAllMyRefreshTokensRequest) returns (RevokeAllMyRefreshTokensResponse) {
option (google.api.http) = {
post: "/users/me/tokens/refresh/_revoke_all"
body: "*"
};
option (zitadel.v1.auth_option) = {
@ -592,6 +667,61 @@ message ListMyUserSessionsResponse {
repeated zitadel.user.v1.Session result = 1;
}
message ListMyMetadataRequest {
zitadel.v1.ListQuery query = 1;
repeated zitadel.metadata.v1.MetadataQuery queries = 2;
}
message ListMyMetadataResponse {
zitadel.v1.ListDetails details = 1;
repeated zitadel.metadata.v1.Metadata result = 2;
}
message GetMyMetadataRequest {
string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message GetMyMetadataResponse {
zitadel.metadata.v1.Metadata metadata = 1;
}
message SetMyMetadataRequest {
string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
bytes value = 2 [(validate.rules).bytes = {min_len: 1, max_len: 500000}];
}
message SetMyMetadataResponse {
zitadel.v1.ObjectDetails details = 1;
}
message BulkSetMyMetadataRequest {
message Metadata {
string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
bytes value = 2 [(validate.rules).bytes = {min_len: 1, max_len: 500000}];
}
repeated Metadata metadata = 1;
}
message BulkSetMyMetadataResponse {
zitadel.v1.ObjectDetails details = 1;
}
message RemoveMyMetadataRequest {
string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message RemoveMyMetadataResponse {
zitadel.v1.ObjectDetails details = 1;
}
message BulkRemoveMyMetadataRequest {
repeated string keys = 1 [(validate.rules).repeated.items.string = {min_len: 1, max_len: 200}];
}
message BulkRemoveMyMetadataResponse {
zitadel.v1.ObjectDetails details = 1;
}
//This is an empty request
message ListMyRefreshTokensRequest {}

View File

@ -14,6 +14,7 @@ import "zitadel/message.proto";
import "zitadel/change.proto";
import "zitadel/auth_n_key.proto";
import "zitadel/features.proto";
import "zitadel/metadata.proto";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
@ -285,6 +286,82 @@ service ManagementService {
};
}
// Sets a user metadata by key
rpc SetUserMetadata(SetUserMetadataRequest) returns (SetUserMetadataResponse) {
option (google.api.http) = {
post: "/users/{id}/metadata/{key}"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "user.write"
feature: "metadata.user"
};
}
// Set a list of user metadata
rpc BulkSetUserMetadata(BulkSetUserMetadataRequest) returns (BulkSetUserMetadataResponse) {
option (google.api.http) = {
post: "/users/{id}/metadata/_bulk"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "user.write"
feature: "metadata.user"
};
}
// Returns the user metadata
rpc ListUserMetadata(ListUserMetadataRequest) returns (ListUserMetadataResponse) {
option (google.api.http) = {
post: "/users/{id}/metadata/_search"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "user.read"
feature: "metadata.user"
};
}
// Returns the user metadata by key
rpc GetUserMetadata(GetUserMetadataRequest) returns (GetUserMetadataResponse) {
option (google.api.http) = {
get: "/users/{id}/metadata/{key}"
};
option (zitadel.v1.auth_option) = {
permission: "user.read"
feature: "metadata.user"
};
}
// Removes a user metadata by key
rpc RemoveUserMetadata(RemoveUserMetadataRequest) returns (RemoveUserMetadataResponse) {
option (google.api.http) = {
delete: "/users/{id}/metadata/{key}"
};
option (zitadel.v1.auth_option) = {
permission: "user.write"
feature: "metadata.user"
};
}
// Set a list of user metadata
rpc BulkRemoveUserMetadata(BulkRemoveUserMetadataRequest) returns (BulkRemoveUserMetadataResponse) {
option (google.api.http) = {
delete: "/users/{id}/metadata/_bulk"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "user.write"
feature: "metadata.user"
};
}
// Returns the profile of the human
rpc GetHumanProfile(GetHumanProfileRequest) returns (GetHumanProfileResponse) {
option (google.api.http) = {
@ -2827,6 +2904,68 @@ message UpdateUserNameResponse {
zitadel.v1.ObjectDetails details = 1;
}
message ListUserMetadataRequest {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
zitadel.v1.ListQuery query = 2;
repeated zitadel.metadata.v1.MetadataQuery queries = 3;
}
message ListUserMetadataResponse {
zitadel.v1.ListDetails details = 1;
repeated zitadel.metadata.v1.Metadata result = 2;
}
message GetUserMetadataRequest {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
string key = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message GetUserMetadataResponse {
zitadel.metadata.v1.Metadata metadata = 1;
}
message SetUserMetadataRequest {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
string key = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
bytes value = 3 [(validate.rules).bytes = {min_len: 1, max_len: 500000}];
}
message SetUserMetadataResponse {
string id = 1;
zitadel.v1.ObjectDetails details = 2;
}
message BulkSetUserMetadataRequest {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
message Metadata {
string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
bytes value = 2 [(validate.rules).bytes = {min_len: 1, max_len: 500000}];
}
repeated Metadata metadata = 2;
}
message BulkSetUserMetadataResponse {
zitadel.v1.ObjectDetails details = 1;
}
message RemoveUserMetadataRequest {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
string key = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message RemoveUserMetadataResponse {
zitadel.v1.ObjectDetails details = 1;
}
message BulkRemoveUserMetadataRequest {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
repeated string keys = 2 [(validate.rules).repeated.items.string = {min_len: 1, max_len: 200}];
}
message BulkRemoveUserMetadataResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetHumanProfileRequest {
string user_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}

View File

@ -0,0 +1,45 @@
syntax = "proto3";
import "zitadel/object.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
package zitadel.metadata.v1;
option go_package ="github.com/caos/zitadel/pkg/grpc/metadata";
message Metadata {
zitadel.v1.ObjectDetails details = 1;
string key = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "metadata key"
}
];
bytes value = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "metadata value"
}
];
}
message MetadataQuery {
oneof query {
option (validate.required) = true;
MetadataKeyQuery key_query = 1;
}
}
message MetadataKeyQuery {
string key = 1 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"key\""
}
];
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";
}
];
}