chore!: Introduce ZITADEL v3 (#9645)

This PR summarizes multiple changes specifically only available with
ZITADEL v3:

- feat: Web Keys management
(https://github.com/zitadel/zitadel/pull/9526)
- fix(cmd): ensure proper working of mirror
(https://github.com/zitadel/zitadel/pull/9509)
- feat(Authz): system user support for permission check v2
(https://github.com/zitadel/zitadel/pull/9640)
- chore(license): change from Apache to AGPL
(https://github.com/zitadel/zitadel/pull/9597)
- feat(console): list v2 sessions
(https://github.com/zitadel/zitadel/pull/9539)
- fix(console): add loginV2 feature flag
(https://github.com/zitadel/zitadel/pull/9682)
- fix(feature flags): allow reading "own" flags
(https://github.com/zitadel/zitadel/pull/9649)
- feat(console): add Actions V2 UI
(https://github.com/zitadel/zitadel/pull/9591)

BREAKING CHANGE
- feat(webkey): migrate to v2beta API
(https://github.com/zitadel/zitadel/pull/9445)
- chore!: remove CockroachDB Support
(https://github.com/zitadel/zitadel/pull/9444)
- feat(actions): migrate to v2beta API
(https://github.com/zitadel/zitadel/pull/9489)

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com>
Co-authored-by: Ramon <mail@conblem.me>
Co-authored-by: Elio Bischof <elio@zitadel.com>
Co-authored-by: Kenta Yamaguchi <56732734+KEY60228@users.noreply.github.com>
Co-authored-by: Harsha Reddy <harsha.reddy@klaviyo.com>
Co-authored-by: Livio Spring <livio@zitadel.com>
Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Iraq <66622793+kkrime@users.noreply.github.com>
Co-authored-by: Florian Forster <florian@zitadel.com>
Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Max Peintner <peintnerm@gmail.com>
This commit is contained in:
Fabienne Bühler
2025-04-02 16:53:06 +02:00
committed by GitHub
parent d14a23ae7e
commit 07ce3b6905
559 changed files with 14578 additions and 7622 deletions

View File

@@ -15,7 +15,7 @@ import (
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/reflection"
internal_authz "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
http_util "github.com/zitadel/zitadel/internal/api/http"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
@@ -29,7 +29,7 @@ import (
type API struct {
port uint16
grpcServer *grpc.Server
verifier internal_authz.APITokenVerifier
verifier authz.APITokenVerifier
health healthCheck
router *mux.Router
hostHeaders []string
@@ -72,8 +72,9 @@ func New(
port uint16,
router *mux.Router,
queries *query.Queries,
verifier internal_authz.APITokenVerifier,
authZ internal_authz.Config,
verifier authz.APITokenVerifier,
systemAuthz authz.Config,
authZ authz.Config,
tlsConfig *tls.Config,
externalDomain string,
hostHeaders []string,
@@ -89,7 +90,7 @@ func New(
hostHeaders: hostHeaders,
}
api.grpcServer = server.CreateServer(api.verifier, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService())
api.grpcServer = server.CreateServer(api.verifier, systemAuthz, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService())
api.grpcGateway, err = server.CreateGateway(ctx, port, hostHeaders, accessInterceptor, tlsConfig)
if err != nil {
return nil, err

View File

@@ -94,13 +94,13 @@ func DefaultErrorHandler(translator *i18n.Translator) func(w http.ResponseWriter
}
}
func NewHandler(commands *command.Commands, verifier authz.APITokenVerifier, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries, callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor func(handler http.Handler) http.Handler) http.Handler {
func NewHandler(commands *command.Commands, verifier authz.APITokenVerifier, systemAuthCOnfig authz.Config, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries, callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor func(handler http.Handler) http.Handler) http.Handler {
translator, err := i18n.NewZitadelTranslator(language.English)
logging.OnError(err).Panic("unable to get translator")
h := &Handler{
commands: commands,
errorHandler: DefaultErrorHandler(translator),
authInterceptor: http_mw.AuthorizationInterceptor(verifier, authConfig),
authInterceptor: http_mw.AuthorizationInterceptor(verifier, systemAuthCOnfig, authConfig),
idGenerator: idGenerator,
storage: storage,
query: queries,
@@ -129,8 +129,10 @@ func (l *publicFileDownloader) ResourceOwner(_ context.Context, ownerPath string
return ownerPath
}
const maxMemory = 2 << 20
const paramFile = "file"
const (
maxMemory = 2 << 20
paramFile = "file"
)
func UploadHandleFunc(s AssetsService, uploader Uploader) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {

View File

@@ -4,8 +4,11 @@ import (
"context"
"fmt"
"reflect"
"slices"
"strings"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -16,10 +19,10 @@ const (
// CheckUserAuthorization verifies that:
// - the token is active,
// - the organisation (**either** provided by ID or verified domain) exists
// - the organization (**either** provided by ID or verified domain) exists
// - the user is permitted to call the requested endpoint (permission option in proto)
// it will pass the [CtxData] and permission of the user into the ctx [context.Context]
func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, orgDomain string, verifier APITokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) {
func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, orgDomain string, verifier APITokenVerifier, systemRolePermissionMapping []RoleMapping, rolePermissionMapping []RoleMapping, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) {
ctx, span := tracing.NewServerInterceptorSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -30,11 +33,12 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID,
if requiredAuthOption.Permission == authenticated {
return func(parent context.Context) context.Context {
parent = addGetSystemUserRolesToCtx(parent, systemRolePermissionMapping, ctxData)
return context.WithValue(parent, dataKey, ctxData)
}, nil
}
requestedPermissions, allPermissions, err := getUserPermissions(ctx, verifier, requiredAuthOption.Permission, authConfig.RolePermissionMappings, ctxData, ctxData.OrgID)
requestedPermissions, allPermissions, err := getUserPermissions(ctx, verifier, requiredAuthOption.Permission, systemRolePermissionMapping, rolePermissionMapping, ctxData, ctxData.OrgID)
if err != nil {
return nil, err
}
@@ -50,6 +54,7 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID,
parent = context.WithValue(parent, dataKey, ctxData)
parent = context.WithValue(parent, allPermissionsKey, allPermissions)
parent = context.WithValue(parent, requestPermissionsKey, requestedPermissions)
parent = addGetSystemUserRolesToCtx(parent, systemRolePermissionMapping, ctxData)
return parent
}, nil
}
@@ -125,3 +130,43 @@ func GetAllPermissionCtxIDs(perms []string) []string {
}
return ctxIDs
}
type SystemUserPermissionsDBQuery struct {
MemberType string `json:"member_type"`
AggregateID string `json:"aggregate_id"`
ObjectID string `json:"object_id"`
Permissions []string `json:"permissions"`
}
func addGetSystemUserRolesToCtx(ctx context.Context, systemUserRoleMap []RoleMapping, ctxData CtxData) context.Context {
if len(ctxData.SystemMemberships) == 0 {
return ctx
}
systemUserPermissions := make([]SystemUserPermissionsDBQuery, len(ctxData.SystemMemberships))
for i, systemPerm := range ctxData.SystemMemberships {
permissions := make([]string, 0, len(systemPerm.Roles))
for _, role := range systemPerm.Roles {
permissions = append(permissions, getPermissionsFromRole(systemUserRoleMap, role)...)
}
slices.Sort(permissions)
permissions = slices.Compact(permissions)
systemUserPermissions[i].MemberType = systemPerm.MemberType.String()
systemUserPermissions[i].AggregateID = systemPerm.AggregateID
systemUserPermissions[i].Permissions = permissions
}
return context.WithValue(ctx, systemUserRolesKey, systemUserPermissions)
}
func GetSystemUserPermissions(ctx context.Context) []SystemUserPermissionsDBQuery {
getSystemUserRolesFuncValue := ctx.Value(systemUserRolesKey)
if getSystemUserRolesFuncValue == nil {
return nil
}
systemUserRoles, ok := getSystemUserRolesFuncValue.([]SystemUserPermissionsDBQuery)
if !ok {
logging.WithFields("Authz").Error("unable to cast []SystemUserPermissionsDBQuery")
return nil
}
return systemUserRoles
}

View File

@@ -22,6 +22,7 @@ const (
dataKey key = 2
allPermissionsKey key = 3
instanceKey key = 4
systemUserRolesKey key = 5
)
type CtxData struct {
@@ -50,7 +51,8 @@ type Memberships []*Membership
type Membership struct {
MemberType MemberType
AggregateID string
//ObjectID differs from aggregate id if object is sub of an aggregate
InstanceID string
// ObjectID differs from aggregate id if object is sub of an aggregate
ObjectID string
Roles []string

View File

@@ -7,8 +7,8 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMappings []RoleMapping, permission, orgID, resourceID string) (err error) {
requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, roleMappings, GetCtxData(ctx), orgID)
func CheckPermission(ctx context.Context, resolver MembershipsResolver, systemUserRoleMapping []RoleMapping, roleMappings []RoleMapping, permission, orgID, resourceID string) (err error) {
requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, systemUserRoleMapping, roleMappings, GetCtxData(ctx), orgID)
if err != nil {
return err
}
@@ -22,7 +22,7 @@ func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMapp
// getUserPermissions retrieves the memberships of the authenticated user (on instance and provided organisation level),
// and maps them to permissions. It will return the requested permission(s) and all other granted permissions separately.
func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requiredPerm string, roleMappings []RoleMapping, ctxData CtxData, orgID string) (requestedPermissions, allPermissions []string, err error) {
func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requiredPerm string, systemUserRoleMappings []RoleMapping, roleMappings []RoleMapping, ctxData CtxData, orgID string) (requestedPermissions, allPermissions []string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -31,7 +31,7 @@ func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requi
}
if ctxData.SystemMemberships != nil {
requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, ctxData.SystemMemberships, roleMappings)
requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, ctxData.SystemMemberships, systemUserRoleMappings)
return requestedPermissions, allPermissions, nil
}

View File

@@ -120,7 +120,7 @@ func Test_GetUserPermissions(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, perms, err := getUserPermissions(context.Background(), tt.args.membershipsResolver, tt.args.requiredPerm, tt.args.authConfig.RolePermissionMappings, tt.args.ctxData, tt.args.ctxData.OrgID)
_, perms, err := getUserPermissions(context.Background(), tt.args.membershipsResolver, tt.args.requiredPerm, nil, tt.args.authConfig.RolePermissionMappings, tt.args.ctxData, tt.args.ctxData.OrgID)
if tt.wantErr && err == nil {
t.Errorf("got wrong result, should get err: actual: %v ", err)

View File

@@ -3,21 +3,21 @@ package action
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
)
func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionRequest) (*action.SetExecutionResponse, error) {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
reqTargets := req.GetExecution().GetTargets()
reqTargets := req.GetTargets()
targets := make([]*execution.Target, len(reqTargets))
for i, target := range reqTargets {
switch t := target.GetType().(type) {
@@ -56,7 +56,7 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque
return nil, err
}
return &action.SetExecutionResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
SetDate: timestamppb.New(details.EventDate),
}, nil
}

View File

@@ -5,12 +5,8 @@ package action_test
import (
"context"
"encoding/base64"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"testing"
"time"
@@ -32,21 +28,17 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/internal/query"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/app"
"github.com/zitadel/zitadel/pkg/grpc/management"
"github.com/zitadel/zitadel/pkg/grpc/metadata"
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"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2"
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
const (
redirectURI = "https://callback"
logoutRedirectURI = "https://logged-out"
redirectURIImplicit = "http://localhost:9999/callback"
)
@@ -58,13 +50,12 @@ func TestServer_ExecutionTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
fullMethod := "/zitadel.resources.action.v3alpha.ZITADELActions/GetTarget"
fullMethod := action.ActionService_GetTarget_FullMethodName
tests := []struct {
name string
ctx context.Context
dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) (func(), error)
dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) (closeF func(), calledF func() bool)
clean func(context.Context)
req *action.GetTargetRequest
want *action.GetTargetResponse
@@ -73,7 +64,7 @@ func TestServer_ExecutionTarget(t *testing.T) {
{
name: "GetTarget, request and response, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), error) {
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), func() bool) {
orgID := instance.DefaultOrg.Id
projectID := ""
@@ -87,50 +78,55 @@ func TestServer_ExecutionTarget(t *testing.T) {
// request received by target
wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instance.ID(), OrgID: orgID, ProjectID: projectID, UserID: userID, Request: request}
changedRequest := &action.GetTargetRequest{Id: targetCreated.GetDetails().GetId()}
changedRequest := &action.GetTargetRequest{Id: targetCreated.GetId()}
// replace original request with different targetID
urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusOK, changedRequest)
urlRequest, closeRequest, calledRequest, _ := integration.TestServerCall(wantRequest, 0, http.StatusOK, changedRequest)
targetRequest := waitForTarget(ctx, t, instance, urlRequest, domain.TargetTypeCall, false)
waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetDetails().GetId()))
waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetId()))
// expected response from the GetTarget
expectedResponse := &action.GetTargetResponse{
Target: &action.GetTarget{
Config: &action.Target{
Name: targetCreatedName,
Endpoint: targetCreatedURL,
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
Details: targetCreated.GetDetails(),
},
}
// has to be set separately because of the pointers
response.Target = &action.GetTarget{
Details: targetCreated.GetDetails(),
Config: &action.Target{
Name: targetCreatedName,
Target: &action.Target{
Id: targetCreated.GetId(),
CreationDate: targetCreated.GetCreationDate(),
ChangeDate: targetCreated.GetCreationDate(),
Name: targetCreatedName,
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
RestCall: &action.RESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
Endpoint: targetCreatedURL,
Timeout: durationpb.New(5 * time.Second),
Endpoint: targetCreatedURL,
SigningKey: targetCreated.GetSigningKey(),
},
}
// has to be set separately because of the pointers
response.Target = &action.Target{
Id: targetCreated.GetId(),
CreationDate: targetCreated.GetCreationDate(),
ChangeDate: targetCreated.GetCreationDate(),
Name: targetCreatedName,
TargetType: &action.Target_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(5 * time.Second),
Endpoint: targetCreatedURL,
SigningKey: targetCreated.GetSigningKey(),
}
// content for partial update
changedResponse := &action.GetTargetResponse{
Target: &action.GetTarget{
Details: &resource_object.Details{
Id: targetCreated.GetDetails().GetId(),
Target: &action.Target{
Id: targetCreated.GetId(),
TargetType: &action.Target_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: false,
},
},
},
}
@@ -146,14 +142,22 @@ func TestServer_ExecutionTarget(t *testing.T) {
Response: expectedResponse,
}
// after request with different targetID, return changed response
targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusOK, changedResponse)
targetResponseURL, closeResponse, calledResponse, _ := integration.TestServerCall(wantResponse, 0, http.StatusOK, changedResponse)
targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, domain.TargetTypeCall, false)
waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetDetails().GetId()))
waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetId()))
return func() {
closeRequest()
closeResponse()
}, nil
closeRequest()
closeResponse()
}, func() bool {
if calledRequest() != 1 {
return false
}
if calledResponse() != 1 {
return false
}
return true
}
},
clean: func(ctx context.Context) {
instance.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod))
@@ -163,38 +167,32 @@ func TestServer_ExecutionTarget(t *testing.T) {
Id: "something",
},
want: &action.GetTargetResponse{
Target: &action.GetTarget{
Details: &resource_object.Details{
Id: "changed",
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
Target: &action.Target{
Id: "changed",
},
},
},
{
name: "GetTarget, request, interrupt",
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), error) {
fullMethod := "/zitadel.resources.action.v3alpha.ZITADELActions/GetTarget"
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), func() bool) {
orgID := instance.DefaultOrg.Id
projectID := ""
userID := instance.Users.Get(integration.UserTypeIAMOwner).ID
// request received by target
wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instance.ID(), OrgID: orgID, ProjectID: projectID, UserID: userID, Request: request}
urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusInternalServerError, &action.GetTargetRequest{Id: "notchanged"})
urlRequest, closeRequest, calledRequest, _ := integration.TestServerCall(wantRequest, 0, http.StatusInternalServerError, &action.GetTargetRequest{Id: "notchanged"})
targetRequest := waitForTarget(ctx, t, instance, urlRequest, domain.TargetTypeCall, true)
waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetDetails().GetId()))
waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetId()))
// GetTarget with used target
request.Id = targetRequest.GetDetails().GetId()
request.Id = targetRequest.GetId()
return func() {
closeRequest()
}, nil
closeRequest()
}, func() bool {
return calledRequest() == 1
}
},
clean: func(ctx context.Context) {
instance.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod))
@@ -205,9 +203,7 @@ func TestServer_ExecutionTarget(t *testing.T) {
{
name: "GetTarget, response, interrupt",
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), error) {
fullMethod := "/zitadel.resources.action.v3alpha.ZITADELActions/GetTarget"
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), func() bool) {
orgID := instance.DefaultOrg.Id
projectID := ""
userID := instance.Users.Get(integration.UserTypeIAMOwner).ID
@@ -219,29 +215,33 @@ func TestServer_ExecutionTarget(t *testing.T) {
targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false)
// GetTarget with used target
request.Id = targetCreated.GetDetails().GetId()
request.Id = targetCreated.GetId()
// expected response from the GetTarget
expectedResponse := &action.GetTargetResponse{
Target: &action.GetTarget{
Details: targetCreated.GetDetails(),
Config: &action.Target{
Name: targetCreatedName,
Endpoint: targetCreatedURL,
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
Target: &action.Target{
Id: targetCreated.GetId(),
CreationDate: targetCreated.GetCreationDate(),
ChangeDate: targetCreated.GetCreationDate(),
Name: targetCreatedName,
TargetType: &action.Target_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: false,
},
Timeout: durationpb.New(10 * time.Second),
},
Timeout: durationpb.New(5 * time.Second),
Endpoint: targetCreatedURL,
SigningKey: targetCreated.GetSigningKey(),
},
}
// content for partial update
changedResponse := &action.GetTargetResponse{
Target: &action.GetTarget{
Details: &resource_object.Details{
Id: "changed",
Target: &action.Target{
Id: "changed",
TargetType: &action.Target_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: false,
},
},
},
}
@@ -257,13 +257,15 @@ func TestServer_ExecutionTarget(t *testing.T) {
Response: expectedResponse,
}
// after request with different targetID, return changed response
targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusInternalServerError, changedResponse)
targetResponseURL, closeResponse, calledResponse, _ := integration.TestServerCall(wantResponse, 0, http.StatusInternalServerError, changedResponse)
targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, domain.TargetTypeCall, true)
waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetDetails().GetId()))
waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetId()))
return func() {
closeResponse()
}, nil
closeResponse()
}, func() bool {
return calledResponse() == 1
}
},
clean: func(ctx context.Context) {
instance.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod))
@@ -274,29 +276,200 @@ func TestServer_ExecutionTarget(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.dep != nil {
close, err := tt.dep(tt.ctx, tt.req, tt.want)
require.NoError(t, err)
defer close()
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, time.Minute)
closeF, calledF := tt.dep(tt.ctx, tt.req, tt.want)
defer closeF()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, err := instance.Client.ActionV3Alpha.GetTarget(tt.ctx, tt.req)
got, err := instance.Client.ActionV2beta.GetTarget(tt.ctx, tt.req)
if tt.wantErr {
require.Error(ttt, err)
return
}
require.NoError(ttt, err)
integration.AssertResourceDetails(ttt, tt.want.GetTarget().GetDetails(), got.GetTarget().GetDetails())
tt.want.Target.Details = got.GetTarget().GetDetails()
assert.EqualExportedValues(ttt, tt.want.GetTarget().GetConfig(), got.GetTarget().GetConfig())
assert.EqualExportedValues(ttt, tt.want.GetTarget(), got.GetTarget())
}, retryDuration, tick, "timeout waiting for expected execution result")
if tt.clean != nil {
tt.clean(tt.ctx)
}
require.True(t, calledF())
})
}
}
func TestServer_ExecutionTarget_Event(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
event := "session.added"
urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 0, http.StatusOK, nil)
defer closeF()
targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, domain.TargetTypeWebhook, true)
waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), executionTargetsSingleTarget(targetResponse.GetId()))
tests := []struct {
name string
ctx context.Context
eventCount int
expectedCalls int
clean func(context.Context)
wantErr bool
}{
{
name: "event, 1 session.added, ok",
ctx: isolatedIAMOwnerCTX,
eventCount: 1,
expectedCalls: 1,
},
{
name: "event, 5 session.added, ok",
ctx: isolatedIAMOwnerCTX,
eventCount: 5,
expectedCalls: 5,
},
{
name: "event, 50 session.added, ok",
ctx: isolatedIAMOwnerCTX,
eventCount: 50,
expectedCalls: 50,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// reset the count of the target
resetF()
for i := 0; i < tt.eventCount; i++ {
_, err := instance.Client.SessionV2.CreateSession(tt.ctx, &session.CreateSessionRequest{})
require.NoError(t, err)
}
// wait for called target
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
assert.True(ttt, calledF() == tt.expectedCalls)
}, retryDuration, tick, "timeout waiting for expected execution result")
})
}
}
func TestServer_ExecutionTarget_Event_LongerThanTargetTimeout(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
event := "session.added"
// call takes longer than timeout of target
urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 5*time.Second, http.StatusOK, nil)
defer closeF()
targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, domain.TargetTypeWebhook, true)
waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), executionTargetsSingleTarget(targetResponse.GetId()))
tests := []struct {
name string
ctx context.Context
eventCount int
expectedCalls int
clean func(context.Context)
wantErr bool
}{
{
name: "event, 1 session.added, error logs",
ctx: isolatedIAMOwnerCTX,
eventCount: 1,
expectedCalls: 1,
},
{
name: "event, 5 session.added, error logs",
ctx: isolatedIAMOwnerCTX,
eventCount: 5,
expectedCalls: 5,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// reset the count of the target
resetF()
for i := 0; i < tt.eventCount; i++ {
_, err := instance.Client.SessionV2.CreateSession(tt.ctx, &session.CreateSessionRequest{})
require.NoError(t, err)
}
// wait for called target
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
assert.True(ttt, calledF() == tt.expectedCalls)
}, retryDuration, tick, "timeout waiting for expected execution result")
})
}
}
func TestServer_ExecutionTarget_Event_LongerThanTransactionTimeout(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
event := "session.added"
urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 1*time.Second, http.StatusOK, nil)
defer closeF()
targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, domain.TargetTypeWebhook, true)
waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), executionTargetsSingleTarget(targetResponse.GetId()))
tests := []struct {
name string
ctx context.Context
eventCount int
expectedCalls int
clean func(context.Context)
wantErr bool
}{
{
name: "event, 1 session.added, ok",
ctx: isolatedIAMOwnerCTX,
eventCount: 1,
expectedCalls: 1,
},
{
name: "event, 5 session.added, ok",
ctx: isolatedIAMOwnerCTX,
eventCount: 5,
expectedCalls: 5,
},
{
name: "event, 5 session.added, ok",
ctx: isolatedIAMOwnerCTX,
eventCount: 5,
expectedCalls: 5,
},
{
name: "event, 20 session.added, ok",
ctx: isolatedIAMOwnerCTX,
eventCount: 20,
expectedCalls: 20,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// reset the count of the target
resetF()
for i := 0; i < tt.eventCount; i++ {
_, err := instance.Client.SessionV2.CreateSession(tt.ctx, &session.CreateSessionRequest{})
require.NoError(t, err)
}
// wait for called target
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
assert.True(ttt, calledF() == tt.expectedCalls)
}, retryDuration, tick, "timeout waiting for expected execution result")
})
}
}
@@ -306,7 +479,7 @@ func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *in
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, err := instance.Client.ActionV3Alpha.SearchExecutions(ctx, &action.SearchExecutionsRequest{
got, err := instance.Client.ActionV2beta.ListExecutions(ctx, &action.ListExecutionsRequest{
Filters: []*action.ExecutionSearchFilter{
{Filter: &action.ExecutionSearchFilter_InConditionsFilter{
InConditionsFilter: &action.InConditionsFilter{Conditions: []*action.Condition{condition}},
@@ -319,7 +492,7 @@ func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *in
if !assert.Len(ttt, got.GetResult(), 1) {
return
}
gotTargets := got.GetResult()[0].GetExecution().GetTargets()
gotTargets := got.GetResult()[0].GetTargets()
// always first check length, otherwise its failed anyway
if assert.Len(ttt, gotTargets, len(targets)) {
for i := range targets {
@@ -335,10 +508,10 @@ func waitForTarget(ctx context.Context, t *testing.T, instance *integration.Inst
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, err := instance.Client.ActionV3Alpha.SearchTargets(ctx, &action.SearchTargetsRequest{
got, err := instance.Client.ActionV2beta.ListTargets(ctx, &action.ListTargetsRequest{
Filters: []*action.TargetSearchFilter{
{Filter: &action.TargetSearchFilter_InTargetIdsFilter{
InTargetIdsFilter: &action.InTargetIDsFilter{TargetIds: []string{resp.GetDetails().GetId()}},
InTargetIdsFilter: &action.InTargetIDsFilter{TargetIds: []string{resp.GetId()}},
}},
},
})
@@ -348,7 +521,7 @@ func waitForTarget(ctx context.Context, t *testing.T, instance *integration.Inst
if !assert.Len(ttt, got.GetResult(), 1) {
return
}
config := got.GetResult()[0].GetConfig()
config := got.GetResult()[0]
assert.Equal(ttt, config.GetEndpoint(), endpoint)
switch ty {
case domain.TargetTypeWebhook:
@@ -392,50 +565,16 @@ func conditionResponseFullMethod(fullMethod string) *action.Condition {
}
}
func testServerCall(
reqBody interface{},
sleep time.Duration,
statusCode int,
respBody interface{},
) (string, func()) {
handler := func(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(reqBody)
if err != nil {
http.Error(w, "error, marshall: "+err.Error(), http.StatusInternalServerError)
return
}
sentBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "error, read body: "+err.Error(), http.StatusInternalServerError)
return
}
if !reflect.DeepEqual(data, sentBody) {
http.Error(w, "error, equal:\n"+string(data)+"\nsent:\n"+string(sentBody), http.StatusInternalServerError)
return
}
if statusCode != http.StatusOK {
http.Error(w, "error, statusCode", statusCode)
return
}
time.Sleep(sleep)
w.Header().Set("Content-Type", "application/json")
resp, err := json.Marshal(respBody)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
if _, err := io.WriteString(w, string(resp)); err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
func conditionEvent(event string) *action.Condition {
return &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Event{
Event: event,
},
},
},
}
server := httptest.NewServer(http.HandlerFunc(handler))
return server.URL, server.Close
}
func conditionFunction(function string) *action.Condition {
@@ -643,10 +782,10 @@ func expectPreUserinfoExecution(ctx context.Context, t *testing.T, instance *int
}
expectedContextInfo := contextInfoForUserOIDC(instance, "function/preuserinfo", userResp, userEmail, userPhone)
targetURL, closeF := testServerCall(expectedContextInfo, 0, http.StatusOK, response)
targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response)
targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true)
waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preuserinfo"), executionTargetsSingleTarget(targetResp.GetDetails().GetId()))
waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preuserinfo"), executionTargetsSingleTarget(targetResp.GetId()))
return userResp.GetUserId(), closeF
}
@@ -949,10 +1088,10 @@ func expectPreAccessTokenExecution(ctx context.Context, t *testing.T, instance *
}
expectedContextInfo := contextInfoForUserOIDC(instance, "function/preaccesstoken", userResp, userEmail, userPhone)
targetURL, closeF := testServerCall(expectedContextInfo, 0, http.StatusOK, response)
targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response)
targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true)
waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preaccesstoken"), executionTargetsSingleTarget(targetResp.GetDetails().GetId()))
waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preaccesstoken"), executionTargetsSingleTarget(targetResp.GetId()))
return userResp.GetUserId(), closeF
}
@@ -1115,10 +1254,10 @@ func expectPreSAMLResponseExecution(ctx context.Context, t *testing.T, instance
}
expectedContextInfo := contextInfoForUserSAML(instance, "function/presamlresponse", userResp, userEmail, userPhone)
targetURL, closeF := testServerCall(expectedContextInfo, 0, http.StatusOK, response)
targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response)
targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true)
waitForExecutionOnCondition(ctx, t, instance, conditionFunction("presamlresponse"), executionTargetsSingleTarget(targetResp.GetDetails().GetId()))
waitForExecutionOnCondition(ctx, t, instance, conditionFunction("presamlresponse"), executionTargetsSingleTarget(targetResp.GetId()))
return userResp.GetUserId(), closeF
}

View File

@@ -5,15 +5,14 @@ package action_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
)
func executionTargetsSingleTarget(id string) []*action.ExecutionTargetType {
@@ -31,11 +30,11 @@ func TestServer_SetExecution_Request(t *testing.T) {
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
name string
ctx context.Context
req *action.SetExecutionRequest
wantSetDate bool
wantErr bool
}{
{
name: "missing permission",
@@ -60,9 +59,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
Request: &action.RequestExecution{},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@@ -79,9 +76,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@@ -98,19 +93,9 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantSetDate: true,
},
{
name: "service, not existing",
@@ -125,9 +110,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@@ -144,19 +127,9 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantSetDate: true,
},
{
name: "all, ok",
@@ -171,33 +144,24 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantSetDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
creationDate := time.Now().UTC()
got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req)
setDate := time.Now().UTC()
if tt.wantErr {
require.Error(t, err)
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.Details, got.Details)
assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
@@ -205,6 +169,18 @@ func TestServer_SetExecution_Request(t *testing.T) {
}
}
func assertSetExecutionResponse(t *testing.T, creationDate, setDate time.Time, expectedSetDate bool, actualResp *action.SetExecutionResponse) {
if expectedSetDate {
if !setDate.IsZero() {
assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, setDate)
} else {
assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, time.Now().UTC())
}
} else {
assert.Nil(t, actualResp.SetDate)
}
}
func TestServer_SetExecution_Request_Include(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
@@ -221,7 +197,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
}
instance.SetExecution(isolatedIAMOwnerCTX, t,
executionCond,
executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
executionTargetsSingleTarget(targetResp.GetId()),
)
circularExecutionService := &action.Condition{
@@ -252,20 +228,18 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
name string
ctx context.Context
req *action.SetExecutionRequest
wantSetDate bool
wantErr bool
}{
{
name: "method, circular error",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: circularExecutionService,
Execution: &action.Execution{
Targets: executionTargetsSingleInclude(circularExecutionMethod),
},
Targets: executionTargetsSingleInclude(circularExecutionMethod),
},
wantErr: true,
},
@@ -282,19 +256,9 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleInclude(executionCond),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
Targets: executionTargetsSingleInclude(executionCond),
},
wantSetDate: true,
},
{
name: "service, ok",
@@ -304,38 +268,28 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
Service: "zitadel.user.v2beta.UserService",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleInclude(executionCond),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
Targets: executionTargetsSingleInclude(executionCond),
},
wantSetDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
creationDate := time.Now().UTC()
got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req)
setDate := time.Now().UTC()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.Details, got.Details)
assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
@@ -350,11 +304,11 @@ func TestServer_SetExecution_Response(t *testing.T) {
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
name string
ctx context.Context
req *action.SetExecutionRequest
wantSetDate bool
wantErr bool
}{
{
name: "missing permission",
@@ -379,9 +333,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
Response: &action.ResponseExecution{},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@@ -398,9 +350,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@@ -417,19 +367,9 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantSetDate: true,
},
{
name: "service, not existing",
@@ -444,9 +384,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@@ -463,19 +401,9 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantSetDate: true,
},
{
name: "all, ok",
@@ -490,33 +418,23 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantSetDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
creationDate := time.Now().UTC()
got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req)
setDate := time.Now().UTC()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.Details, got.Details)
assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
@@ -531,11 +449,11 @@ func TestServer_SetExecution_Event(t *testing.T) {
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
name string
ctx context.Context
req *action.SetExecutionRequest
wantSetDate bool
wantErr bool
}{
{
name: "missing permission",
@@ -562,33 +480,27 @@ func TestServer_SetExecution_Event(t *testing.T) {
Event: &action.EventExecution{},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
/*
//TODO event existing check
{
name: "event, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Event{
Event: "xxx",
},
{
name: "event, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Event{
Event: "user.human.notexisting",
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
*/
wantErr: true,
},
{
name: "event, ok",
ctx: isolatedIAMOwnerCTX,
@@ -597,72 +509,65 @@ func TestServer_SetExecution_Event(t *testing.T) {
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Event{
Event: "xxx",
Event: "user.human.added",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantSetDate: true,
},
/*
// TODO:
{
name: "group, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Group{
Group: "xxx",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
*/
{
name: "group, ok",
name: "group, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Group{
Group: "xxx",
Group: "user.notexisting",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
wantErr: true,
},
{
name: "group, level 1, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Group{
Group: "user",
},
},
},
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantSetDate: true,
},
{
name: "group, level 2, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Group{
Group: "user.human",
},
},
},
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantSetDate: true,
},
{
name: "all, ok",
@@ -677,33 +582,23 @@ func TestServer_SetExecution_Event(t *testing.T) {
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantSetDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
creationDate := time.Now().UTC()
got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req)
setDate := time.Now().UTC()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.Details, got.Details)
assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
@@ -718,11 +613,11 @@ func TestServer_SetExecution_Function(t *testing.T) {
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
name string
ctx context.Context
req *action.SetExecutionRequest
wantSetDate bool
wantErr bool
}{
{
name: "missing permission",
@@ -747,9 +642,7 @@ func TestServer_SetExecution_Function(t *testing.T) {
Response: &action.ResponseExecution{},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@@ -762,9 +655,7 @@ func TestServer_SetExecution_Function(t *testing.T) {
Function: &action.FunctionExecution{Name: "xxx"},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@@ -777,33 +668,23 @@ func TestServer_SetExecution_Function(t *testing.T) {
Function: &action.FunctionExecution{Name: "presamlresponse"},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantSetDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
creationDate := time.Now().UTC()
got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req)
setDate := time.Now().UTC()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.Details, got.Details)
assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())

View File

@@ -13,8 +13,8 @@ import (
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
)
var (
@@ -60,7 +60,7 @@ func ensureFeatureEnabled(t *testing.T, instance *integration.Instance) {
retryDuration, tick = integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute)
require.EventuallyWithT(t,
func(ttt *assert.CollectT) {
_, err := instance.Client.ActionV3Alpha.ListExecutionMethods(ctx, &action.ListExecutionMethodsRequest{})
_, err := instance.Client.ActionV2beta.ListExecutionMethods(ctx, &action.ListExecutionMethodsRequest{})
assert.NoError(ttt, err)
},
retryDuration,

View File

@@ -0,0 +1,553 @@
//go:build integration
package action_test
import (
"context"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
)
func TestServer_CreateTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
type want struct {
id bool
creationDate bool
signingKey bool
}
alreadyExistingTargetName := gofakeit.AppName()
instance.CreateTarget(isolatedIAMOwnerCTX, t, alreadyExistingTargetName, "https://example.com", domain.TargetTypeAsync, false)
tests := []struct {
name string
ctx context.Context
req *action.CreateTargetRequest
want
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
},
wantErr: true,
},
{
name: "empty name",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: "",
},
wantErr: true,
},
{
name: "empty type",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
TargetType: nil,
},
wantErr: true,
},
{
name: "empty webhook url",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{},
},
},
wantErr: true,
},
{
name: "empty request response url",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.RESTCall{},
},
},
wantErr: true,
},
{
name: "empty timeout",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{},
},
Timeout: nil,
},
wantErr: true,
},
{
name: "async, already existing, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: alreadyExistingTargetName,
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestAsync{
RestAsync: &action.RESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
},
wantErr: true,
},
{
name: "async, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestAsync{
RestAsync: &action.RESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
},
want: want{
id: true,
creationDate: true,
signingKey: true,
},
},
{
name: "webhook, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: want{
id: true,
creationDate: true,
signingKey: true,
},
},
{
name: "webhook, interrupt on error, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: want{
id: true,
creationDate: true,
signingKey: true,
},
},
{
name: "call, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: want{
id: true,
creationDate: true,
signingKey: true,
},
},
{
name: "call, interruptOnError, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: want{
id: true,
creationDate: true,
signingKey: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
creationDate := time.Now().UTC()
got, err := instance.Client.ActionV2beta.CreateTarget(tt.ctx, tt.req)
changeDate := time.Now().UTC()
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assertCreateTargetResponse(t, creationDate, changeDate, tt.want.creationDate, tt.want.id, tt.want.signingKey, got)
})
}
}
func assertCreateTargetResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate, expectedID, expectedSigningKey bool, actualResp *action.CreateTargetResponse) {
if expectedCreationDate {
if !changeDate.IsZero() {
assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate)
} else {
assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC())
}
} else {
assert.Nil(t, actualResp.CreationDate)
}
if expectedID {
assert.NotEmpty(t, actualResp.GetId())
} else {
assert.Nil(t, actualResp.Id)
}
if expectedSigningKey {
assert.NotEmpty(t, actualResp.GetSigningKey())
} else {
assert.Nil(t, actualResp.SigningKey)
}
}
func TestServer_UpdateTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
type args struct {
ctx context.Context
req *action.UpdateTargetRequest
}
type want struct {
change bool
changeDate bool
signingKey bool
}
tests := []struct {
name string
prepare func(request *action.UpdateTargetRequest)
args args
want want
wantErr bool
}{
{
name: "missing permission",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.UpdateTargetRequest{
Name: gu.Ptr(gofakeit.Name()),
},
},
wantErr: true,
},
{
name: "not existing",
prepare: func(request *action.UpdateTargetRequest) {
request.Id = "notexisting"
return
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
Name: gu.Ptr(gofakeit.Name()),
},
},
wantErr: true,
},
{
name: "no change, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
Endpoint: gu.Ptr("https://example.com"),
},
},
want: want{
change: false,
changeDate: true,
signingKey: false,
},
},
{
name: "change name, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
Name: gu.Ptr(gofakeit.Name()),
},
},
want: want{
change: true,
changeDate: true,
signingKey: false,
},
},
{
name: "regenerate signingkey, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
ExpirationSigningKey: durationpb.New(0 * time.Second),
},
},
want: want{
change: true,
changeDate: true,
signingKey: true,
},
},
{
name: "change type, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
TargetType: &action.UpdateTargetRequest_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: true,
},
},
},
},
want: want{
change: true,
changeDate: true,
signingKey: false,
},
},
{
name: "change url, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
Endpoint: gu.Ptr("https://example.com/hooks/new"),
},
},
want: want{
change: true,
changeDate: true,
signingKey: false,
},
},
{
name: "change timeout, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
Timeout: durationpb.New(20 * time.Second),
},
},
want: want{
change: true,
changeDate: true,
signingKey: false,
},
},
{
name: "change type async, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
TargetType: &action.UpdateTargetRequest_RestAsync{
RestAsync: &action.RESTAsync{},
},
},
},
want: want{
change: true,
changeDate: true,
signingKey: false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
creationDate := time.Now().UTC()
tt.prepare(tt.args.req)
got, err := instance.Client.ActionV2beta.UpdateTarget(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
changeDate := time.Time{}
if tt.want.change {
changeDate = time.Now().UTC()
}
assert.NoError(t, err)
assertUpdateTargetResponse(t, creationDate, changeDate, tt.want.changeDate, tt.want.signingKey, got)
})
}
}
func assertUpdateTargetResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate, expectedSigningKey bool, actualResp *action.UpdateTargetResponse) {
if expectedChangeDate {
if !changeDate.IsZero() {
assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate)
} else {
assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC())
}
} else {
assert.Nil(t, actualResp.ChangeDate)
}
if expectedSigningKey {
assert.NotEmpty(t, actualResp.GetSigningKey())
} else {
assert.Nil(t, actualResp.SigningKey)
}
}
func TestServer_DeleteTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
tests := []struct {
name string
ctx context.Context
prepare func(request *action.DeleteTargetRequest) (time.Time, time.Time)
req *action.DeleteTargetRequest
wantDeletionDate bool
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.DeleteTargetRequest{
Id: "notexisting",
},
wantErr: true,
},
{
name: "empty id",
ctx: iamOwnerCtx,
req: &action.DeleteTargetRequest{
Id: "",
},
wantErr: true,
},
{
name: "delete target, not existing",
ctx: iamOwnerCtx,
req: &action.DeleteTargetRequest{
Id: "notexisting",
},
wantDeletionDate: false,
},
{
name: "delete target",
ctx: iamOwnerCtx,
prepare: func(request *action.DeleteTargetRequest) (time.Time, time.Time) {
creationDate := time.Now().UTC()
targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
return creationDate, time.Time{}
},
req: &action.DeleteTargetRequest{},
wantDeletionDate: true,
},
{
name: "delete target, already removed",
ctx: iamOwnerCtx,
prepare: func(request *action.DeleteTargetRequest) (time.Time, time.Time) {
creationDate := time.Now().UTC()
targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
instance.DeleteTarget(iamOwnerCtx, t, targetID)
return creationDate, time.Now().UTC()
},
req: &action.DeleteTargetRequest{},
wantDeletionDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var creationDate, deletionDate time.Time
if tt.prepare != nil {
creationDate, deletionDate = tt.prepare(tt.req)
}
got, err := instance.Client.ActionV2beta.DeleteTarget(tt.ctx, tt.req)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assertDeleteTargetResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got)
})
}
}
func assertDeleteTargetResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *action.DeleteTargetResponse) {
if expectedDeletionDate {
if !deletionDate.IsZero() {
assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate)
} else {
assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC())
}
} else {
assert.Nil(t, actualResp.DeletionDate)
}
}

View File

@@ -5,14 +5,14 @@ import (
"strings"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
)
const (
@@ -45,11 +45,11 @@ type Context interface {
GetOwner() InstanceContext
}
func (s *Server) SearchTargets(ctx context.Context, req *action.SearchTargetsRequest) (*action.SearchTargetsResponse, error) {
func (s *Server) ListTargets(ctx context.Context, req *action.ListTargetsRequest) (*action.ListTargetsResponse, error) {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
queries, err := s.searchTargetsRequestToModel(req)
queries, err := s.ListTargetsRequestToModel(req)
if err != nil {
return nil, err
}
@@ -57,17 +57,17 @@ func (s *Server) SearchTargets(ctx context.Context, req *action.SearchTargetsReq
if err != nil {
return nil, err
}
return &action.SearchTargetsResponse{
Result: targetsToPb(resp.Targets),
Details: resource_object.ToSearchDetailsPb(queries.SearchRequest, resp.SearchResponse),
return &action.ListTargetsResponse{
Result: targetsToPb(resp.Targets),
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse),
}, nil
}
func (s *Server) SearchExecutions(ctx context.Context, req *action.SearchExecutionsRequest) (*action.SearchExecutionsResponse, error) {
func (s *Server) ListExecutions(ctx context.Context, req *action.ListExecutionsRequest) (*action.ListExecutionsResponse, error) {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
queries, err := s.searchExecutionsRequestToModel(req)
queries, err := s.ListExecutionsRequestToModel(req)
if err != nil {
return nil, err
}
@@ -75,45 +75,50 @@ func (s *Server) SearchExecutions(ctx context.Context, req *action.SearchExecuti
if err != nil {
return nil, err
}
return &action.SearchExecutionsResponse{
Result: executionsToPb(resp.Executions),
Details: resource_object.ToSearchDetailsPb(queries.SearchRequest, resp.SearchResponse),
return &action.ListExecutionsResponse{
Result: executionsToPb(resp.Executions),
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse),
}, nil
}
func targetsToPb(targets []*query.Target) []*action.GetTarget {
t := make([]*action.GetTarget, len(targets))
func targetsToPb(targets []*query.Target) []*action.Target {
t := make([]*action.Target, len(targets))
for i, target := range targets {
t[i] = targetToPb(target)
}
return t
}
func targetToPb(t *query.Target) *action.GetTarget {
target := &action.GetTarget{
Details: resource_object.DomainToDetailsPb(&t.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, t.ResourceOwner),
Config: &action.Target{
Name: t.Name,
Timeout: durationpb.New(t.Timeout),
Endpoint: t.Endpoint,
},
func targetToPb(t *query.Target) *action.Target {
target := &action.Target{
Id: t.ObjectDetails.ID,
Name: t.Name,
Timeout: durationpb.New(t.Timeout),
Endpoint: t.Endpoint,
SigningKey: t.SigningKey,
}
switch t.TargetType {
case domain.TargetTypeWebhook:
target.Config.TargetType = &action.Target_RestWebhook{RestWebhook: &action.SetRESTWebhook{InterruptOnError: t.InterruptOnError}}
target.TargetType = &action.Target_RestWebhook{RestWebhook: &action.RESTWebhook{InterruptOnError: t.InterruptOnError}}
case domain.TargetTypeCall:
target.Config.TargetType = &action.Target_RestCall{RestCall: &action.SetRESTCall{InterruptOnError: t.InterruptOnError}}
target.TargetType = &action.Target_RestCall{RestCall: &action.RESTCall{InterruptOnError: t.InterruptOnError}}
case domain.TargetTypeAsync:
target.Config.TargetType = &action.Target_RestAsync{RestAsync: &action.SetRESTAsync{}}
target.TargetType = &action.Target_RestAsync{RestAsync: &action.RESTAsync{}}
default:
target.Config.TargetType = nil
target.TargetType = nil
}
if !t.ObjectDetails.EventDate.IsZero() {
target.ChangeDate = timestamppb.New(t.ObjectDetails.EventDate)
}
if !t.ObjectDetails.CreationDate.IsZero() {
target.CreationDate = timestamppb.New(t.ObjectDetails.CreationDate)
}
return target
}
func (s *Server) searchTargetsRequestToModel(req *action.SearchTargetsRequest) (*query.TargetSearchQueries, error) {
offset, limit, asc, err := resource_object.SearchQueryPbToQuery(s.systemDefaults, req.Query)
func (s *Server) ListTargetsRequestToModel(req *action.ListTargetsRequest) (*query.TargetSearchQueries, error) {
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination)
if err != nil {
return nil, err
}
@@ -155,7 +160,7 @@ func targetQueryToQuery(filter *action.TargetSearchFilter) (query.SearchQuery, e
}
func targetNameQueryToQuery(q *action.TargetNameFilter) (query.SearchQuery, error) {
return query.NewTargetNameSearchQuery(resource_object.TextMethodPbToQuery(q.Method), q.GetTargetName())
return query.NewTargetNameSearchQuery(filter.TextMethodPbToQuery(q.Method), q.GetTargetName())
}
func targetInTargetIdsQueryToQuery(q *action.InTargetIDsFilter) (query.SearchQuery, error) {
@@ -210,8 +215,8 @@ func executionFieldNameToSortingColumn(field *action.ExecutionFieldName) query.C
}
}
func (s *Server) searchExecutionsRequestToModel(req *action.SearchExecutionsRequest) (*query.ExecutionSearchQueries, error) {
offset, limit, asc, err := resource_object.SearchQueryPbToQuery(s.systemDefaults, req.Query)
func (s *Server) ListExecutionsRequestToModel(req *action.ListExecutionsRequest) (*query.ExecutionSearchQueries, error) {
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination)
if err != nil {
return nil, err
}
@@ -319,15 +324,15 @@ func conditionToID(q *action.Condition) (string, error) {
}
}
func executionsToPb(executions []*query.Execution) []*action.GetExecution {
e := make([]*action.GetExecution, len(executions))
func executionsToPb(executions []*query.Execution) []*action.Execution {
e := make([]*action.Execution, len(executions))
for i, execution := range executions {
e[i] = executionToPb(execution)
}
return e
}
func executionToPb(e *query.Execution) *action.GetExecution {
func executionToPb(e *query.Execution) *action.Execution {
targets := make([]*action.ExecutionTargetType, len(e.Targets))
for i := range e.Targets {
switch e.Targets[i].Type {
@@ -342,12 +347,17 @@ func executionToPb(e *query.Execution) *action.GetExecution {
}
}
return &action.GetExecution{
Details: resource_object.DomainToDetailsPb(&e.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, e.ResourceOwner),
Execution: &action.Execution{
Targets: targets,
},
exec := &action.Execution{
Condition: executionIDToCondition(e.ID),
Targets: targets,
}
if !e.ObjectDetails.EventDate.IsZero() {
exec.ChangeDate = timestamppb.New(e.ObjectDetails.EventDate)
}
if !e.ObjectDetails.CreationDate.IsZero() {
exec.CreationDate = timestamppb.New(e.ObjectDetails.CreationDate)
}
return exec
}
func executionIDToCondition(include string) *action.Condition {

View File

@@ -11,13 +11,13 @@ import (
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
)
var _ action.ZITADELActionsServer = (*Server)(nil)
var _ action.ActionServiceServer = (*Server)(nil)
type Server struct {
action.UnimplementedZITADELActionsServer
action.UnimplementedActionServiceServer
systemDefaults systemdefaults.SystemDefaults
command *command.Commands
query *query.Queries
@@ -47,23 +47,23 @@ func CreateServer(
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
action.RegisterZITADELActionsServer(grpcServer, s)
action.RegisterActionServiceServer(grpcServer, s)
}
func (s *Server) AppName() string {
return action.ZITADELActions_ServiceDesc.ServiceName
return action.ActionService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return action.ZITADELActions_ServiceDesc.ServiceName
return action.ActionService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return action.ZITADELActions_AuthMethods
return action.ActionService_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return action.RegisterZITADELActionsHandler
return action.RegisterActionServiceHandler
}
func checkActionsEnabled(ctx context.Context) error {

View File

@@ -4,14 +4,13 @@ import (
"context"
"github.com/muhlemmer/gu"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
)
func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetRequest) (*action.CreateTargetResponse, error) {
@@ -20,29 +19,38 @@ func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetReque
}
add := createTargetToCommand(req)
instanceID := authz.GetInstance(ctx).InstanceID()
details, err := s.command.AddTarget(ctx, add, instanceID)
createdAt, err := s.command.AddTarget(ctx, add, instanceID)
if err != nil {
return nil, err
}
var creationDate *timestamppb.Timestamp
if !createdAt.IsZero() {
creationDate = timestamppb.New(createdAt)
}
return &action.CreateTargetResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
SigningKey: add.SigningKey,
Id: add.AggregateID,
CreationDate: creationDate,
SigningKey: add.SigningKey,
}, nil
}
func (s *Server) PatchTarget(ctx context.Context, req *action.PatchTargetRequest) (*action.PatchTargetResponse, error) {
func (s *Server) UpdateTarget(ctx context.Context, req *action.UpdateTargetRequest) (*action.UpdateTargetResponse, error) {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
instanceID := authz.GetInstance(ctx).InstanceID()
patch := patchTargetToCommand(req)
details, err := s.command.ChangeTarget(ctx, patch, instanceID)
update := updateTargetToCommand(req)
changedAt, err := s.command.ChangeTarget(ctx, update, instanceID)
if err != nil {
return nil, err
}
return &action.PatchTargetResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
SigningKey: patch.SigningKey,
var changeDate *timestamppb.Timestamp
if !changedAt.IsZero() {
changeDate = timestamppb.New(changedAt)
}
return &action.UpdateTargetResponse{
ChangeDate: changeDate,
SigningKey: update.SigningKey,
}, nil
}
@@ -51,74 +59,76 @@ func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetReque
return nil, err
}
instanceID := authz.GetInstance(ctx).InstanceID()
details, err := s.command.DeleteTarget(ctx, req.GetId(), instanceID)
deletedAt, err := s.command.DeleteTarget(ctx, req.GetId(), instanceID)
if err != nil {
return nil, err
}
var deletionDate *timestamppb.Timestamp
if !deletedAt.IsZero() {
deletionDate = timestamppb.New(deletedAt)
}
return &action.DeleteTargetResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
DeletionDate: deletionDate,
}, nil
}
func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget {
reqTarget := req.GetTarget()
var (
targetType domain.TargetType
interruptOnError bool
)
switch t := reqTarget.GetTargetType().(type) {
case *action.Target_RestWebhook:
switch t := req.GetTargetType().(type) {
case *action.CreateTargetRequest_RestWebhook:
targetType = domain.TargetTypeWebhook
interruptOnError = t.RestWebhook.InterruptOnError
case *action.Target_RestCall:
case *action.CreateTargetRequest_RestCall:
targetType = domain.TargetTypeCall
interruptOnError = t.RestCall.InterruptOnError
case *action.Target_RestAsync:
case *action.CreateTargetRequest_RestAsync:
targetType = domain.TargetTypeAsync
}
return &command.AddTarget{
Name: reqTarget.GetName(),
Name: req.GetName(),
TargetType: targetType,
Endpoint: reqTarget.GetEndpoint(),
Timeout: reqTarget.GetTimeout().AsDuration(),
Endpoint: req.GetEndpoint(),
Timeout: req.GetTimeout().AsDuration(),
InterruptOnError: interruptOnError,
}
}
func patchTargetToCommand(req *action.PatchTargetRequest) *command.ChangeTarget {
func updateTargetToCommand(req *action.UpdateTargetRequest) *command.ChangeTarget {
expirationSigningKey := false
// TODO handle expiration, currently only immediate expiration is supported
if req.GetTarget().GetExpirationSigningKey() != nil {
if req.GetExpirationSigningKey() != nil {
expirationSigningKey = true
}
reqTarget := req.GetTarget()
if reqTarget == nil {
if req == nil {
return nil
}
target := &command.ChangeTarget{
ObjectRoot: models.ObjectRoot{
AggregateID: req.GetId(),
},
Name: reqTarget.Name,
Endpoint: reqTarget.Endpoint,
Name: req.Name,
Endpoint: req.Endpoint,
ExpirationSigningKey: expirationSigningKey,
}
if reqTarget.TargetType != nil {
switch t := reqTarget.GetTargetType().(type) {
case *action.PatchTarget_RestWebhook:
if req.TargetType != nil {
switch t := req.GetTargetType().(type) {
case *action.UpdateTargetRequest_RestWebhook:
target.TargetType = gu.Ptr(domain.TargetTypeWebhook)
target.InterruptOnError = gu.Ptr(t.RestWebhook.InterruptOnError)
case *action.PatchTarget_RestCall:
case *action.UpdateTargetRequest_RestCall:
target.TargetType = gu.Ptr(domain.TargetTypeCall)
target.InterruptOnError = gu.Ptr(t.RestCall.InterruptOnError)
case *action.PatchTarget_RestAsync:
case *action.UpdateTargetRequest_RestAsync:
target.TargetType = gu.Ptr(domain.TargetTypeAsync)
target.InterruptOnError = gu.Ptr(false)
}
}
if reqTarget.Timeout != nil {
target.Timeout = gu.Ptr(reqTarget.GetTimeout().AsDuration())
if req.Timeout != nil {
target.Timeout = gu.Ptr(req.GetTimeout().AsDuration())
}
return target
}

View File

@@ -10,12 +10,12 @@ import (
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
)
func Test_createTargetToCommand(t *testing.T) {
type args struct {
req *action.Target
req *action.CreateTargetRequest
}
tests := []struct {
name string
@@ -34,11 +34,11 @@ func Test_createTargetToCommand(t *testing.T) {
},
{
name: "all fields (webhook)",
args: args{&action.Target{
args: args{&action.CreateTargetRequest{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{},
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{},
},
Timeout: durationpb.New(10 * time.Second),
}},
@@ -52,11 +52,11 @@ func Test_createTargetToCommand(t *testing.T) {
},
{
name: "all fields (async)",
args: args{&action.Target{
args: args{&action.CreateTargetRequest{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.Target_RestAsync{
RestAsync: &action.SetRESTAsync{},
TargetType: &action.CreateTargetRequest_RestAsync{
RestAsync: &action.RESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
}},
@@ -70,11 +70,11 @@ func Test_createTargetToCommand(t *testing.T) {
},
{
name: "all fields (interrupting response)",
args: args{&action.Target{
args: args{&action.CreateTargetRequest{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: true,
},
},
@@ -91,7 +91,7 @@ func Test_createTargetToCommand(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := createTargetToCommand(&action.CreateTargetRequest{Target: tt.args.req})
got := createTargetToCommand(tt.args.req)
assert.Equal(t, tt.want, got)
})
}
@@ -99,7 +99,7 @@ func Test_createTargetToCommand(t *testing.T) {
func Test_updateTargetToCommand(t *testing.T) {
type args struct {
req *action.PatchTarget
req *action.UpdateTargetRequest
}
tests := []struct {
name string
@@ -113,7 +113,7 @@ func Test_updateTargetToCommand(t *testing.T) {
},
{
name: "all fields nil",
args: args{&action.PatchTarget{
args: args{&action.UpdateTargetRequest{
Name: nil,
TargetType: nil,
Timeout: nil,
@@ -128,7 +128,7 @@ func Test_updateTargetToCommand(t *testing.T) {
},
{
name: "all fields empty",
args: args{&action.PatchTarget{
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr(""),
TargetType: nil,
Timeout: durationpb.New(0),
@@ -143,11 +143,11 @@ func Test_updateTargetToCommand(t *testing.T) {
},
{
name: "all fields (webhook)",
args: args{&action.PatchTarget{
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.PatchTarget_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
TargetType: &action.UpdateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: false,
},
},
@@ -163,11 +163,11 @@ func Test_updateTargetToCommand(t *testing.T) {
},
{
name: "all fields (webhook interrupt)",
args: args{&action.PatchTarget{
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.PatchTarget_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
TargetType: &action.UpdateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: true,
},
},
@@ -183,11 +183,11 @@ func Test_updateTargetToCommand(t *testing.T) {
},
{
name: "all fields (async)",
args: args{&action.PatchTarget{
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.PatchTarget_RestAsync{
RestAsync: &action.SetRESTAsync{},
TargetType: &action.UpdateTargetRequest_RestAsync{
RestAsync: &action.RESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
}},
@@ -201,11 +201,11 @@ func Test_updateTargetToCommand(t *testing.T) {
},
{
name: "all fields (interrupting response)",
args: args{&action.PatchTarget{
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.PatchTarget_RestCall{
RestCall: &action.SetRESTCall{
TargetType: &action.UpdateTargetRequest_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: true,
},
},
@@ -222,7 +222,7 @@ func Test_updateTargetToCommand(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := patchTargetToCommand(&action.PatchTargetRequest{Target: tt.args.req})
got := updateTargetToCommand(tt.args.req)
assert.Equal(t, tt.want, got)
})
}

View File

@@ -69,7 +69,6 @@ func (s *Server) ListMyUserChanges(ctx context.Context, req *auth_pb.ListMyUserC
}
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AllowTimeTravel().
Limit(limit).
OrderDesc().
AwaitOpenTransactions().

View File

@@ -158,14 +158,6 @@ func TestServer_GetSystemFeatures(t *testing.T) {
want *feature.GetSystemFeaturesResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: IamCTX,
req: &feature.GetSystemFeaturesRequest{},
},
wantErr: true,
},
{
name: "nothing set",
args: args{
@@ -349,14 +341,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) {
want *feature.GetInstanceFeaturesResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: OrgCTX,
req: &feature.GetInstanceFeaturesRequest{},
},
wantErr: true,
},
{
name: "defaults, no inheritance",
args: args{

View File

@@ -0,0 +1,56 @@
package filter
import (
"fmt"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta"
)
func TextMethodPbToQuery(method filter.TextFilterMethod) query.TextComparison {
switch method {
case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS:
return query.TextEquals
case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE:
return query.TextEqualsIgnoreCase
case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH:
return query.TextStartsWith
case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE:
return query.TextStartsWithIgnoreCase
case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS:
return query.TextContains
case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE:
return query.TextContainsIgnoreCase
case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH:
return query.TextEndsWith
case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE:
return query.TextEndsWithIgnoreCase
default:
return -1
}
}
func PaginationPbToQuery(defaults systemdefaults.SystemDefaults, query *filter.PaginationRequest) (offset, limit uint64, asc bool, err error) {
limit = defaults.DefaultQueryLimit
if query == nil {
return 0, limit, asc, nil
}
offset = query.Offset
asc = query.Asc
if defaults.MaxQueryLimit > 0 && uint64(query.Limit) > defaults.MaxQueryLimit {
return 0, 0, false, zerrors.ThrowInvalidArgumentf(fmt.Errorf("given: %d, allowed: %d", query.Limit, defaults.MaxQueryLimit), "QUERY-4M0fs", "Errors.Query.LimitExceeded")
}
if query.Limit > 0 {
limit = uint64(query.Limit)
}
return offset, limit, asc, nil
}
func QueryToPaginationPb(request query.SearchRequest, response query.SearchResponse) *filter.PaginationResponse {
return &filter.PaginationResponse{
AppliedLimit: request.Limit,
TotalResult: response.Count,
}
}

View File

@@ -50,7 +50,6 @@ func (s *Server) ListOrgChanges(ctx context.Context, req *mgmt_pb.ListOrgChanges
}
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AllowTimeTravel().
Limit(limit).
OrderDesc().
AwaitOpenTransactions().

View File

@@ -70,7 +70,6 @@ func (s *Server) ListProjectGrantChanges(ctx context.Context, req *mgmt_pb.ListP
}
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AllowTimeTravel().
Limit(limit).
OrderDesc().
ResourceOwner(authz.GetCtxData(ctx).OrgID).
@@ -152,7 +151,6 @@ func (s *Server) ListProjectChanges(ctx context.Context, req *mgmt_pb.ListProjec
}
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AllowTimeTravel().
Limit(limit).
AwaitOpenTransactions().
OrderDesc().

View File

@@ -52,7 +52,6 @@ func (s *Server) ListAppChanges(ctx context.Context, req *mgmt_pb.ListAppChanges
}
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AllowTimeTravel().
Limit(limit).
AwaitOpenTransactions().
OrderDesc().

View File

@@ -92,7 +92,6 @@ func (s *Server) ListUserChanges(ctx context.Context, req *mgmt_pb.ListUserChang
}
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AllowTimeTravel().
Limit(limit).
AwaitOpenTransactions().
OrderDesc().

View File

@@ -1,499 +0,0 @@
//go:build integration
package action_test
import (
"context"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
)
func TestServer_CreateTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
tests := []struct {
name string
ctx context.Context
req *action.Target
want *resource_object.Details
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.Target{
Name: gofakeit.Name(),
},
wantErr: true,
},
{
name: "empty name",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: "",
},
wantErr: true,
},
{
name: "empty type",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
TargetType: nil,
},
wantErr: true,
},
{
name: "empty webhook url",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{},
},
},
wantErr: true,
},
{
name: "empty request response url",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{},
},
},
wantErr: true,
},
{
name: "empty timeout",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{},
},
Timeout: nil,
},
wantErr: true,
},
{
name: "async, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.Target_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
{
name: "webhook, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
{
name: "webhook, interrupt on error, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
{
name: "call, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
{
name: "call, interruptOnError, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := instance.Client.ActionV3Alpha.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req})
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
assert.NotEmpty(t, got.GetSigningKey())
}
})
}
}
func TestServer_PatchTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
type args struct {
ctx context.Context
req *action.PatchTargetRequest
}
type want struct {
details *resource_object.Details
signingKey bool
}
tests := []struct {
name string
prepare func(request *action.PatchTargetRequest) error
args args
want want
wantErr bool
}{
{
name: "missing permission",
prepare: func(request *action.PatchTargetRequest) error {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Name: gu.Ptr(gofakeit.Name()),
},
},
},
wantErr: true,
},
{
name: "not existing",
prepare: func(request *action.PatchTargetRequest) error {
request.Id = "notexisting"
return nil
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Name: gu.Ptr(gofakeit.Name()),
},
},
},
wantErr: true,
},
{
name: "change name, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Name: gu.Ptr(gofakeit.Name()),
},
},
},
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
{
name: "regenerate signingkey, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
ExpirationSigningKey: durationpb.New(0 * time.Second),
},
},
},
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
signingKey: true,
},
},
{
name: "change type, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
TargetType: &action.PatchTarget_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
},
},
},
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
{
name: "change url, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Endpoint: gu.Ptr("https://example.com/hooks/new"),
},
},
},
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
{
name: "change timeout, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Timeout: durationpb.New(20 * time.Second),
},
},
},
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
{
name: "change type async, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
TargetType: &action.PatchTarget_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
},
},
},
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.prepare(tt.args.req)
require.NoError(t, err)
// We want to have the same response no matter how often we call the function
instance.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req)
got, err := instance.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.details, got.Details)
if tt.want.signingKey {
assert.NotEmpty(t, got.SigningKey)
}
}
})
}
}
func TestServer_DeleteTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
target := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.DeleteTargetRequest
want *resource_object.Details
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.DeleteTargetRequest{
Id: target.GetDetails().GetId(),
},
wantErr: true,
},
{
name: "empty id",
ctx: iamOwnerCtx,
req: &action.DeleteTargetRequest{
Id: "",
},
wantErr: true,
},
{
name: "delete target",
ctx: iamOwnerCtx,
req: &action.DeleteTargetRequest{
Id: target.GetDetails().GetId(),
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := instance.Client.ActionV3Alpha.DeleteTarget(tt.ctx, tt.req)
if tt.wantErr {
assert.Error(t, err)
return
} else {
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
}
})
}
}

View File

@@ -1,173 +0,0 @@
package webkey
import (
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
)
func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig {
switch config := req.GetKey().GetConfig().(type) {
case *webkey.WebKey_Rsa:
return webKeyRSAConfigToCrypto(config.Rsa)
case *webkey.WebKey_Ecdsa:
return webKeyECDSAConfigToCrypto(config.Ecdsa)
case *webkey.WebKey_Ed25519:
return new(crypto.WebKeyED25519Config)
default:
return webKeyRSAConfigToCrypto(nil)
}
}
func webKeyRSAConfigToCrypto(config *webkey.WebKeyRSAConfig) *crypto.WebKeyRSAConfig {
out := new(crypto.WebKeyRSAConfig)
switch config.GetBits() {
case webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED:
out.Bits = crypto.RSABits2048
case webkey.WebKeyRSAConfig_RSA_BITS_2048:
out.Bits = crypto.RSABits2048
case webkey.WebKeyRSAConfig_RSA_BITS_3072:
out.Bits = crypto.RSABits3072
case webkey.WebKeyRSAConfig_RSA_BITS_4096:
out.Bits = crypto.RSABits4096
default:
out.Bits = crypto.RSABits2048
}
switch config.GetHasher() {
case webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED:
out.Hasher = crypto.RSAHasherSHA256
case webkey.WebKeyRSAConfig_RSA_HASHER_SHA256:
out.Hasher = crypto.RSAHasherSHA256
case webkey.WebKeyRSAConfig_RSA_HASHER_SHA384:
out.Hasher = crypto.RSAHasherSHA384
case webkey.WebKeyRSAConfig_RSA_HASHER_SHA512:
out.Hasher = crypto.RSAHasherSHA512
default:
out.Hasher = crypto.RSAHasherSHA256
}
return out
}
func webKeyECDSAConfigToCrypto(config *webkey.WebKeyECDSAConfig) *crypto.WebKeyECDSAConfig {
out := new(crypto.WebKeyECDSAConfig)
switch config.GetCurve() {
case webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED:
out.Curve = crypto.EllipticCurveP256
case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256:
out.Curve = crypto.EllipticCurveP256
case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384:
out.Curve = crypto.EllipticCurveP384
case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512:
out.Curve = crypto.EllipticCurveP512
default:
out.Curve = crypto.EllipticCurveP256
}
return out
}
func webKeyDetailsListToPb(list []query.WebKeyDetails, instanceID string) []*webkey.GetWebKey {
out := make([]*webkey.GetWebKey, len(list))
for i := range list {
out[i] = webKeyDetailsToPb(&list[i], instanceID)
}
return out
}
func webKeyDetailsToPb(details *query.WebKeyDetails, instanceID string) *webkey.GetWebKey {
out := &webkey.GetWebKey{
Details: resource_object.DomainToDetailsPb(&domain.ObjectDetails{
ID: details.KeyID,
CreationDate: details.CreationDate,
EventDate: details.ChangeDate,
}, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
State: webKeyStateToPb(details.State),
Config: &webkey.WebKey{},
}
switch config := details.Config.(type) {
case *crypto.WebKeyRSAConfig:
out.Config.Config = &webkey.WebKey_Rsa{
Rsa: webKeyRSAConfigToPb(config),
}
case *crypto.WebKeyECDSAConfig:
out.Config.Config = &webkey.WebKey_Ecdsa{
Ecdsa: webKeyECDSAConfigToPb(config),
}
case *crypto.WebKeyED25519Config:
out.Config.Config = &webkey.WebKey_Ed25519{
Ed25519: new(webkey.WebKeyED25519Config),
}
}
return out
}
func webKeyStateToPb(state domain.WebKeyState) webkey.WebKeyState {
switch state {
case domain.WebKeyStateUnspecified:
return webkey.WebKeyState_STATE_UNSPECIFIED
case domain.WebKeyStateInitial:
return webkey.WebKeyState_STATE_INITIAL
case domain.WebKeyStateActive:
return webkey.WebKeyState_STATE_ACTIVE
case domain.WebKeyStateInactive:
return webkey.WebKeyState_STATE_INACTIVE
case domain.WebKeyStateRemoved:
return webkey.WebKeyState_STATE_REMOVED
default:
return webkey.WebKeyState_STATE_UNSPECIFIED
}
}
func webKeyRSAConfigToPb(config *crypto.WebKeyRSAConfig) *webkey.WebKeyRSAConfig {
out := new(webkey.WebKeyRSAConfig)
switch config.Bits {
case crypto.RSABitsUnspecified:
out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED
case crypto.RSABits2048:
out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_2048
case crypto.RSABits3072:
out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_3072
case crypto.RSABits4096:
out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_4096
}
switch config.Hasher {
case crypto.RSAHasherUnspecified:
out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED
case crypto.RSAHasherSHA256:
out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA256
case crypto.RSAHasherSHA384:
out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA384
case crypto.RSAHasherSHA512:
out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA512
}
return out
}
func webKeyECDSAConfigToPb(config *crypto.WebKeyECDSAConfig) *webkey.WebKeyECDSAConfig {
out := new(webkey.WebKeyECDSAConfig)
switch config.Curve {
case crypto.EllipticCurveUnspecified:
out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED
case crypto.EllipticCurveP256:
out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256
case crypto.EllipticCurveP384:
out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384
case crypto.EllipticCurveP512:
out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512
}
return out
}

View File

@@ -13,13 +13,13 @@ import (
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
func AuthorizationInterceptor(verifier authz.APITokenVerifier, authConfig authz.Config) grpc.UnaryServerInterceptor {
func AuthorizationInterceptor(verifier authz.APITokenVerifier, systemUserPermissions authz.Config, authConfig authz.Config) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
return authorize(ctx, req, info, handler, verifier, authConfig)
return authorize(ctx, req, info, handler, verifier, systemUserPermissions, authConfig)
}
}
func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.APITokenVerifier, authConfig authz.Config) (_ interface{}, err error) {
func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.APITokenVerifier, systemUserPermissions authz.Config, authConfig authz.Config) (_ interface{}, err error) {
authOpt, needsToken := verifier.CheckAuthMethod(info.FullMethod)
if !needsToken {
return handler(ctx, req)
@@ -34,7 +34,7 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
}
orgID, orgDomain := orgIDAndDomainFromRequest(authCtx, req)
ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, orgDomain, verifier, authConfig, authOpt, info.FullMethod)
ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, orgDomain, verifier, systemUserPermissions.RolePermissionMappings, authConfig.RolePermissionMappings, authOpt, info.FullMethod)
if err != nil {
return nil, err
}

View File

@@ -20,6 +20,7 @@ type authzRepoMock struct{}
func (v *authzRepoMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) {
return "", "", "", "", "", nil
}
func (v *authzRepoMock) SearchMyMemberships(ctx context.Context, orgID string, _ bool) ([]*authz.Membership, error) {
return authz.Memberships{{
MemberType: authz.MemberTypeOrganization,
@@ -31,9 +32,11 @@ func (v *authzRepoMock) SearchMyMemberships(ctx context.Context, orgID string, _
func (v *authzRepoMock) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) {
return "", nil, nil
}
func (v *authzRepoMock) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) {
return orgID, nil
}
func (v *authzRepoMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) {
return "", "", nil
}
@@ -252,7 +255,7 @@ func Test_authorize(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := authorize(tt.args.ctx, tt.args.req, tt.args.info, tt.args.handler, tt.args.verifier(), tt.args.authConfig)
got, err := authorize(tt.args.ctx, tt.args.req, tt.args.info, tt.args.handler, tt.args.verifier(), tt.args.authConfig, tt.args.authConfig)
if (err != nil) != tt.res.wantErr {
t.Errorf("authorize() error = %v, wantErr %v", err, tt.res.wantErr)
return

View File

@@ -36,6 +36,7 @@ type WithGatewayPrefix interface {
func CreateServer(
verifier authz.APITokenVerifier,
systemAuthz authz.Config,
authConfig authz.Config,
queries *query.Queries,
externalDomain string,
@@ -53,7 +54,7 @@ func CreateServer(
middleware.AccessStorageInterceptor(accessSvc),
middleware.ErrorHandler(),
middleware.LimitsInterceptor(system_pb.SystemService_ServiceDesc.ServiceName),
middleware.AuthorizationInterceptor(verifier, authConfig),
middleware.AuthorizationInterceptor(verifier, systemAuthz, authConfig),
middleware.TranslationHandler(),
middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_ServiceDesc.ServiceName),
middleware.ExecutionHandler(queries),

View File

@@ -73,6 +73,7 @@ func requireEventually(
assertCounts func(assert.TestingT, *eventCounts),
msg string,
) (counts *eventCounts) {
t.Helper()
countTimeout := 30 * time.Second
assertTimeout := countTimeout + time.Second
countCtx, cancel := context.WithTimeout(ctx, time.Minute)

View File

@@ -415,6 +415,10 @@ func createUsers(ctx context.Context, orgID string, count int, passwordChangeReq
func createUser(ctx context.Context, orgID string, passwordChangeRequired bool) userAttr {
username := gofakeit.Email()
return createUserWithUserName(ctx, username, orgID, passwordChangeRequired)
}
func createUserWithUserName(ctx context.Context, username string, orgID string, passwordChangeRequired bool) userAttr {
// used as default country prefix
phone := "+41" + gofakeit.Phone()
resp := Instance.CreateHumanUserVerified(ctx, orgID, username, phone)
@@ -1179,6 +1183,97 @@ func TestServer_ListUsers(t *testing.T) {
}
}
func TestServer_SystemUsers_ListUsers(t *testing.T) {
defer func() {
_, err := Instance.Client.FeatureV2.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{})
require.NoError(t, err)
}()
org1 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), gofakeit.Email())
org2 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), "org2@zitadel.com")
org3 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), gofakeit.Email())
_ = createUserWithUserName(IamCTX, "Test_SystemUsers_ListUser1@zitadel.com", org1.OrganizationId, false)
_ = createUserWithUserName(IamCTX, "Test_SystemUsers_ListUser2@zitadel.com", org2.OrganizationId, false)
_ = createUserWithUserName(IamCTX, "Test_SystemUsers_ListUser3@zitadel.com", org3.OrganizationId, false)
tests := []struct {
name string
ctx context.Context
req *user.ListUsersRequest
expectedFoundUsernames []string
checkNumberOfUsersReturned bool
}{
{
name: "list users with neccessary permissions",
ctx: SystemCTX,
req: &user.ListUsersRequest{},
// the number of users returned will vary from test run to test run,
// so just check the system user gets back users from different orgs whcih it is not a memeber of
checkNumberOfUsersReturned: false,
expectedFoundUsernames: []string{"Test_SystemUsers_ListUser1@zitadel.com", "Test_SystemUsers_ListUser2@zitadel.com", "Test_SystemUsers_ListUser3@zitadel.com"},
},
{
name: "list users without neccessary permissions",
ctx: SystemUserWithNoPermissionsCTX,
req: &user.ListUsersRequest{},
// check no users returned
checkNumberOfUsersReturned: true,
},
{
name: "list users with neccessary permissions specifying org",
req: &user.ListUsersRequest{
Queries: []*user.SearchQuery{OrganizationIdQuery(org2.OrganizationId)},
},
ctx: SystemCTX,
expectedFoundUsernames: []string{"Test_SystemUsers_ListUser2@zitadel.com", "org2@zitadel.com"},
checkNumberOfUsersReturned: true,
},
{
name: "list users without neccessary permissions specifying org",
req: &user.ListUsersRequest{
Queries: []*user.SearchQuery{OrganizationIdQuery(org2.OrganizationId)},
},
ctx: SystemUserWithNoPermissionsCTX,
// check no users returned
checkNumberOfUsersReturned: true,
},
}
for _, f := range permissionCheckV2Settings {
f := f
for _, tt := range tests {
t.Run(f.TestNamePrependString+tt.name, func(t *testing.T) {
setPermissionCheckV2Flag(t, f.SetFlag)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, 1*time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, err := Client.ListUsers(tt.ctx, tt.req)
require.NoError(ttt, err)
if tt.checkNumberOfUsersReturned {
require.Equal(t, len(tt.expectedFoundUsernames), len(got.Result))
}
if tt.expectedFoundUsernames != nil {
for _, user := range got.Result {
for i, username := range tt.expectedFoundUsernames {
if username == user.Username {
tt.expectedFoundUsernames = tt.expectedFoundUsernames[i+1:]
break
}
}
if len(tt.expectedFoundUsernames) == 0 {
return
}
}
require.FailNow(t, "unable to find all users with specified usernames")
}
}, retryDuration, tick, "timeout waiting for expected user result")
})
}
}
}
func InUserIDsQuery(ids []string) *user.SearchQuery {
return &user.SearchQuery{
Query: &user.SearchQuery_InUserIdsQuery{

View File

@@ -31,12 +31,13 @@ import (
)
var (
CTX context.Context
IamCTX context.Context
UserCTX context.Context
SystemCTX context.Context
Instance *integration.Instance
Client user.UserServiceClient
CTX context.Context
IamCTX context.Context
UserCTX context.Context
SystemCTX context.Context
SystemUserWithNoPermissionsCTX context.Context
Instance *integration.Instance
Client user.UserServiceClient
)
func TestMain(m *testing.M) {
@@ -46,6 +47,7 @@ func TestMain(m *testing.M) {
Instance = integration.NewInstance(ctx)
SystemUserWithNoPermissionsCTX = integration.WithSystemUserWithNoPermissionsAuthorization(ctx)
UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeNoPermission)
IamCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
SystemCTX = integration.WithSystemAuthorization(ctx)
@@ -1306,7 +1308,6 @@ func TestServer_UpdateHumanUser_Permission(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.UpdateHumanUser(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
@@ -3048,7 +3049,6 @@ func TestServer_ListAuthenticationFactors(t *testing.T) {
assert.ElementsMatch(t, tt.want.GetResult(), got.GetResult())
}, retryDuration, tick, "timeout waiting for expected auth methods result")
})
}
}

View File

@@ -17,9 +17,7 @@ import (
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
)
var (
@@ -37,7 +35,7 @@ func TestMain(m *testing.M) {
func TestServer_Feature_Disabled(t *testing.T) {
instance, iamCtx, _ := createInstance(t, false)
client := instance.Client.WebKeyV3Alpha
client := instance.Client.WebKeyV2Beta
t.Run("CreateWebKey", func(t *testing.T) {
_, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{})
@@ -65,84 +63,78 @@ func TestServer_ListWebKeys(t *testing.T) {
instance, iamCtx, creationDate := createInstance(t, true)
// After the feature is first enabled, we can expect 2 generated keys with the default config.
checkWebKeyListState(iamCtx, t, instance, 2, "", &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
Rsa: &webkey.RSA{
Bits: webkey.RSABits_RSA_BITS_2048,
Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
},
}, creationDate)
}
func TestServer_CreateWebKey(t *testing.T) {
instance, iamCtx, creationDate := createInstance(t, true)
client := instance.Client.WebKeyV3Alpha
client := instance.Client.WebKeyV2Beta
_, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
Key: &webkey.CreateWebKeyRequest_Rsa{
Rsa: &webkey.RSA{
Bits: webkey.RSABits_RSA_BITS_2048,
Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
},
},
})
require.NoError(t, err)
checkWebKeyListState(iamCtx, t, instance, 3, "", &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
Rsa: &webkey.RSA{
Bits: webkey.RSABits_RSA_BITS_2048,
Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
},
}, creationDate)
}
func TestServer_ActivateWebKey(t *testing.T) {
instance, iamCtx, creationDate := createInstance(t, true)
client := instance.Client.WebKeyV3Alpha
client := instance.Client.WebKeyV2Beta
resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
Key: &webkey.CreateWebKeyRequest_Rsa{
Rsa: &webkey.RSA{
Bits: webkey.RSABits_RSA_BITS_2048,
Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
},
},
})
require.NoError(t, err)
_, err = client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{
Id: resp.GetDetails().GetId(),
Id: resp.GetId(),
})
require.NoError(t, err)
checkWebKeyListState(iamCtx, t, instance, 3, resp.GetDetails().GetId(), &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
checkWebKeyListState(iamCtx, t, instance, 3, resp.GetId(), &webkey.WebKey_Rsa{
Rsa: &webkey.RSA{
Bits: webkey.RSABits_RSA_BITS_2048,
Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
},
}, creationDate)
}
func TestServer_DeleteWebKey(t *testing.T) {
instance, iamCtx, creationDate := createInstance(t, true)
client := instance.Client.WebKeyV3Alpha
client := instance.Client.WebKeyV2Beta
keyIDs := make([]string, 2)
for i := 0; i < 2; i++ {
resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
Key: &webkey.CreateWebKeyRequest_Rsa{
Rsa: &webkey.RSA{
Bits: webkey.RSABits_RSA_BITS_2048,
Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
},
},
})
require.NoError(t, err)
keyIDs[i] = resp.GetDetails().GetId()
keyIDs[i] = resp.GetId()
}
_, err := client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{
Id: keyIDs[0],
@@ -162,11 +154,35 @@ func TestServer_DeleteWebKey(t *testing.T) {
return
}
start := time.Now()
ok = t.Run("delete inactive key", func(t *testing.T) {
_, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{
resp, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{
Id: keyIDs[1],
})
require.NoError(t, err)
require.WithinRange(t, resp.GetDeletionDate().AsTime(), start, time.Now())
})
if !ok {
return
}
ok = t.Run("delete inactive key again", func(t *testing.T) {
resp, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{
Id: keyIDs[1],
})
require.NoError(t, err)
require.WithinRange(t, resp.GetDeletionDate().AsTime(), start, time.Now())
})
if !ok {
return
}
ok = t.Run("delete not existing key", func(t *testing.T) {
resp, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{
Id: "not-existing",
})
require.NoError(t, err)
require.Nil(t, resp.DeletionDate)
})
if !ok {
return
@@ -174,9 +190,9 @@ func TestServer_DeleteWebKey(t *testing.T) {
// There are 2 keys from feature setup, +2 created, -1 deleted = 3
checkWebKeyListState(iamCtx, t, instance, 3, keyIDs[0], &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
Rsa: &webkey.RSA{
Bits: webkey.RSABits_RSA_BITS_2048,
Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
},
}, creationDate)
}
@@ -195,7 +211,7 @@ func createInstance(t *testing.T, enableFeature bool) (*integration.Instance, co
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamCTX, time.Minute)
assert.EventuallyWithT(t, func(collect *assert.CollectT) {
resp, err := instance.Client.WebKeyV3Alpha.ListWebKeys(iamCTX, &webkey.ListWebKeysRequest{})
resp, err := instance.Client.WebKeyV2Beta.ListWebKeys(iamCTX, &webkey.ListWebKeysRequest{})
if enableFeature {
assert.NoError(collect, err)
assert.Len(collect, resp.GetWebKeys(), 2)
@@ -220,7 +236,7 @@ func checkWebKeyListState(ctx context.Context, t *testing.T, instance *integrati
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
assert.EventuallyWithT(t, func(collect *assert.CollectT) {
resp, err := instance.Client.WebKeyV3Alpha.ListWebKeys(ctx, &webkey.ListWebKeysRequest{})
resp, err := instance.Client.WebKeyV2Beta.ListWebKeys(ctx, &webkey.ListWebKeysRequest{})
require.NoError(collect, err)
list := resp.GetWebKeys()
assert.Len(collect, list, nKeys)
@@ -228,21 +244,14 @@ func checkWebKeyListState(ctx context.Context, t *testing.T, instance *integrati
now := time.Now()
var gotActiveKeyID string
for _, key := range list {
integration.AssertResourceDetails(t, &resource_object.Details{
Created: creationDate,
Changed: creationDate,
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
}, key.GetDetails())
assert.WithinRange(collect, key.GetDetails().GetChanged().AsTime(), now.Add(-time.Minute), now.Add(time.Minute))
assert.NotEqual(collect, webkey.WebKeyState_STATE_UNSPECIFIED, key.GetState())
assert.NotEqual(collect, webkey.WebKeyState_STATE_REMOVED, key.GetState())
assert.Equal(collect, config, key.GetConfig().GetConfig())
assert.WithinRange(collect, key.GetCreationDate().AsTime(), now.Add(-time.Minute), now.Add(time.Minute))
assert.WithinRange(collect, key.GetChangeDate().AsTime(), now.Add(-time.Minute), now.Add(time.Minute))
assert.NotEqual(collect, webkey.State_STATE_UNSPECIFIED, key.GetState())
assert.NotEqual(collect, webkey.State_STATE_REMOVED, key.GetState())
assert.Equal(collect, config, key.GetKey())
if key.GetState() == webkey.WebKeyState_STATE_ACTIVE {
gotActiveKeyID = key.GetDetails().GetId()
if key.GetState() == webkey.State_STATE_ACTIVE {
gotActiveKeyID = key.GetId()
}
}
assert.NotEmpty(collect, gotActiveKeyID)

View File

@@ -7,11 +7,11 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
)
type Server struct {
webkey.UnimplementedZITADELWebKeysServer
webkey.UnimplementedWebKeyServiceServer
command *command.Commands
query *query.Queries
}
@@ -27,21 +27,21 @@ func CreateServer(
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
webkey.RegisterZITADELWebKeysServer(grpcServer, s)
webkey.RegisterWebKeyServiceServer(grpcServer, s)
}
func (s *Server) AppName() string {
return webkey.ZITADELWebKeys_ServiceDesc.ServiceName
return webkey.WebKeyService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return webkey.ZITADELWebKeys_ServiceDesc.ServiceName
return webkey.WebKeyService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return webkey.ZITADELWebKeys_AuthMethods
return webkey.WebKeyService_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return webkey.RegisterZITADELWebKeysHandler
return webkey.RegisterWebKeyServiceHandler
}

View File

@@ -3,12 +3,12 @@ package webkey
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
)
func (s *Server) CreateWebKey(ctx context.Context, req *webkey.CreateWebKeyRequest) (_ *webkey.CreateWebKeyResponse, err error) {
@@ -24,7 +24,8 @@ func (s *Server) CreateWebKey(ctx context.Context, req *webkey.CreateWebKeyReque
}
return &webkey.CreateWebKeyResponse{
Details: resource_object.DomainToDetailsPb(webKey.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()),
Id: webKey.KeyID,
CreationDate: timestamppb.New(webKey.ObjectDetails.EventDate),
}, nil
}
@@ -41,7 +42,7 @@ func (s *Server) ActivateWebKey(ctx context.Context, req *webkey.ActivateWebKeyR
}
return &webkey.ActivateWebKeyResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()),
ChangeDate: timestamppb.New(details.EventDate),
}, nil
}
@@ -52,13 +53,17 @@ func (s *Server) DeleteWebKey(ctx context.Context, req *webkey.DeleteWebKeyReque
if err = checkWebKeyFeature(ctx); err != nil {
return nil, err
}
details, err := s.command.DeleteWebKey(ctx, req.GetId())
deletedAt, err := s.command.DeleteWebKey(ctx, req.GetId())
if err != nil {
return nil, err
}
var deletionDate *timestamppb.Timestamp
if !deletedAt.IsZero() {
deletionDate = timestamppb.New(deletedAt)
}
return &webkey.DeleteWebKeyResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()),
DeletionDate: deletionDate,
}, nil
}
@@ -75,7 +80,7 @@ func (s *Server) ListWebKeys(ctx context.Context, _ *webkey.ListWebKeysRequest)
}
return &webkey.ListWebKeysResponse{
WebKeys: webKeyDetailsListToPb(list, authz.GetInstance(ctx).InstanceID()),
WebKeys: webKeyDetailsListToPb(list),
}, nil
}

View File

@@ -0,0 +1,170 @@
package webkey
import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
)
func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig {
switch config := req.GetKey().(type) {
case *webkey.CreateWebKeyRequest_Rsa:
return rsaToCrypto(config.Rsa)
case *webkey.CreateWebKeyRequest_Ecdsa:
return ecdsaToCrypto(config.Ecdsa)
case *webkey.CreateWebKeyRequest_Ed25519:
return new(crypto.WebKeyED25519Config)
default:
return rsaToCrypto(nil)
}
}
func rsaToCrypto(config *webkey.RSA) *crypto.WebKeyRSAConfig {
out := new(crypto.WebKeyRSAConfig)
switch config.GetBits() {
case webkey.RSABits_RSA_BITS_UNSPECIFIED:
out.Bits = crypto.RSABits2048
case webkey.RSABits_RSA_BITS_2048:
out.Bits = crypto.RSABits2048
case webkey.RSABits_RSA_BITS_3072:
out.Bits = crypto.RSABits3072
case webkey.RSABits_RSA_BITS_4096:
out.Bits = crypto.RSABits4096
default:
out.Bits = crypto.RSABits2048
}
switch config.GetHasher() {
case webkey.RSAHasher_RSA_HASHER_UNSPECIFIED:
out.Hasher = crypto.RSAHasherSHA256
case webkey.RSAHasher_RSA_HASHER_SHA256:
out.Hasher = crypto.RSAHasherSHA256
case webkey.RSAHasher_RSA_HASHER_SHA384:
out.Hasher = crypto.RSAHasherSHA384
case webkey.RSAHasher_RSA_HASHER_SHA512:
out.Hasher = crypto.RSAHasherSHA512
default:
out.Hasher = crypto.RSAHasherSHA256
}
return out
}
func ecdsaToCrypto(config *webkey.ECDSA) *crypto.WebKeyECDSAConfig {
out := new(crypto.WebKeyECDSAConfig)
switch config.GetCurve() {
case webkey.ECDSACurve_ECDSA_CURVE_UNSPECIFIED:
out.Curve = crypto.EllipticCurveP256
case webkey.ECDSACurve_ECDSA_CURVE_P256:
out.Curve = crypto.EllipticCurveP256
case webkey.ECDSACurve_ECDSA_CURVE_P384:
out.Curve = crypto.EllipticCurveP384
case webkey.ECDSACurve_ECDSA_CURVE_P512:
out.Curve = crypto.EllipticCurveP512
default:
out.Curve = crypto.EllipticCurveP256
}
return out
}
func webKeyDetailsListToPb(list []query.WebKeyDetails) []*webkey.WebKey {
out := make([]*webkey.WebKey, len(list))
for i := range list {
out[i] = webKeyDetailsToPb(&list[i])
}
return out
}
func webKeyDetailsToPb(details *query.WebKeyDetails) *webkey.WebKey {
out := &webkey.WebKey{
Id: details.KeyID,
CreationDate: timestamppb.New(details.CreationDate),
ChangeDate: timestamppb.New(details.ChangeDate),
State: webKeyStateToPb(details.State),
}
switch config := details.Config.(type) {
case *crypto.WebKeyRSAConfig:
out.Key = &webkey.WebKey_Rsa{
Rsa: webKeyRSAConfigToPb(config),
}
case *crypto.WebKeyECDSAConfig:
out.Key = &webkey.WebKey_Ecdsa{
Ecdsa: webKeyECDSAConfigToPb(config),
}
case *crypto.WebKeyED25519Config:
out.Key = &webkey.WebKey_Ed25519{
Ed25519: new(webkey.ED25519),
}
}
return out
}
func webKeyStateToPb(state domain.WebKeyState) webkey.State {
switch state {
case domain.WebKeyStateUnspecified:
return webkey.State_STATE_UNSPECIFIED
case domain.WebKeyStateInitial:
return webkey.State_STATE_INITIAL
case domain.WebKeyStateActive:
return webkey.State_STATE_ACTIVE
case domain.WebKeyStateInactive:
return webkey.State_STATE_INACTIVE
case domain.WebKeyStateRemoved:
return webkey.State_STATE_REMOVED
default:
return webkey.State_STATE_UNSPECIFIED
}
}
func webKeyRSAConfigToPb(config *crypto.WebKeyRSAConfig) *webkey.RSA {
out := new(webkey.RSA)
switch config.Bits {
case crypto.RSABitsUnspecified:
out.Bits = webkey.RSABits_RSA_BITS_UNSPECIFIED
case crypto.RSABits2048:
out.Bits = webkey.RSABits_RSA_BITS_2048
case crypto.RSABits3072:
out.Bits = webkey.RSABits_RSA_BITS_3072
case crypto.RSABits4096:
out.Bits = webkey.RSABits_RSA_BITS_4096
}
switch config.Hasher {
case crypto.RSAHasherUnspecified:
out.Hasher = webkey.RSAHasher_RSA_HASHER_UNSPECIFIED
case crypto.RSAHasherSHA256:
out.Hasher = webkey.RSAHasher_RSA_HASHER_SHA256
case crypto.RSAHasherSHA384:
out.Hasher = webkey.RSAHasher_RSA_HASHER_SHA384
case crypto.RSAHasherSHA512:
out.Hasher = webkey.RSAHasher_RSA_HASHER_SHA512
}
return out
}
func webKeyECDSAConfigToPb(config *crypto.WebKeyECDSAConfig) *webkey.ECDSA {
out := new(webkey.ECDSA)
switch config.Curve {
case crypto.EllipticCurveUnspecified:
out.Curve = webkey.ECDSACurve_ECDSA_CURVE_UNSPECIFIED
case crypto.EllipticCurveP256:
out.Curve = webkey.ECDSACurve_ECDSA_CURVE_P256
case crypto.EllipticCurveP384:
out.Curve = webkey.ECDSACurve_ECDSA_CURVE_P384
case crypto.EllipticCurveP512:
out.Curve = webkey.ECDSACurve_ECDSA_CURVE_P512
}
return out
}

View File

@@ -10,9 +10,7 @@ import (
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
)
func Test_createWebKeyRequestToConfig(t *testing.T) {
@@ -27,12 +25,10 @@ func Test_createWebKeyRequestToConfig(t *testing.T) {
{
name: "RSA",
args: args{&webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
},
Key: &webkey.CreateWebKeyRequest_Rsa{
Rsa: &webkey.RSA{
Bits: webkey.RSABits_RSA_BITS_3072,
Hasher: webkey.RSAHasher_RSA_HASHER_SHA384,
},
},
}},
@@ -44,11 +40,9 @@ func Test_createWebKeyRequestToConfig(t *testing.T) {
{
name: "ECDSA",
args: args{&webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Ecdsa{
Ecdsa: &webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
},
Key: &webkey.CreateWebKeyRequest_Ecdsa{
Ecdsa: &webkey.ECDSA{
Curve: webkey.ECDSACurve_ECDSA_CURVE_P384,
},
},
}},
@@ -59,10 +53,8 @@ func Test_createWebKeyRequestToConfig(t *testing.T) {
{
name: "ED25519",
args: args{&webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Ed25519{
Ed25519: &webkey.WebKeyED25519Config{},
},
Key: &webkey.CreateWebKeyRequest_Ed25519{
Ed25519: &webkey.ED25519{},
},
}},
want: &crypto.WebKeyED25519Config{},
@@ -86,7 +78,7 @@ func Test_createWebKeyRequestToConfig(t *testing.T) {
func Test_webKeyRSAConfigToCrypto(t *testing.T) {
type args struct {
config *webkey.WebKeyRSAConfig
config *webkey.RSA
}
tests := []struct {
name string
@@ -95,9 +87,9 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) {
}{
{
name: "unspecified",
args: args{&webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED,
args: args{&webkey.RSA{
Bits: webkey.RSABits_RSA_BITS_UNSPECIFIED,
Hasher: webkey.RSAHasher_RSA_HASHER_UNSPECIFIED,
}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits2048,
@@ -106,9 +98,9 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) {
},
{
name: "2048, RSA256",
args: args{&webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
args: args{&webkey.RSA{
Bits: webkey.RSABits_RSA_BITS_2048,
Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits2048,
@@ -117,9 +109,9 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) {
},
{
name: "3072, RSA384",
args: args{&webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
args: args{&webkey.RSA{
Bits: webkey.RSABits_RSA_BITS_3072,
Hasher: webkey.RSAHasher_RSA_HASHER_SHA384,
}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits3072,
@@ -128,9 +120,9 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) {
},
{
name: "4096, RSA512",
args: args{&webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_4096,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA512,
args: args{&webkey.RSA{
Bits: webkey.RSABits_RSA_BITS_4096,
Hasher: webkey.RSAHasher_RSA_HASHER_SHA512,
}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits4096,
@@ -139,7 +131,7 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) {
},
{
name: "invalid",
args: args{&webkey.WebKeyRSAConfig{
args: args{&webkey.RSA{
Bits: 99,
Hasher: 99,
}},
@@ -151,7 +143,7 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := webKeyRSAConfigToCrypto(tt.args.config)
got := rsaToCrypto(tt.args.config)
assert.Equal(t, tt.want, got)
})
}
@@ -159,7 +151,7 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) {
func Test_webKeyECDSAConfigToCrypto(t *testing.T) {
type args struct {
config *webkey.WebKeyECDSAConfig
config *webkey.ECDSA
}
tests := []struct {
name string
@@ -168,8 +160,8 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) {
}{
{
name: "unspecified",
args: args{&webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED,
args: args{&webkey.ECDSA{
Curve: webkey.ECDSACurve_ECDSA_CURVE_UNSPECIFIED,
}},
want: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP256,
@@ -177,8 +169,8 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) {
},
{
name: "P256",
args: args{&webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256,
args: args{&webkey.ECDSA{
Curve: webkey.ECDSACurve_ECDSA_CURVE_P256,
}},
want: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP256,
@@ -186,8 +178,8 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) {
},
{
name: "P384",
args: args{&webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
args: args{&webkey.ECDSA{
Curve: webkey.ECDSACurve_ECDSA_CURVE_P384,
}},
want: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
@@ -195,8 +187,8 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) {
},
{
name: "P512",
args: args{&webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512,
args: args{&webkey.ECDSA{
Curve: webkey.ECDSACurve_ECDSA_CURVE_P512,
}},
want: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP512,
@@ -204,7 +196,7 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) {
},
{
name: "invalid",
args: args{&webkey.WebKeyECDSAConfig{
args: args{&webkey.ECDSA{
Curve: 99,
}},
want: &crypto.WebKeyECDSAConfig{
@@ -214,14 +206,13 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := webKeyECDSAConfigToCrypto(tt.args.config)
got := ecdsaToCrypto(tt.args.config)
assert.Equal(t, tt.want, got)
})
}
}
func Test_webKeyDetailsListToPb(t *testing.T) {
instanceID := "ownerid"
list := []query.WebKeyDetails{
{
KeyID: "key1",
@@ -243,52 +234,41 @@ func Test_webKeyDetailsListToPb(t *testing.T) {
Config: &crypto.WebKeyED25519Config{},
},
}
want := []*webkey.GetWebKey{
want := []*webkey.WebKey{
{
Details: &resource_object.Details{
Id: "key1",
Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
State: webkey.WebKeyState_STATE_ACTIVE,
Config: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
},
Id: "key1",
CreationDate: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
ChangeDate: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
State: webkey.State_STATE_ACTIVE,
Key: &webkey.WebKey_Rsa{
Rsa: &webkey.RSA{
Bits: webkey.RSABits_RSA_BITS_3072,
Hasher: webkey.RSAHasher_RSA_HASHER_SHA384,
},
},
},
{
Details: &resource_object.Details{
Id: "key2",
Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
State: webkey.WebKeyState_STATE_ACTIVE,
Config: &webkey.WebKey{
Config: &webkey.WebKey_Ed25519{
Ed25519: &webkey.WebKeyED25519Config{},
},
Id: "key2",
CreationDate: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
ChangeDate: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
State: webkey.State_STATE_ACTIVE,
Key: &webkey.WebKey_Ed25519{
Ed25519: &webkey.ED25519{},
},
},
}
got := webKeyDetailsListToPb(list, instanceID)
got := webKeyDetailsListToPb(list)
assert.Equal(t, want, got)
}
func Test_webKeyDetailsToPb(t *testing.T) {
instanceID := "ownerid"
type args struct {
details *query.WebKeyDetails
}
tests := []struct {
name string
args args
want *webkey.GetWebKey
want *webkey.WebKey
}{
{
name: "RSA",
@@ -303,20 +283,15 @@ func Test_webKeyDetailsToPb(t *testing.T) {
Hasher: crypto.RSAHasherSHA384,
},
}},
want: &webkey.GetWebKey{
Details: &resource_object.Details{
Id: "keyID",
Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
State: webkey.WebKeyState_STATE_ACTIVE,
Config: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
},
want: &webkey.WebKey{
Id: "keyID",
CreationDate: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
ChangeDate: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
State: webkey.State_STATE_ACTIVE,
Key: &webkey.WebKey_Rsa{
Rsa: &webkey.RSA{
Bits: webkey.RSABits_RSA_BITS_3072,
Hasher: webkey.RSAHasher_RSA_HASHER_SHA384,
},
},
},
@@ -333,19 +308,14 @@ func Test_webKeyDetailsToPb(t *testing.T) {
Curve: crypto.EllipticCurveP384,
},
}},
want: &webkey.GetWebKey{
Details: &resource_object.Details{
Id: "keyID",
Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
State: webkey.WebKeyState_STATE_ACTIVE,
Config: &webkey.WebKey{
Config: &webkey.WebKey_Ecdsa{
Ecdsa: &webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
},
want: &webkey.WebKey{
Id: "keyID",
CreationDate: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
ChangeDate: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
State: webkey.State_STATE_ACTIVE,
Key: &webkey.WebKey_Ecdsa{
Ecdsa: &webkey.ECDSA{
Curve: webkey.ECDSACurve_ECDSA_CURVE_P384,
},
},
},
@@ -360,25 +330,20 @@ func Test_webKeyDetailsToPb(t *testing.T) {
State: domain.WebKeyStateActive,
Config: &crypto.WebKeyED25519Config{},
}},
want: &webkey.GetWebKey{
Details: &resource_object.Details{
Id: "keyID",
Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
State: webkey.WebKeyState_STATE_ACTIVE,
Config: &webkey.WebKey{
Config: &webkey.WebKey_Ed25519{
Ed25519: &webkey.WebKeyED25519Config{},
},
want: &webkey.WebKey{
Id: "keyID",
CreationDate: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
ChangeDate: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
State: webkey.State_STATE_ACTIVE,
Key: &webkey.WebKey_Ed25519{
Ed25519: &webkey.ED25519{},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := webKeyDetailsToPb(tt.args.details, instanceID)
got := webKeyDetailsToPb(tt.args.details)
assert.Equal(t, tt.want, got)
})
}
@@ -391,37 +356,37 @@ func Test_webKeyStateToPb(t *testing.T) {
tests := []struct {
name string
args args
want webkey.WebKeyState
want webkey.State
}{
{
name: "unspecified",
args: args{domain.WebKeyStateUnspecified},
want: webkey.WebKeyState_STATE_UNSPECIFIED,
want: webkey.State_STATE_UNSPECIFIED,
},
{
name: "initial",
args: args{domain.WebKeyStateInitial},
want: webkey.WebKeyState_STATE_INITIAL,
want: webkey.State_STATE_INITIAL,
},
{
name: "active",
args: args{domain.WebKeyStateActive},
want: webkey.WebKeyState_STATE_ACTIVE,
want: webkey.State_STATE_ACTIVE,
},
{
name: "inactive",
args: args{domain.WebKeyStateInactive},
want: webkey.WebKeyState_STATE_INACTIVE,
want: webkey.State_STATE_INACTIVE,
},
{
name: "removed",
args: args{domain.WebKeyStateRemoved},
want: webkey.WebKeyState_STATE_REMOVED,
want: webkey.State_STATE_REMOVED,
},
{
name: "invalid",
args: args{99},
want: webkey.WebKeyState_STATE_UNSPECIFIED,
want: webkey.State_STATE_UNSPECIFIED,
},
}
for _, tt := range tests {
@@ -439,7 +404,7 @@ func Test_webKeyRSAConfigToPb(t *testing.T) {
tests := []struct {
name string
args args
want *webkey.WebKeyRSAConfig
want *webkey.RSA
}{
{
name: "2048, RSA256",
@@ -447,9 +412,9 @@ func Test_webKeyRSAConfigToPb(t *testing.T) {
Bits: crypto.RSABits2048,
Hasher: crypto.RSAHasherSHA256,
}},
want: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
want: &webkey.RSA{
Bits: webkey.RSABits_RSA_BITS_2048,
Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
},
},
{
@@ -458,9 +423,9 @@ func Test_webKeyRSAConfigToPb(t *testing.T) {
Bits: crypto.RSABits3072,
Hasher: crypto.RSAHasherSHA384,
}},
want: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
want: &webkey.RSA{
Bits: webkey.RSABits_RSA_BITS_3072,
Hasher: webkey.RSAHasher_RSA_HASHER_SHA384,
},
},
{
@@ -469,9 +434,9 @@ func Test_webKeyRSAConfigToPb(t *testing.T) {
Bits: crypto.RSABits4096,
Hasher: crypto.RSAHasherSHA512,
}},
want: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_4096,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA512,
want: &webkey.RSA{
Bits: webkey.RSABits_RSA_BITS_4096,
Hasher: webkey.RSAHasher_RSA_HASHER_SHA512,
},
},
}
@@ -490,15 +455,15 @@ func Test_webKeyECDSAConfigToPb(t *testing.T) {
tests := []struct {
name string
args args
want *webkey.WebKeyECDSAConfig
want *webkey.ECDSA
}{
{
name: "P256",
args: args{&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP256,
}},
want: &webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256,
want: &webkey.ECDSA{
Curve: webkey.ECDSACurve_ECDSA_CURVE_P256,
},
},
{
@@ -506,8 +471,8 @@ func Test_webKeyECDSAConfigToPb(t *testing.T) {
args: args{&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
}},
want: &webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
want: &webkey.ECDSA{
Curve: webkey.ECDSACurve_ECDSA_CURVE_P384,
},
},
{
@@ -515,8 +480,8 @@ func Test_webKeyECDSAConfigToPb(t *testing.T) {
args: args{&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP512,
}},
want: &webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512,
want: &webkey.ECDSA{
Curve: webkey.ECDSACurve_ECDSA_CURVE_P512,
},
},
}

View File

@@ -14,14 +14,16 @@ import (
)
type AuthInterceptor struct {
verifier authz.APITokenVerifier
authConfig authz.Config
verifier authz.APITokenVerifier
authConfig authz.Config
systemAuthConfig authz.Config
}
func AuthorizationInterceptor(verifier authz.APITokenVerifier, authConfig authz.Config) *AuthInterceptor {
func AuthorizationInterceptor(verifier authz.APITokenVerifier, systemAuthConfig authz.Config, authConfig authz.Config) *AuthInterceptor {
return &AuthInterceptor{
verifier: verifier,
authConfig: authConfig,
verifier: verifier,
authConfig: authConfig,
systemAuthConfig: systemAuthConfig,
}
}
@@ -31,7 +33,7 @@ func (a *AuthInterceptor) Handler(next http.Handler) http.Handler {
func (a *AuthInterceptor) HandlerFunc(next http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, err := authorize(r, a.verifier, a.authConfig)
ctx, err := authorize(r, a.verifier, a.systemAuthConfig, a.authConfig)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
@@ -44,7 +46,7 @@ func (a *AuthInterceptor) HandlerFunc(next http.Handler) http.HandlerFunc {
func (a *AuthInterceptor) HandlerFuncWithError(next HandlerFuncWithError) HandlerFuncWithError {
return func(w http.ResponseWriter, r *http.Request) error {
ctx, err := authorize(r, a.verifier, a.authConfig)
ctx, err := authorize(r, a.verifier, a.systemAuthConfig, a.authConfig)
if err != nil {
return err
}
@@ -56,7 +58,7 @@ func (a *AuthInterceptor) HandlerFuncWithError(next HandlerFuncWithError) Handle
type httpReq struct{}
func authorize(r *http.Request, verifier authz.APITokenVerifier, authConfig authz.Config) (_ context.Context, err error) {
func authorize(r *http.Request, verifier authz.APITokenVerifier, systemAuthConfig authz.Config, authConfig authz.Config) (_ context.Context, err error) {
ctx := r.Context()
authOpt, needsToken := checkAuthMethod(r, verifier)
@@ -71,7 +73,7 @@ func authorize(r *http.Request, verifier authz.APITokenVerifier, authConfig auth
return nil, zerrors.ThrowUnauthenticated(nil, "AUT-1179", "auth header missing")
}
ctxSetter, err := authz.CheckUserAuthorization(authCtx, &httpReq{}, authToken, http_util.GetOrgID(r), "", verifier, authConfig, authOpt, r.RequestURI)
ctxSetter, err := authz.CheckUserAuthorization(authCtx, &httpReq{}, authToken, http_util.GetOrgID(r), "", verifier, systemAuthConfig.RolePermissionMappings, authConfig.RolePermissionMappings, authOpt, r.RequestURI)
if err != nil {
return nil, err
}

View File

@@ -1,6 +1,7 @@
package middleware
import (
"os"
"testing"
"golang.org/x/text/language"
@@ -14,5 +15,5 @@ var (
func TestMain(m *testing.M) {
i18n.SupportLanguages(SupportedLanguages...)
m.Run()
os.Exit(m.Run())
}

View File

@@ -417,7 +417,6 @@ func (o *OPStorage) getMaxKeySequence(ctx context.Context) (float64, error) {
eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence).
ResourceOwner(authz.GetInstance(ctx).InstanceID()).
AwaitOpenTransactions().
AllowTimeTravel().
AddQuery().
AggregateTypes(
keypair.AggregateType,

View File

@@ -5,444 +5,438 @@ package integration_test
import (
"context"
"fmt"
"net/http"
"slices"
"strings"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/api/scim/resources"
"github.com/zitadel/zitadel/internal/api/scim/schemas"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/internal/integration/scim"
"github.com/zitadel/zitadel/internal/test"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
var totalCountOfHumanUsers = 13
func TestListUser(t *testing.T) {
createdUserIDs := createUsers(t, CTX, Instance.DefaultOrg.Id)
defer func() {
// only the full user needs to be deleted, all others have random identification data
// fullUser is always the first one.
_, err := Instance.Client.UserV2.DeleteUser(CTX, &user_v2.DeleteUserRequest{
UserId: createdUserIDs[0],
})
require.NoError(t, err)
}()
/*
func TestListUser(t *testing.T) {
createdUserIDs := createUsers(t, CTX, Instance.DefaultOrg.Id)
defer func() {
// only the full user needs to be deleted, all others have random identification data
// fullUser is always the first one.
_, err := Instance.Client.UserV2.DeleteUser(CTX, &user_v2.DeleteUserRequest{
UserId: createdUserIDs[0],
})
require.NoError(t, err)
}()
// secondary organization with same set of users,
// these should never be modified.
// This allows testing list requests without filters.
iamOwnerCtx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
secondaryOrg := Instance.CreateOrganization(iamOwnerCtx, gofakeit.Name(), gofakeit.Email())
secondaryOrgCreatedUserIDs := createUsers(t, iamOwnerCtx, secondaryOrg.OrganizationId)
// secondary organization with same set of users,
// these should never be modified.
// This allows testing list requests without filters.
iamOwnerCtx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
secondaryOrg := Instance.CreateOrganization(iamOwnerCtx, gofakeit.Name(), gofakeit.Email())
secondaryOrgCreatedUserIDs := createUsers(t, iamOwnerCtx, secondaryOrg.OrganizationId)
testsInitializedUtc := time.Now().UTC()
testsInitializedUtc := time.Now().UTC()
// Wait one second to ensure a change in the least significant value of the timestamp.
time.Sleep(time.Second)
// Wait one second to ensure a change in the least significant value of the timestamp.
time.Sleep(time.Second)
tests := []struct {
name string
ctx context.Context
orgID string
req *scim.ListRequest
prepare func(require.TestingT) *scim.ListRequest
wantErr bool
errorStatus int
errorType string
assert func(assert.TestingT, *scim.ListResponse[*resources.ScimUser])
cleanup func(require.TestingT)
}{
{
name: "not authenticated",
ctx: context.Background(),
req: new(scim.ListRequest),
wantErr: true,
errorStatus: http.StatusUnauthorized,
},
{
name: "no permissions",
ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
req: new(scim.ListRequest),
wantErr: true,
errorStatus: http.StatusNotFound,
},
{
name: "unknown sort order",
req: &scim.ListRequest{
SortBy: gu.Ptr("id"),
SortOrder: gu.Ptr(scim.ListRequestSortOrder("fooBar")),
tests := []struct {
name string
ctx context.Context
orgID string
req *scim.ListRequest
prepare func(require.TestingT) *scim.ListRequest
wantErr bool
errorStatus int
errorType string
assert func(assert.TestingT, *scim.ListResponse[*resources.ScimUser])
cleanup func(require.TestingT)
}{
{
name: "not authenticated",
ctx: context.Background(),
req: new(scim.ListRequest),
wantErr: true,
errorStatus: http.StatusUnauthorized,
},
wantErr: true,
errorType: "invalidValue",
},
{
name: "unknown sort field",
req: &scim.ListRequest{
SortBy: gu.Ptr("fooBar"),
{
name: "no permissions",
ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
req: new(scim.ListRequest),
wantErr: true,
errorStatus: http.StatusNotFound,
},
wantErr: true,
errorType: "invalidValue",
},
{
name: "custom sort field",
req: &scim.ListRequest{
SortBy: gu.Ptr("externalid"),
{
name: "unknown sort order",
req: &scim.ListRequest{
SortBy: gu.Ptr("id"),
SortOrder: gu.Ptr(scim.ListRequestSortOrder("fooBar")),
},
wantErr: true,
errorType: "invalidValue",
},
wantErr: true,
errorType: "invalidValue",
},
{
name: "unknown filter field",
req: &scim.ListRequest{
Filter: gu.Ptr(`fooBar eq "10"`),
{
name: "unknown sort field",
req: &scim.ListRequest{
SortBy: gu.Ptr("fooBar"),
},
wantErr: true,
errorType: "invalidValue",
},
wantErr: true,
errorType: "invalidFilter",
},
{
name: "invalid filter",
req: &scim.ListRequest{
Filter: gu.Ptr(`fooBarBaz`),
{
name: "custom sort field",
req: &scim.ListRequest{
SortBy: gu.Ptr("externalid"),
},
wantErr: true,
errorType: "invalidValue",
},
wantErr: true,
errorType: "invalidFilter",
},
{
name: "list users without filter",
// use other org, modifications of users happens only on primary org
orgID: secondaryOrg.OrganizationId,
ctx: iamOwnerCtx,
req: new(scim.ListRequest),
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Equal(t, 100, resp.ItemsPerPage)
assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, resp.Resources, totalCountOfHumanUsers)
{
name: "unknown filter field",
req: &scim.ListRequest{
Filter: gu.Ptr(`fooBar eq "10"`),
},
wantErr: true,
errorType: "invalidFilter",
},
},
{
name: "list paged sorted users without filter",
// use other org, modifications of users happens only on primary org
orgID: secondaryOrg.OrganizationId,
ctx: iamOwnerCtx,
req: &scim.ListRequest{
Count: gu.Ptr(2),
StartIndex: gu.Ptr(5),
SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc),
SortBy: gu.Ptr("username"),
{
name: "invalid filter",
req: &scim.ListRequest{
Filter: gu.Ptr(`fooBarBaz`),
},
wantErr: true,
errorType: "invalidFilter",
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
// sort the created users with usernames instead of creation date
sortedResources := sortScimUserByUsername(resp.Resources)
{
name: "list users without filter",
// use other org, modifications of users happens only on primary org
orgID: secondaryOrg.OrganizationId,
ctx: iamOwnerCtx,
req: new(scim.ListRequest),
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Equal(t, 100, resp.ItemsPerPage)
assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, resp.Resources, totalCountOfHumanUsers)
},
},
{
name: "list paged sorted users without filter",
// use other org, modifications of users happens only on primary org
orgID: secondaryOrg.OrganizationId,
ctx: iamOwnerCtx,
req: &scim.ListRequest{
Count: gu.Ptr(2),
StartIndex: gu.Ptr(5),
SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc),
SortBy: gu.Ptr("username"),
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
// sort the created users with usernames instead of creation date
sortedResources := sortScimUserByUsername(resp.Resources)
assert.Equal(t, 2, resp.ItemsPerPage)
assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults)
assert.Equal(t, 5, resp.StartIndex)
assert.Len(t, sortedResources, 2)
assert.True(t, strings.HasPrefix(sortedResources[0].UserName, "scim-username-1: "))
assert.True(t, strings.HasPrefix(sortedResources[1].UserName, "scim-username-2: "))
assert.Equal(t, 2, resp.ItemsPerPage)
assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults)
assert.Equal(t, 5, resp.StartIndex)
assert.Len(t, sortedResources, 2)
assert.True(t, strings.HasPrefix(sortedResources[0].UserName, "scim-username-1: "), "got %q", resp.Resources[0].UserName)
assert.True(t, strings.HasPrefix(sortedResources[1].UserName, "scim-username-2: "), "got %q", resp.Resources[1].UserName)
},
},
},
{
name: "list users with simple filter",
req: &scim.ListRequest{
Filter: gu.Ptr(`username sw "scim-username-1"`),
{
name: "list users with simple filter",
req: &scim.ListRequest{
Filter: gu.Ptr(`username sw "scim-username-1"`),
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Equal(t, 100, resp.ItemsPerPage)
assert.Equal(t, 2, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, resp.Resources, 2)
for _, resource := range resp.Resources {
assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1"))
}
},
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Equal(t, 100, resp.ItemsPerPage)
assert.Equal(t, 2, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, resp.Resources, 2)
for _, resource := range resp.Resources {
assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1"))
}
},
},
{
name: "list paged sorted users with filter",
req: &scim.ListRequest{
Count: gu.Ptr(5),
StartIndex: gu.Ptr(1),
SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc),
SortBy: gu.Ptr("username"),
Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`),
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
// sort the created users with usernames instead of creation date
sortedResources := sortScimUserByUsername(resp.Resources)
{
name: "list paged sorted users with filter",
req: &scim.ListRequest{
Count: gu.Ptr(5),
StartIndex: gu.Ptr(1),
SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc),
SortBy: gu.Ptr("username"),
Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`),
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
// sort the created users with usernames instead of creation date
sortedResources := sortScimUserByUsername(resp.Resources)
assert.Equal(t, 5, resp.ItemsPerPage)
assert.Equal(t, 2, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, sortedResources, 2)
for _, resource := range sortedResources {
assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1"))
assert.Len(t, resource.Emails, 1)
assert.True(t, strings.HasPrefix(resource.Emails[0].Value, "scim-email-1"))
assert.True(t, strings.HasSuffix(resource.Emails[0].Value, "@example.com"))
}
assert.Equal(t, 5, resp.ItemsPerPage)
assert.Equal(t, 2, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, sortedResources, 2)
for _, resource := range sortedResources {
assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1"))
assert.Len(t, resource.Emails, 1)
assert.True(t, strings.HasPrefix(resource.Emails[0].Value, "scim-email-1"))
assert.True(t, strings.HasSuffix(resource.Emails[0].Value, "@example.com"))
}
},
},
},
{
name: "list paged sorted users with filter as post",
req: &scim.ListRequest{
Schemas: []schemas.ScimSchemaType{schemas.IdSearchRequest},
Count: gu.Ptr(5),
StartIndex: gu.Ptr(1),
SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc),
SortBy: gu.Ptr("username"),
Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`),
SendAsPost: true,
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
// sort the created users with usernames instead of creation date
sortedResources := sortScimUserByUsername(resp.Resources)
{
name: "list paged sorted users with filter as post",
req: &scim.ListRequest{
Schemas: []schemas.ScimSchemaType{schemas.IdSearchRequest},
Count: gu.Ptr(5),
StartIndex: gu.Ptr(1),
SortOrder: gu.Ptr(scim.ListRequestSortOrderAsc),
SortBy: gu.Ptr("username"),
Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`),
SendAsPost: true,
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
// sort the created users with usernames instead of creation date
sortedResources := sortScimUserByUsername(resp.Resources)
assert.Equal(t, 5, resp.ItemsPerPage)
assert.Equal(t, 2, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, sortedResources, 2)
for _, resource := range sortedResources {
assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1"))
assert.Len(t, resource.Emails, 1)
assert.True(t, strings.HasPrefix(resource.Emails[0].Value, "scim-email-1"))
assert.True(t, strings.HasSuffix(resource.Emails[0].Value, "@example.com"))
}
assert.Equal(t, 5, resp.ItemsPerPage)
assert.Equal(t, 2, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, sortedResources, 2)
for _, resource := range sortedResources {
assert.True(t, strings.HasPrefix(resource.UserName, "scim-username-1"))
assert.Len(t, resource.Emails, 1)
assert.True(t, strings.HasPrefix(resource.Emails[0].Value, "scim-email-1"))
assert.True(t, strings.HasSuffix(resource.Emails[0].Value, "@example.com"))
}
},
},
},
{
name: "count users without filter",
// use other org, modifications of users happens only on primary org
orgID: secondaryOrg.OrganizationId,
ctx: iamOwnerCtx,
prepare: func(t require.TestingT) *scim.ListRequest {
return &scim.ListRequest{
Count: gu.Ptr(0),
}
{
name: "count users without filter",
// use other org, modifications of users happens only on primary org
orgID: secondaryOrg.OrganizationId,
ctx: iamOwnerCtx,
prepare: func(t require.TestingT) *scim.ListRequest {
return &scim.ListRequest{
Count: gu.Ptr(0),
}
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Equal(t, 0, resp.ItemsPerPage)
assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, resp.Resources, 0)
},
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Equal(t, 0, resp.ItemsPerPage)
assert.Equal(t, totalCountOfHumanUsers, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, resp.Resources, 0)
{
name: "list users with active filter",
req: &scim.ListRequest{
Filter: gu.Ptr(`active eq false`),
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Equal(t, 100, resp.ItemsPerPage)
assert.Equal(t, 1, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, resp.Resources, 1)
assert.True(t, strings.HasPrefix(resp.Resources[0].UserName, "scim-username-0"))
assert.False(t, resp.Resources[0].Active.Bool())
},
},
},
{
name: "list users with active filter",
req: &scim.ListRequest{
Filter: gu.Ptr(`active eq false`),
{
name: "list users with externalid filter",
req: &scim.ListRequest{
Filter: gu.Ptr(`externalid eq "701984"`),
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Equal(t, 100, resp.ItemsPerPage)
assert.Equal(t, 1, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, resp.Resources, 1)
assert.Equal(t, resp.Resources[0].ExternalID, "701984")
},
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Equal(t, 100, resp.ItemsPerPage)
assert.Equal(t, 1, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, resp.Resources, 1)
assert.True(t, strings.HasPrefix(resp.Resources[0].UserName, "scim-username-0"))
assert.False(t, resp.Resources[0].Active.Bool())
{
name: "list users with externalid filter invalid operator",
req: &scim.ListRequest{
Filter: gu.Ptr(`externalid pr`),
},
wantErr: true,
errorType: "invalidFilter",
},
},
{
name: "list users with externalid filter",
req: &scim.ListRequest{
Filter: gu.Ptr(`externalid eq "701984"`),
{
name: "list users with externalid complex filter",
req: &scim.ListRequest{
Filter: gu.Ptr(`externalid eq "701984" and username eq "bjensen@example.com"`),
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Equal(t, 100, resp.ItemsPerPage)
assert.Equal(t, 1, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, resp.Resources, 1)
assert.Equal(t, resp.Resources[0].UserName, "bjensen@example.com")
assert.Equal(t, resp.Resources[0].ExternalID, "701984")
},
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Equal(t, 100, resp.ItemsPerPage)
assert.Equal(t, 1, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, resp.Resources, 1)
assert.Equal(t, resp.Resources[0].ExternalID, "701984")
},
},
{
name: "list users with externalid filter invalid operator",
req: &scim.ListRequest{
Filter: gu.Ptr(`externalid pr`),
},
wantErr: true,
errorType: "invalidFilter",
},
{
name: "list users with externalid complex filter",
req: &scim.ListRequest{
Filter: gu.Ptr(`externalid eq "701984" and username eq "bjensen@example.com"`),
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Equal(t, 100, resp.ItemsPerPage)
assert.Equal(t, 1, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, resp.Resources, 1)
assert.Equal(t, resp.Resources[0].UserName, "bjensen@example.com")
assert.Equal(t, resp.Resources[0].ExternalID, "701984")
},
},
{
name: "count users with filter",
req: &scim.ListRequest{
Count: gu.Ptr(0),
Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`),
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Equal(t, 0, resp.ItemsPerPage)
assert.Equal(t, 2, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, resp.Resources, 0)
},
},
{
name: "list users with modification date filter",
prepare: func(t require.TestingT) *scim.ListRequest {
userID := createdUserIDs[len(createdUserIDs)-1] // use the last entry, as we use the others for other assertions
_, err := Instance.Client.UserV2.UpdateHumanUser(CTX, &user_v2.UpdateHumanUserRequest{
UserId: userID,
Profile: &user_v2.SetHumanProfile{
GivenName: "scim-user-given-name-modified-0: " + gofakeit.FirstName(),
FamilyName: "scim-user-family-name-modified-0: " + gofakeit.LastName(),
},
})
require.NoError(t, err)
return &scim.ListRequest{
// filter by id too to exclude other random users
Filter: gu.Ptr(fmt.Sprintf(`id eq "%s" and meta.LASTMODIFIED gt "%s"`, userID, testsInitializedUtc.Format(time.RFC3339))),
}
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Len(t, resp.Resources, 1)
assert.Equal(t, resp.Resources[0].ID, createdUserIDs[len(createdUserIDs)-1])
assert.True(t, strings.HasPrefix(resp.Resources[0].Name.FamilyName, "scim-user-family-name-modified-0:"))
assert.True(t, strings.HasPrefix(resp.Resources[0].Name.GivenName, "scim-user-given-name-modified-0:"))
},
},
{
name: "list users with creation date filter",
prepare: func(t require.TestingT) *scim.ListRequest {
resp := createHumanUser(t, CTX, Instance.DefaultOrg.Id, 100)
return &scim.ListRequest{
Filter: gu.Ptr(fmt.Sprintf(`id eq "%s" and meta.created gt "%s"`, resp.UserId, testsInitializedUtc.Format(time.RFC3339))),
}
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Len(t, resp.Resources, 1)
assert.True(t, strings.HasPrefix(resp.Resources[0].UserName, "scim-username-100:"))
},
},
{
name: "validate returned objects",
req: &scim.ListRequest{
Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, createdUserIDs[0])),
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Len(t, resp.Resources, 1)
if !test.PartiallyDeepEqual(fullUser, resp.Resources[0]) {
t.Errorf("got = %#v, want %#v", resp.Resources[0], fullUser)
}
},
},
{
name: "do not return user of other org",
req: &scim.ListRequest{
Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, secondaryOrgCreatedUserIDs[0])),
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Len(t, resp.Resources, 0)
},
},
{
name: "do not count user of other org",
prepare: func(t require.TestingT) *scim.ListRequest {
iamOwnerCtx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
org := Instance.CreateOrganization(iamOwnerCtx, gofakeit.Name(), gofakeit.Email())
resp := createHumanUser(t, iamOwnerCtx, org.OrganizationId, 102)
return &scim.ListRequest{
{
name: "count users with filter",
req: &scim.ListRequest{
Count: gu.Ptr(0),
Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, resp.UserId)),
Filter: gu.Ptr(`emails sw "scim-email-1" and emails ew "@example.com"`),
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Equal(t, 0, resp.ItemsPerPage)
assert.Equal(t, 2, resp.TotalResults)
assert.Equal(t, 1, resp.StartIndex)
assert.Len(t, resp.Resources, 0)
},
},
{
name: "list users with modification date filter",
prepare: func(t require.TestingT) *scim.ListRequest {
userID := createdUserIDs[len(createdUserIDs)-1] // use the last entry, as we use the others for other assertions
_, err := Instance.Client.UserV2.UpdateHumanUser(CTX, &user_v2.UpdateHumanUserRequest{
UserId: userID,
Profile: &user_v2.SetHumanProfile{
GivenName: "scim-user-given-name-modified-0: " + gofakeit.FirstName(),
FamilyName: "scim-user-family-name-modified-0: " + gofakeit.LastName(),
},
})
require.NoError(t, err)
return &scim.ListRequest{
// filter by id too to exclude other random users
Filter: gu.Ptr(fmt.Sprintf(`id eq "%s" and meta.LASTMODIFIED gt "%s"`, userID, testsInitializedUtc.Format(time.RFC3339))),
}
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Len(t, resp.Resources, 1)
assert.Equal(t, resp.Resources[0].ID, createdUserIDs[len(createdUserIDs)-1])
assert.True(t, strings.HasPrefix(resp.Resources[0].Name.FamilyName, "scim-user-family-name-modified-0:"))
assert.True(t, strings.HasPrefix(resp.Resources[0].Name.GivenName, "scim-user-given-name-modified-0:"))
},
},
{
name: "list users with creation date filter",
prepare: func(t require.TestingT) *scim.ListRequest {
resp := createHumanUser(t, CTX, Instance.DefaultOrg.Id, 100)
return &scim.ListRequest{
Filter: gu.Ptr(fmt.Sprintf(`id eq "%s" and meta.created gt "%s"`, resp.UserId, testsInitializedUtc.Format(time.RFC3339))),
}
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Len(t, resp.Resources, 1)
assert.True(t, strings.HasPrefix(resp.Resources[0].UserName, "scim-username-100:"))
},
},
{
name: "validate returned objects",
req: &scim.ListRequest{
Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, createdUserIDs[0])),
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Len(t, resp.Resources, 1)
if !test.PartiallyDeepEqual(fullUser, resp.Resources[0]) {
t.Errorf("got = %#v, want %#v", resp.Resources[0], fullUser)
}
},
},
{
name: "do not return user of other org",
req: &scim.ListRequest{
Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, secondaryOrgCreatedUserIDs[0])),
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Len(t, resp.Resources, 0)
},
},
{
name: "do not count user of other org",
prepare: func(t require.TestingT) *scim.ListRequest {
iamOwnerCtx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
org := Instance.CreateOrganization(iamOwnerCtx, gofakeit.Name(), gofakeit.Email())
resp := createHumanUser(t, iamOwnerCtx, org.OrganizationId, 102)
return &scim.ListRequest{
Count: gu.Ptr(0),
Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, resp.UserId)),
}
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Len(t, resp.Resources, 0)
},
},
{
name: "scoped externalID",
prepare: func(t require.TestingT) *scim.ListRequest {
resp := createHumanUser(t, CTX, Instance.DefaultOrg.Id, 102)
// set provisioning domain of service user
setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBar")
// set externalID for provisioning domain
setAndEnsureMetadata(t, resp.UserId, "urn:zitadel:scim:fooBar:externalId", "100-scopedExternalId")
return &scim.ListRequest{
Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, resp.UserId)),
}
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Len(t, resp.Resources, 1)
assert.Equal(t, resp.Resources[0].ExternalID, "100-scopedExternalId")
},
cleanup: func(t require.TestingT) {
// delete provisioning domain of service user
removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.ctx == nil {
tt.ctx = CTX
}
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Len(t, resp.Resources, 0)
},
},
{
name: "scoped externalID",
prepare: func(t require.TestingT) *scim.ListRequest {
resp := createHumanUser(t, CTX, Instance.DefaultOrg.Id, 102)
// set provisioning domain of service user
setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBar")
// set externalID for provisioning domain
setAndEnsureMetadata(t, resp.UserId, "urn:zitadel:scim:fooBar:externalId", "100-scopedExternalId")
return &scim.ListRequest{
Filter: gu.Ptr(fmt.Sprintf(`id eq "%s"`, resp.UserId)),
if tt.prepare != nil {
tt.req = tt.prepare(t)
}
},
assert: func(t assert.TestingT, resp *scim.ListResponse[*resources.ScimUser]) {
assert.Len(t, resp.Resources, 1)
assert.Equal(t, resp.Resources[0].ExternalID, "100-scopedExternalId")
},
cleanup: func(t require.TestingT) {
// delete provisioning domain of service user
removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.ctx == nil {
tt.ctx = CTX
}
if tt.prepare != nil {
tt.req = tt.prepare(t)
}
if tt.orgID == "" {
tt.orgID = Instance.DefaultOrg.Id
}
if tt.orgID == "" {
tt.orgID = Instance.DefaultOrg.Id
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
listResp, err := Instance.Client.SCIM.Users.List(tt.ctx, tt.orgID, tt.req)
if tt.wantErr {
statusCode := tt.errorStatus
if statusCode == 0 {
statusCode = http.StatusBadRequest
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
listResp, err := Instance.Client.SCIM.Users.List(tt.ctx, tt.orgID, tt.req)
if tt.wantErr {
statusCode := tt.errorStatus
if statusCode == 0 {
statusCode = http.StatusBadRequest
scimErr := scim.RequireScimError(ttt, statusCode, err)
if tt.errorType != "" {
assert.Equal(t, tt.errorType, scimErr.Error.ScimType)
}
return
}
scimErr := scim.RequireScimError(ttt, statusCode, err)
if tt.errorType != "" {
assert.Equal(t, tt.errorType, scimErr.Error.ScimType)
require.NoError(t, err)
assert.EqualValues(ttt, []schemas.ScimSchemaType{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}, listResp.Schemas)
if tt.assert != nil {
tt.assert(ttt, listResp)
}
return
}
}, retryDuration, tick)
require.NoError(t, err)
assert.EqualValues(ttt, []schemas.ScimSchemaType{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}, listResp.Schemas)
if tt.assert != nil {
tt.assert(ttt, listResp)
if tt.cleanup != nil {
tt.cleanup(t)
}
}, retryDuration, tick)
if tt.cleanup != nil {
tt.cleanup(t)
}
})
})
}
}
}
*/
func sortScimUserByUsername(users []*resources.ScimUser) []*resources.ScimUser {
sortedResources := users
slices.SortFunc(sortedResources, func(a, b *resources.ScimUser) int {