mirror of
https://github.com/zitadel/zitadel.git
synced 2025-03-01 08:17:23 +00:00
feat: preuserinfo execution for actions v2
This commit is contained in:
parent
afb9fe0f3d
commit
0b0e660d2d
@ -460,7 +460,7 @@ func startAPIs(
|
|||||||
if err := apis.RegisterService(ctx, idp_v2.CreateServer(commands, queries, permissionCheck)); err != nil {
|
if err := apis.RegisterService(ctx, idp_v2.CreateServer(commands, queries, permissionCheck)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := apis.RegisterService(ctx, action_v3_alpha.CreateServer(config.SystemDefaults, commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil {
|
if err := apis.RegisterService(ctx, action_v3_alpha.CreateServer(config.SystemDefaults, commands, queries, domain.AllActionFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := apis.RegisterService(ctx, userschema_v3_alpha.CreateServer(config.SystemDefaults, commands, queries)); err != nil {
|
if err := apis.RegisterService(ctx, userschema_v3_alpha.CreateServer(config.SystemDefaults, commands, queries)); err != nil {
|
||||||
|
@ -106,9 +106,7 @@ func TestServer_CreateCallback(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
req *oidc_pb.CreateCallbackRequest
|
req *oidc_pb.CreateCallbackRequest
|
||||||
AuthError string
|
|
||||||
want *oidc_pb.CreateCallbackResponse
|
want *oidc_pb.CreateCallbackResponse
|
||||||
wantURL *url.URL
|
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
@ -8,21 +8,45 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/brianvoe/gofakeit/v6"
|
"github.com/brianvoe/gofakeit/v6"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||||
|
"golang.org/x/text/language"
|
||||||
"google.golang.org/protobuf/types/known/durationpb"
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
|
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
|
||||||
|
oidc_api "github.com/zitadel/zitadel/internal/api/oidc"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
|
"github.com/zitadel/zitadel/pkg/grpc/app"
|
||||||
|
"github.com/zitadel/zitadel/pkg/grpc/management"
|
||||||
|
"github.com/zitadel/zitadel/pkg/grpc/metadata"
|
||||||
|
object_v2 "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
|
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
|
||||||
|
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
|
||||||
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
|
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
|
||||||
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
|
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
|
||||||
|
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||||
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
redirectURIImplicit = "http://localhost:9999/callback"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
loginV2 = &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: nil}}}
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServer_ExecutionTarget(t *testing.T) {
|
func TestServer_ExecutionTarget(t *testing.T) {
|
||||||
@ -408,3 +432,375 @@ func testServerCall(
|
|||||||
|
|
||||||
return server.URL, server.Close
|
return server.URL, server.Close
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func conditionFunction(function string) *action.Condition {
|
||||||
|
return &action.Condition{
|
||||||
|
ConditionType: &action.Condition_Function{
|
||||||
|
Function: &action.FunctionExecution{
|
||||||
|
Name: function,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_ExecutionTargetPreUserinfo(t *testing.T) {
|
||||||
|
instance := integration.NewInstance(CTX)
|
||||||
|
ensureFeatureEnabled(t, instance)
|
||||||
|
isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
|
||||||
|
ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin)
|
||||||
|
|
||||||
|
client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, redirectURIImplicit, loginV2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
userEmail := gofakeit.Email()
|
||||||
|
userPhone := "+41" + gofakeit.Phone()
|
||||||
|
userResp := instance.CreateHumanUserVerified(isolatedIAMCtx, instance.DefaultOrg.Id, userEmail, userPhone)
|
||||||
|
sessionResp, err := instance.Client.SessionV2.CreateSession(ctxLoginClient, &session.CreateSessionRequest{
|
||||||
|
Checks: &session.Checks{
|
||||||
|
User: &session.CheckUser{
|
||||||
|
Search: &session.CheckUser_UserId{
|
||||||
|
UserId: userResp.GetUserId(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
type want struct {
|
||||||
|
resp *oidc_pb.CreateCallbackResponse
|
||||||
|
addedClaims map[string]any
|
||||||
|
addedLogClaims map[string][]string
|
||||||
|
setUserMetadata []*metadata.Metadata
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
dep func(ctx context.Context, t *testing.T) func()
|
||||||
|
req *oidc_pb.CreateCallbackRequest
|
||||||
|
want want
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "append claim",
|
||||||
|
ctx: ctxLoginClient,
|
||||||
|
dep: func(ctx context.Context, t *testing.T) func() {
|
||||||
|
changedRequest := &oidc_api.ContextInfoPreUserinfoResponse{
|
||||||
|
AppendClaims: []*oidc_api.AppendClaim{
|
||||||
|
{Key: "added", Value: "value"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expectedContextInfo := contextInfoPreUserinfoForUser(instance, userResp, userEmail, userPhone)
|
||||||
|
|
||||||
|
targetURL, closeF := testServerCall(expectedContextInfo, 0, http.StatusOK, changedRequest)
|
||||||
|
|
||||||
|
targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true)
|
||||||
|
waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preuserinfo"), executionTargetsSingleTarget(targetResp.GetDetails().GetId()))
|
||||||
|
|
||||||
|
return closeF
|
||||||
|
},
|
||||||
|
req: &oidc_pb.CreateCallbackRequest{
|
||||||
|
AuthRequestId: func() string {
|
||||||
|
authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return authRequestID
|
||||||
|
}(),
|
||||||
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
||||||
|
Session: &oidc_pb.Session{
|
||||||
|
SessionId: sessionResp.GetSessionId(),
|
||||||
|
SessionToken: sessionResp.GetSessionToken(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
resp: &oidc_pb.CreateCallbackResponse{
|
||||||
|
CallbackUrl: `http:\/\/localhost:9999\/callback#access_token=(.*)&expires_in=(.*)&id_token=(.*)&state=state&token_type=Bearer`,
|
||||||
|
Details: &object_v2.Details{
|
||||||
|
ChangeDate: timestamppb.Now(),
|
||||||
|
ResourceOwner: instance.ID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
addedClaims: map[string]any{
|
||||||
|
"added": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "append log claim",
|
||||||
|
ctx: ctxLoginClient,
|
||||||
|
dep: func(ctx context.Context, t *testing.T) func() {
|
||||||
|
changedRequest := &oidc_api.ContextInfoPreUserinfoResponse{
|
||||||
|
AppendLogClaims: []string{
|
||||||
|
"addedLog",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expectedContextInfo := contextInfoPreUserinfoForUser(instance, userResp, userEmail, userPhone)
|
||||||
|
|
||||||
|
targetURL, closeF := testServerCall(expectedContextInfo, 0, http.StatusOK, changedRequest)
|
||||||
|
|
||||||
|
targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true)
|
||||||
|
waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preuserinfo"), executionTargetsSingleTarget(targetResp.GetDetails().GetId()))
|
||||||
|
|
||||||
|
return closeF
|
||||||
|
},
|
||||||
|
req: &oidc_pb.CreateCallbackRequest{
|
||||||
|
AuthRequestId: func() string {
|
||||||
|
authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return authRequestID
|
||||||
|
}(),
|
||||||
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
||||||
|
Session: &oidc_pb.Session{
|
||||||
|
SessionId: sessionResp.GetSessionId(),
|
||||||
|
SessionToken: sessionResp.GetSessionToken(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
resp: &oidc_pb.CreateCallbackResponse{
|
||||||
|
CallbackUrl: `http:\/\/localhost:9999\/callback#access_token=(.*)&expires_in=(.*)&id_token=(.*)&state=state&token_type=Bearer`,
|
||||||
|
Details: &object_v2.Details{
|
||||||
|
ChangeDate: timestamppb.Now(),
|
||||||
|
ResourceOwner: instance.ID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
addedLogClaims: map[string][]string{
|
||||||
|
"urn:zitadel:iam:action:function/preuserinfo:log": {"addedLog"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set user metadata",
|
||||||
|
ctx: ctxLoginClient,
|
||||||
|
dep: func(ctx context.Context, t *testing.T) func() {
|
||||||
|
changedRequest := &oidc_api.ContextInfoPreUserinfoResponse{
|
||||||
|
SetUserMetadata: []*domain.Metadata{
|
||||||
|
{Key: "key", Value: []byte("value")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expectedContextInfo := contextInfoPreUserinfoForUser(instance, userResp, userEmail, userPhone)
|
||||||
|
|
||||||
|
targetURL, closeF := testServerCall(expectedContextInfo, 0, http.StatusOK, changedRequest)
|
||||||
|
|
||||||
|
targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true)
|
||||||
|
waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preuserinfo"), executionTargetsSingleTarget(targetResp.GetDetails().GetId()))
|
||||||
|
|
||||||
|
return closeF
|
||||||
|
},
|
||||||
|
req: &oidc_pb.CreateCallbackRequest{
|
||||||
|
AuthRequestId: func() string {
|
||||||
|
authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return authRequestID
|
||||||
|
}(),
|
||||||
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
||||||
|
Session: &oidc_pb.Session{
|
||||||
|
SessionId: sessionResp.GetSessionId(),
|
||||||
|
SessionToken: sessionResp.GetSessionToken(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
resp: &oidc_pb.CreateCallbackResponse{
|
||||||
|
CallbackUrl: `http:\/\/localhost:9999\/callback#access_token=(.*)&expires_in=(.*)&id_token=(.*)&state=state&token_type=Bearer`,
|
||||||
|
Details: &object_v2.Details{
|
||||||
|
ChangeDate: timestamppb.Now(),
|
||||||
|
ResourceOwner: instance.ID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setUserMetadata: []*metadata.Metadata{
|
||||||
|
{Key: "key", Value: []byte("value")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "full usage",
|
||||||
|
ctx: ctxLoginClient,
|
||||||
|
dep: func(ctx context.Context, t *testing.T) func() {
|
||||||
|
changedRequest := &oidc_api.ContextInfoPreUserinfoResponse{
|
||||||
|
SetUserMetadata: []*domain.Metadata{
|
||||||
|
{Key: "key1", Value: []byte("value1")},
|
||||||
|
{Key: "key2", Value: []byte("value2")},
|
||||||
|
{Key: "key3", Value: []byte("value3")},
|
||||||
|
},
|
||||||
|
AppendLogClaims: []string{
|
||||||
|
"addedLog1",
|
||||||
|
"addedLog2",
|
||||||
|
"addedLog3",
|
||||||
|
},
|
||||||
|
AppendClaims: []*oidc_api.AppendClaim{
|
||||||
|
{Key: "added1", Value: "value1"},
|
||||||
|
{Key: "added2", Value: "value2"},
|
||||||
|
{Key: "added3", Value: "value3"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expectedContextInfo := contextInfoPreUserinfoForUser(instance, userResp, userEmail, userPhone)
|
||||||
|
|
||||||
|
targetURL, closeF := testServerCall(expectedContextInfo, 0, http.StatusOK, changedRequest)
|
||||||
|
|
||||||
|
targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true)
|
||||||
|
waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preuserinfo"), executionTargetsSingleTarget(targetResp.GetDetails().GetId()))
|
||||||
|
|
||||||
|
return closeF
|
||||||
|
},
|
||||||
|
req: &oidc_pb.CreateCallbackRequest{
|
||||||
|
AuthRequestId: func() string {
|
||||||
|
authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return authRequestID
|
||||||
|
}(),
|
||||||
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
||||||
|
Session: &oidc_pb.Session{
|
||||||
|
SessionId: sessionResp.GetSessionId(),
|
||||||
|
SessionToken: sessionResp.GetSessionToken(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
resp: &oidc_pb.CreateCallbackResponse{
|
||||||
|
CallbackUrl: `http:\/\/localhost:9999\/callback#access_token=(.*)&expires_in=(.*)&id_token=(.*)&state=state&token_type=Bearer`,
|
||||||
|
Details: &object_v2.Details{
|
||||||
|
ChangeDate: timestamppb.Now(),
|
||||||
|
ResourceOwner: instance.ID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
addedClaims: map[string]any{
|
||||||
|
"added1": "value1",
|
||||||
|
"added2": "value2",
|
||||||
|
"added3": "value3",
|
||||||
|
},
|
||||||
|
setUserMetadata: []*metadata.Metadata{
|
||||||
|
{Key: "key1", Value: []byte("value1")},
|
||||||
|
{Key: "key2", Value: []byte("value2")},
|
||||||
|
{Key: "key3", Value: []byte("value3")},
|
||||||
|
},
|
||||||
|
addedLogClaims: map[string][]string{
|
||||||
|
"urn:zitadel:iam:action:function/preuserinfo:log": {"addedLog1", "addedLog2", "addedLog3"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
closeF := tt.dep(isolatedIAMCtx, t)
|
||||||
|
defer closeF()
|
||||||
|
|
||||||
|
got, err := instance.Client.OIDCv2.CreateCallback(tt.ctx, tt.req)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
if tt.want.resp != nil {
|
||||||
|
if !assert.Regexp(t, regexp.MustCompile(tt.want.resp.CallbackUrl), got.GetCallbackUrl()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
callbackUrl, err := url.Parse(strings.Replace(got.GetCallbackUrl(), "#", "?", 1))
|
||||||
|
require.NoError(t, err)
|
||||||
|
claims := getClaimsFromCallbackURL(tt.ctx, t, instance, client.GetClientId(), callbackUrl)
|
||||||
|
|
||||||
|
for k, v := range tt.want.addedClaims {
|
||||||
|
value, ok := claims.Claims[k]
|
||||||
|
if !assert.True(t, ok) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, v, value)
|
||||||
|
}
|
||||||
|
for k, v := range tt.want.addedLogClaims {
|
||||||
|
value, ok := claims.Claims[k]
|
||||||
|
if !assert.True(t, ok) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.ElementsMatch(t, v, value)
|
||||||
|
}
|
||||||
|
if len(tt.want.setUserMetadata) > 0 {
|
||||||
|
checkForSetMetadata(isolatedIAMCtx, t, instance, userResp.GetUserId(), tt.want.setUserMetadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkForSetMetadata(ctx context.Context, t *testing.T, instance *integration.Instance, userID string, metadataExpected []*metadata.Metadata) {
|
||||||
|
integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
|
||||||
|
|
||||||
|
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
|
||||||
|
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||||
|
metadataResp, err := instance.Client.Mgmt.ListUserMetadata(ctx, &management.ListUserMetadataRequest{Id: userID})
|
||||||
|
if !assert.NoError(ct, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, dataExpected := range metadataExpected {
|
||||||
|
found := false
|
||||||
|
for _, dataCheck := range metadataResp.GetResult() {
|
||||||
|
if dataExpected.Key == dataCheck.Key {
|
||||||
|
found = true
|
||||||
|
if !assert.Equal(ct, dataExpected.Value, dataCheck.Value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !assert.True(ct, found) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, retryDuration, tick)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClaimsFromCallbackURL(ctx context.Context, t *testing.T, instance *integration.Instance, clientID string, callbackURL *url.URL) *oidc.IDTokenClaims {
|
||||||
|
accessToken := callbackURL.Query().Get("access_token")
|
||||||
|
idToken := callbackURL.Query().Get("id_token")
|
||||||
|
|
||||||
|
provider, err := instance.CreateRelyingParty(ctx, clientID, redirectURIImplicit, oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopePhone)
|
||||||
|
require.NoError(t, err)
|
||||||
|
claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), accessToken, idToken, provider.IDTokenVerifier())
|
||||||
|
require.NoError(t, err)
|
||||||
|
return claims
|
||||||
|
}
|
||||||
|
|
||||||
|
func contextInfoPreUserinfoForUser(instance *integration.Instance, userResp *user.AddHumanUserResponse, email, phone string) *oidc_api.ContextInfoPreUserinfo {
|
||||||
|
return &oidc_api.ContextInfoPreUserinfo{
|
||||||
|
Function: "function/preuserinfo",
|
||||||
|
UserInfo: &oidc.UserInfo{
|
||||||
|
Subject: userResp.GetUserId(),
|
||||||
|
},
|
||||||
|
User: &query.User{
|
||||||
|
ID: userResp.GetUserId(),
|
||||||
|
CreationDate: userResp.Details.ChangeDate.AsTime(),
|
||||||
|
ChangeDate: userResp.Details.ChangeDate.AsTime(),
|
||||||
|
ResourceOwner: instance.DefaultOrg.GetId(),
|
||||||
|
Sequence: userResp.Details.Sequence,
|
||||||
|
State: 1,
|
||||||
|
Username: email,
|
||||||
|
PreferredLoginName: email,
|
||||||
|
Human: &query.Human{
|
||||||
|
FirstName: "Mickey",
|
||||||
|
LastName: "Mouse",
|
||||||
|
NickName: "Mickey",
|
||||||
|
DisplayName: "Mickey Mouse",
|
||||||
|
AvatarKey: "",
|
||||||
|
PreferredLanguage: language.Dutch,
|
||||||
|
Gender: 2,
|
||||||
|
Email: domain.EmailAddress(email),
|
||||||
|
IsEmailVerified: true,
|
||||||
|
Phone: domain.PhoneNumber(phone),
|
||||||
|
IsPhoneVerified: true,
|
||||||
|
PasswordChangeRequired: false,
|
||||||
|
PasswordChanged: time.Time{},
|
||||||
|
MFAInitSkipped: time.Time{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
UserMetadata: nil,
|
||||||
|
Org: &query.UserInfoOrg{
|
||||||
|
ID: instance.DefaultOrg.GetId(),
|
||||||
|
Name: instance.DefaultOrg.GetName(),
|
||||||
|
PrimaryDomain: instance.DefaultOrg.GetPrimaryDomain(),
|
||||||
|
},
|
||||||
|
UserGrants: nil,
|
||||||
|
Response: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -20,7 +20,9 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/actions/object"
|
"github.com/zitadel/zitadel/internal/actions/object"
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/execution"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
|
exec_repo "github.com/zitadel/zitadel/internal/repository/execution"
|
||||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
)
|
)
|
||||||
@ -410,5 +412,115 @@ func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, user
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function := exec_repo.ID(domain.ExecutionTypeFunction, domain.ActionFunctionPreUserinfo.LocalizationKey())
|
||||||
|
executionTargets, err := queryExecutionTargets(ctx, s.query, function)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
info := &ContextInfoPreUserinfo{
|
||||||
|
Function: function,
|
||||||
|
UserInfo: userInfo,
|
||||||
|
User: qu.User,
|
||||||
|
UserMetadata: qu.Metadata,
|
||||||
|
Org: qu.Org,
|
||||||
|
UserGrants: qu.UserGrants,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := execution.CallTargets(ctx, executionTargets, info)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
contextInfoResponse := resp.(*ContextInfoPreUserinfoResponse)
|
||||||
|
claimLogs := make([]string, 0)
|
||||||
|
for _, metadata := range contextInfoResponse.SetUserMetadata {
|
||||||
|
if _, err = s.command.SetUserMetadata(ctx, metadata, userInfo.Subject, qu.User.ResourceOwner); err != nil {
|
||||||
|
claimLogs = append(claimLogs, fmt.Sprintf("failed to set user metadata key %q", metadata.Key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, claim := range contextInfoResponse.AppendClaims {
|
||||||
|
if strings.HasPrefix(claim.Key, ClaimPrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if userInfo.Claims[claim.Key] == nil {
|
||||||
|
userInfo.AppendClaims(claim.Key, claim.Value)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
claimLogs = append(claimLogs, fmt.Sprintf("key %q already exists", claim.Key))
|
||||||
|
}
|
||||||
|
for _, log := range contextInfoResponse.AppendLogClaims {
|
||||||
|
claimLogs = append(claimLogs, log)
|
||||||
|
}
|
||||||
|
if len(claimLogs) > 0 {
|
||||||
|
userInfo.AppendClaims(fmt.Sprintf(ClaimActionLogFormat, function), claimLogs)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ContextInfoPreUserinfo struct {
|
||||||
|
Function string `json:"function,omitempty"`
|
||||||
|
UserInfo *oidc.UserInfo `json:"userinfo,omitempty"`
|
||||||
|
User *query.User `json:"user,omitempty"`
|
||||||
|
UserMetadata []query.UserMetadata `json:"user_metadata,omitempty"`
|
||||||
|
Org *query.UserInfoOrg `json:"org,omitempty"`
|
||||||
|
UserGrants []query.UserGrant `json:"user_grants,omitempty"`
|
||||||
|
Response *ContextInfoPreUserinfoResponse `json:"response,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContextInfoPreUserinfoResponse struct {
|
||||||
|
SetUserMetadata []*domain.Metadata `json:"set_user_metadata,omitempty"`
|
||||||
|
AppendClaims []*AppendClaim `json:"append_claims,omitempty"`
|
||||||
|
AppendLogClaims []string `json:"append_log_claims,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppendClaim struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value any `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ContextInfoPreUserinfo) GetHTTPRequestBody() []byte {
|
||||||
|
data, err := json.Marshal(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ContextInfoPreUserinfo) SetHTTPResponseBody(resp []byte) error {
|
||||||
|
if !json.Valid(resp) {
|
||||||
|
return zerrors.ThrowPreconditionFailed(nil, "ACTION-4m9s2", "Errors.Execution.ResponseIsNotValidJSON")
|
||||||
|
}
|
||||||
|
if c.Response == nil {
|
||||||
|
c.Response = &ContextInfoPreUserinfoResponse{}
|
||||||
|
}
|
||||||
|
return json.Unmarshal(resp, c.Response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ContextInfoPreUserinfo) GetContent() interface{} {
|
||||||
|
return c.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryExecutionTargets(ctx context.Context, query *query.Queries, function string) ([]execution.Target, error) {
|
||||||
|
queriedActionsV2, err := query.TargetsByExecutionID(ctx, []string{function})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
executionTargets := make([]execution.Target, len(queriedActionsV2))
|
||||||
|
for i, action := range queriedActionsV2 {
|
||||||
|
executionTargets[i] = action
|
||||||
|
}
|
||||||
|
return executionTargets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) queryOrgMetadata(ctx context.Context, organizationID string) ([]*query.OrgMetadata, error) {
|
||||||
|
metadata, err := s.query.SearchOrgMetadata(
|
||||||
|
ctx,
|
||||||
|
true,
|
||||||
|
organizationID,
|
||||||
|
&query.OrgMetadataSearchQueries{},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return metadata.Metadata, nil
|
||||||
|
}
|
||||||
|
@ -183,7 +183,7 @@ func StartCommands(
|
|||||||
EventGroupExisting: func(group string) bool { return true },
|
EventGroupExisting: func(group string) bool { return true },
|
||||||
GrpcServiceExisting: func(service string) bool { return false },
|
GrpcServiceExisting: func(service string) bool { return false },
|
||||||
GrpcMethodExisting: func(method string) bool { return false },
|
GrpcMethodExisting: func(method string) bool { return false },
|
||||||
ActionFunctionExisting: domain.FunctionExists(),
|
ActionFunctionExisting: domain.ActionFunctionExists(),
|
||||||
multifactors: domain.MultifactorConfigs{
|
multifactors: domain.MultifactorConfigs{
|
||||||
OTP: domain.OTPConfig{
|
OTP: domain.OTPConfig{
|
||||||
CryptoMFA: otpEncryption,
|
CryptoMFA: otpEncryption,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package domain
|
package domain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||||
@ -45,3 +46,49 @@ const (
|
|||||||
ActionsMaxAllowed
|
ActionsMaxAllowed
|
||||||
ActionsAllowedUnlimited
|
ActionsAllowedUnlimited
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ActionFunction int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActionFunctionUnspecified ActionFunction = iota
|
||||||
|
ActionFunctionPreUserinfo
|
||||||
|
ActionFunctionPreAccessToken
|
||||||
|
ActionFunctionPreSAMLResponse
|
||||||
|
actionFunctionCount
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s ActionFunction) Valid() bool {
|
||||||
|
return s >= 0 && s < actionFunctionCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ActionFunction) LocalizationKey() string {
|
||||||
|
if !s.Valid() {
|
||||||
|
return ActionFunctionUnspecified.LocalizationKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case ActionFunctionPreUserinfo:
|
||||||
|
return "preuserinfo"
|
||||||
|
case ActionFunctionPreAccessToken:
|
||||||
|
return "preaccesstoken"
|
||||||
|
case ActionFunctionPreSAMLResponse:
|
||||||
|
return "presamlresponse"
|
||||||
|
default:
|
||||||
|
return "unspecified"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AllActionFunctions() []string {
|
||||||
|
return []string{
|
||||||
|
ActionFunctionPreUserinfo.LocalizationKey(),
|
||||||
|
ActionFunctionPreAccessToken.LocalizationKey(),
|
||||||
|
ActionFunctionPreSAMLResponse.LocalizationKey(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ActionFunctionExists() func(string) bool {
|
||||||
|
functions := AllActionFunctions()
|
||||||
|
return func(s string) bool {
|
||||||
|
return slices.Contains(functions, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package domain
|
package domain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -150,20 +149,3 @@ func (s TriggerType) LocalizationKey() string {
|
|||||||
return "Action.TriggerType.Unspecified"
|
return "Action.TriggerType.Unspecified"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AllFunctions() []string {
|
|
||||||
functions := make([]string, 0)
|
|
||||||
for _, flowType := range AllFlowTypes() {
|
|
||||||
for _, triggerType := range flowType.TriggerTypes() {
|
|
||||||
functions = append(functions, flowType.LocalizationKey()+"."+triggerType.LocalizationKey())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return functions
|
|
||||||
}
|
|
||||||
|
|
||||||
func FunctionExists() func(string) bool {
|
|
||||||
functions := AllFunctions()
|
|
||||||
return func(s string) bool {
|
|
||||||
return slices.Contains(functions, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user