chore(tests): use a coverage server binary (#8407)

# Which Problems Are Solved

Use a single server instance for API integration tests. This optimizes
the time taken for the integration test pipeline,
because it allows running tests on multiple packages in parallel. Also,
it saves time by not start and stopping a zitadel server for every
package.

# How the Problems Are Solved

- Build a binary with `go build -race -cover ....`
- Integration tests only construct clients. The server remains running
in the background.
- The integration package and tested packages now fully utilize the API.
No more direct database access trough `query` and `command` packages.
- Use Makefile recipes to setup, start and stop the server in the
background.
- The binary has the race detector enabled
- Init and setup jobs are configured to halt immediately on race
condition
- Because the server runs in the background, races are only logged. When
the server is stopped and race logs exist, the Makefile recipe will
throw an error and print the logs.
- Makefile recipes include logic to print logs and convert coverage
reports after the server is stopped.
- Some tests need a downstream HTTP server to make requests, like quota
and milestones. A new `integration/sink` package creates an HTTP server
and uses websockets to forward HTTP request back to the test packages.
The package API uses Go channels for abstraction and easy usage.

# Additional Changes

- Integration test files already used the `//go:build integration`
directive. In order to properly split integration from unit tests,
integration test files need to be in a `integration_test` subdirectory
of their package.
- `UseIsolatedInstance` used to overwrite the `Tester.Client` for each
instance. Now a `Instance` object is returned with a gRPC client that is
connected to the isolated instance's hostname.
- The `Tester` type is now `Instance`. The object is created for the
first instance, used by default in any test. Isolated instances are also
`Instance` objects and therefore benefit from the same methods and
values. The first instance and any other us capable of creating an
isolated instance over the system API.
- All test packages run in an Isolated instance by calling
`NewInstance()`
- Individual tests that use an isolated instance use `t.Parallel()`

# Additional Context

- Closes #6684
- https://go.dev/doc/articles/race_detector
- https://go.dev/doc/build-cover

---------

Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
This commit is contained in:
Tim Möhlmann
2024-09-06 15:47:57 +03:00
committed by GitHub
parent b522588d98
commit d2e0ac07f1
115 changed files with 3632 additions and 3348 deletions

View File

@@ -50,7 +50,7 @@ type ResourceListDetailsMsg interface {
// If the change date is populated, it is checked with a tolerance of 1 minute around Now.
//
// The resource owner is compared with expected.
func AssertDetails[D Details, M DetailsMsg[D]](t testing.TB, expected, actual M) {
func AssertDetails[D Details, M DetailsMsg[D]](t assert.TestingT, expected, actual M) {
wantDetails, gotDetails := expected.GetDetails(), actual.GetDetails()
var nilDetails D
if wantDetails == nilDetails {
@@ -69,7 +69,7 @@ func AssertDetails[D Details, M DetailsMsg[D]](t testing.TB, expected, actual M)
assert.Equal(t, wantDetails.GetResourceOwner(), gotDetails.GetResourceOwner())
}
func AssertResourceDetails(t testing.TB, expected *resources_object.Details, actual *resources_object.Details) {
func AssertResourceDetails(t assert.TestingT, expected *resources_object.Details, actual *resources_object.Details) {
if expected.GetChanged() != nil {
wantChangeDate := time.Now()
gotChangeDate := actual.GetChanged().AsTime()
@@ -87,7 +87,7 @@ func AssertResourceDetails(t testing.TB, expected *resources_object.Details, act
}
}
func AssertListDetails[L ListDetails, D ListDetailsMsg[L]](t testing.TB, expected, actual D) {
func AssertListDetails[L ListDetails, D ListDetailsMsg[L]](t assert.TestingT, expected, actual D) {
wantDetails, gotDetails := expected.GetDetails(), actual.GetDetails()
var nilDetails L
if wantDetails == nilDetails {
@@ -99,11 +99,11 @@ func AssertListDetails[L ListDetails, D ListDetailsMsg[L]](t testing.TB, expecte
if wantDetails.GetTimestamp() != nil {
gotCD := gotDetails.GetTimestamp().AsTime()
wantCD := time.Now()
assert.WithinRange(t, gotCD, wantCD.Add(-time.Minute), wantCD.Add(time.Minute))
assert.WithinRange(t, gotCD, wantCD.Add(-10*time.Minute), wantCD.Add(time.Minute))
}
}
func AssertResourceListDetails[D ResourceListDetailsMsg](t testing.TB, expected, actual D) {
func AssertResourceListDetails[D ResourceListDetailsMsg](t assert.TestingT, expected, actual D) {
wantDetails, gotDetails := expected.GetDetails(), actual.GetDetails()
if wantDetails == nil {
assert.Nil(t, gotDetails)
@@ -116,7 +116,7 @@ func AssertResourceListDetails[D ResourceListDetailsMsg](t testing.TB, expected,
if wantDetails.GetTimestamp() != nil {
gotCD := gotDetails.GetTimestamp().AsTime()
wantCD := time.Now()
assert.WithinRange(t, gotCD, wantCD.Add(-time.Minute), wantCD.Add(time.Minute))
assert.WithinRange(t, gotCD, wantCD.Add(-10*time.Minute), wantCD.Add(time.Minute))
}
}

View File

@@ -6,26 +6,17 @@ import (
"testing"
"time"
crewjam_saml "github.com/crewjam/saml"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v3/pkg/oidc"
"golang.org/x/oauth2"
"golang.org/x/text/language"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/structpb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
"github.com/zitadel/zitadel/internal/idp/providers/saml"
idp_rp "github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/auth"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
@@ -37,7 +28,7 @@ import (
object_v3alpha "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
oidc_pb_v2beta "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2"
org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
user_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
@@ -47,7 +38,6 @@ import (
session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
settings_v2beta "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/system"
user_pb "github.com/zitadel/zitadel/pkg/grpc/user"
user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2"
user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
@@ -68,7 +58,6 @@ type Client struct {
OIDCv2 oidc_pb.OIDCServiceClient
OrgV2beta org_v2beta.OrganizationServiceClient
OrgV2 org.OrganizationServiceClient
System system.SystemServiceClient
ActionV3Alpha action.ZITADELActionsClient
FeatureV2beta feature_v2beta.FeatureServiceClient
FeatureV2 feature.FeatureServiceClient
@@ -78,8 +67,14 @@ type Client struct {
UserV3Alpha user_v3alpha.ZITADELUsersClient
}
func newClient(cc *grpc.ClientConn) Client {
return Client{
func newClient(ctx context.Context, target string) (*Client, error) {
cc, err := grpc.NewClient(target,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
return nil, err
}
client := &Client{
CC: cc,
Admin: admin.NewAdminServiceClient(cc),
Mgmt: mgmt.NewManagementServiceClient(cc),
@@ -94,7 +89,6 @@ func newClient(cc *grpc.ClientConn) Client {
OIDCv2: oidc_pb.NewOIDCServiceClient(cc),
OrgV2beta: org_v2beta.NewOrganizationServiceClient(cc),
OrgV2: org.NewOrganizationServiceClient(cc),
System: system.NewSystemServiceClient(cc),
ActionV3Alpha: action.NewZITADELActionsClient(cc),
FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc),
FeatureV2: feature.NewFeatureServiceClient(cc),
@@ -103,60 +97,38 @@ func newClient(cc *grpc.ClientConn) Client {
IDPv2: idp_pb.NewIdentityProviderServiceClient(cc),
UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc),
}
return client, client.pollHealth(ctx)
}
func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx context.Context) (primaryDomain, instanceId, adminID string, authenticatedIamOwnerCtx context.Context) {
primaryDomain = RandString(5) + ".integration.localhost"
instance, err := t.Client.System.CreateInstance(systemCtx, &system.CreateInstanceRequest{
InstanceName: "testinstance",
CustomDomain: primaryDomain,
Owner: &system.CreateInstanceRequest_Machine_{
Machine: &system.CreateInstanceRequest_Machine{
UserName: "owner",
Name: "owner",
PersonalAccessToken: &system.CreateInstanceRequest_PersonalAccessToken{},
},
},
})
require.NoError(tt, err)
t.createClientConn(iamOwnerCtx, fmt.Sprintf("%s:%d", primaryDomain, t.Config.Port))
instanceId = instance.GetInstanceId()
owner, err := t.Queries.GetUserByLoginName(authz.WithInstanceID(iamOwnerCtx, instanceId), true, "owner@"+primaryDomain)
require.NoError(tt, err)
t.Users.Set(instanceId, IAMOwner, &User{
User: owner,
Token: instance.GetPat(),
})
newCtx := t.WithInstanceAuthorization(iamOwnerCtx, IAMOwner, instanceId)
var adminUser *mgmt.ImportHumanUserResponse
// the following serves two purposes:
// 1. it ensures that the instance is ready to be used
// 2. it enables a normal login with the default admin user credentials
require.EventuallyWithT(tt, func(collectT *assert.CollectT) {
var importErr error
adminUser, importErr = t.Client.Mgmt.ImportHumanUser(newCtx, &mgmt.ImportHumanUserRequest{
UserName: "zitadel-admin@zitadel.localhost",
Email: &mgmt.ImportHumanUserRequest_Email{
Email: "zitadel-admin@zitadel.localhost",
IsEmailVerified: true,
},
Password: "Password1!",
Profile: &mgmt.ImportHumanUserRequest_Profile{
FirstName: "hodor",
LastName: "hodor",
NickName: "hodor",
},
})
assert.NoError(collectT, importErr)
}, 2*time.Minute, 100*time.Millisecond, "instance not ready")
return primaryDomain, instanceId, adminUser.GetUserId(), t.updateInstanceAndOrg(newCtx, fmt.Sprintf("%s:%d", primaryDomain, t.Config.ExternalPort))
// pollHealth waits until a healthy status is reported.
func (c *Client) pollHealth(ctx context.Context) (err error) {
for {
err = func(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
_, err := c.Admin.Healthz(ctx, &admin.HealthzRequest{})
return err
}(ctx)
if err == nil {
return nil
}
logging.WithError(err).Debug("poll healthz")
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Second):
continue
}
}
}
func (s *Tester) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserResponse {
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{
func (i *Instance) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserResponse {
resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{
Organization: &object.Organization{
Org: &object.Organization_OrgId{
OrgId: s.Organisation.ID,
OrgId: i.DefaultOrg.GetId(),
},
},
Profile: &user_v2.SetHumanProfile{
@@ -178,15 +150,15 @@ func (s *Tester) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserRespo
},
},
})
logging.OnError(err).Fatal("create human user")
logging.OnError(err).Panic("create human user")
return resp
}
func (s *Tester) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHumanUserResponse {
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{
func (i *Instance) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHumanUserResponse {
resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{
Organization: &object.Organization{
Org: &object.Organization_OrgId{
OrgId: s.Organisation.ID,
OrgId: i.DefaultOrg.GetId(),
},
},
Profile: &user_v2.SetHumanProfile{
@@ -202,15 +174,15 @@ func (s *Tester) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHumanUs
},
},
})
logging.OnError(err).Fatal("create human user")
logging.OnError(err).Panic("create human user")
return resp
}
func (s *Tester) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user_v2.AddHumanUserResponse {
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{
func (i *Instance) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user_v2.AddHumanUserResponse {
resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{
Organization: &object.Organization{
Org: &object.Organization_OrgId{
OrgId: s.Organisation.ID,
OrgId: i.DefaultOrg.GetId(),
},
},
Profile: &user_v2.SetHumanProfile{
@@ -233,12 +205,12 @@ func (s *Tester) CreateHumanUserWithTOTP(ctx context.Context, secret string) *us
},
TotpSecret: gu.Ptr(secret),
})
logging.OnError(err).Fatal("create human user")
logging.OnError(err).Panic("create human user")
return resp
}
func (s *Tester) CreateOrganization(ctx context.Context, name, adminEmail string) *org.AddOrganizationResponse {
resp, err := s.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{
func (i *Instance) CreateOrganization(ctx context.Context, name, adminEmail string) *org.AddOrganizationResponse {
resp, err := i.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{
Name: name,
Admins: []*org.AddOrganizationRequest_Admin{
{
@@ -259,12 +231,12 @@ func (s *Tester) CreateOrganization(ctx context.Context, name, adminEmail string
},
},
})
logging.OnError(err).Fatal("create org")
logging.OnError(err).Panic("create org")
return resp
}
func (s *Tester) DeactivateOrganization(ctx context.Context, orgID string) *mgmt.DeactivateOrgResponse {
resp, err := s.Client.Mgmt.DeactivateOrg(
func (i *Instance) DeactivateOrganization(ctx context.Context, orgID string) *mgmt.DeactivateOrgResponse {
resp, err := i.Client.Mgmt.DeactivateOrg(
SetOrgID(ctx, orgID),
&mgmt.DeactivateOrgRequest{},
)
@@ -281,8 +253,8 @@ func SetOrgID(ctx context.Context, orgID string) context.Context {
return metadata.NewOutgoingContext(ctx, md)
}
func (s *Tester) CreateOrganizationWithUserID(ctx context.Context, name, userID string) *org.AddOrganizationResponse {
resp, err := s.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{
func (i *Instance) CreateOrganizationWithUserID(ctx context.Context, name, userID string) *org.AddOrganizationResponse {
resp, err := i.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{
Name: name,
Admins: []*org.AddOrganizationRequest_Admin{
{
@@ -296,8 +268,8 @@ func (s *Tester) CreateOrganizationWithUserID(ctx context.Context, name, userID
return resp
}
func (s *Tester) CreateHumanUserVerified(ctx context.Context, org, email string) *user_v2.AddHumanUserResponse {
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{
func (i *Instance) CreateHumanUserVerified(ctx context.Context, org, email string) *user_v2.AddHumanUserResponse {
resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{
Organization: &object.Organization{
Org: &object.Organization_OrgId{
OrgId: org,
@@ -323,23 +295,23 @@ func (s *Tester) CreateHumanUserVerified(ctx context.Context, org, email string)
},
},
})
logging.OnError(err).Fatal("create human user")
logging.OnError(err).Panic("create human user")
return resp
}
func (s *Tester) CreateMachineUser(ctx context.Context) *mgmt.AddMachineUserResponse {
resp, err := s.Client.Mgmt.AddMachineUser(ctx, &mgmt.AddMachineUserRequest{
func (i *Instance) CreateMachineUser(ctx context.Context) *mgmt.AddMachineUserResponse {
resp, err := i.Client.Mgmt.AddMachineUser(ctx, &mgmt.AddMachineUserRequest{
UserName: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
Name: "Mickey",
Description: "Mickey Mouse",
AccessTokenType: user_pb.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER,
})
logging.OnError(err).Fatal("create human user")
logging.OnError(err).Panic("create human user")
return resp
}
func (s *Tester) CreateUserIDPlink(ctx context.Context, userID, externalID, idpID, username string) *user_v2.AddIDPLinkResponse {
resp, err := s.Client.UserV2.AddIDPLink(
func (i *Instance) CreateUserIDPlink(ctx context.Context, userID, externalID, idpID, username string) (*user_v2.AddIDPLinkResponse, error) {
return i.Client.UserV2.AddIDPLink(
ctx,
&user_v2.AddIDPLinkRequest{
UserId: userID,
@@ -350,67 +322,65 @@ func (s *Tester) CreateUserIDPlink(ctx context.Context, userID, externalID, idpI
},
},
)
logging.OnError(err).Fatal("create human user link")
return resp
}
func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) {
reg, err := s.Client.UserV2.CreatePasskeyRegistrationLink(ctx, &user_v2.CreatePasskeyRegistrationLinkRequest{
func (i *Instance) RegisterUserPasskey(ctx context.Context, userID string) {
reg, err := i.Client.UserV2.CreatePasskeyRegistrationLink(ctx, &user_v2.CreatePasskeyRegistrationLinkRequest{
UserId: userID,
Medium: &user_v2.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
})
logging.OnError(err).Fatal("create user passkey")
logging.OnError(err).Panic("create user passkey")
pkr, err := s.Client.UserV2.RegisterPasskey(ctx, &user_v2.RegisterPasskeyRequest{
pkr, err := i.Client.UserV2.RegisterPasskey(ctx, &user_v2.RegisterPasskeyRequest{
UserId: userID,
Code: reg.GetCode(),
Domain: s.Config.ExternalDomain,
Domain: i.Domain,
})
logging.OnError(err).Fatal("create user passkey")
attestationResponse, err := s.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
logging.OnError(err).Fatal("create user passkey")
logging.OnError(err).Panic("create user passkey")
attestationResponse, err := i.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
logging.OnError(err).Panic("create user passkey")
_, err = s.Client.UserV2.VerifyPasskeyRegistration(ctx, &user_v2.VerifyPasskeyRegistrationRequest{
_, err = i.Client.UserV2.VerifyPasskeyRegistration(ctx, &user_v2.VerifyPasskeyRegistrationRequest{
UserId: userID,
PasskeyId: pkr.GetPasskeyId(),
PublicKeyCredential: attestationResponse,
PasskeyName: "nice name",
})
logging.OnError(err).Fatal("create user passkey")
logging.OnError(err).Panic("create user passkey")
}
func (s *Tester) RegisterUserU2F(ctx context.Context, userID string) {
pkr, err := s.Client.UserV2.RegisterU2F(ctx, &user_v2.RegisterU2FRequest{
func (i *Instance) RegisterUserU2F(ctx context.Context, userID string) {
pkr, err := i.Client.UserV2.RegisterU2F(ctx, &user_v2.RegisterU2FRequest{
UserId: userID,
Domain: s.Config.ExternalDomain,
Domain: i.Domain,
})
logging.OnError(err).Fatal("create user u2f")
attestationResponse, err := s.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
logging.OnError(err).Fatal("create user u2f")
logging.OnError(err).Panic("create user u2f")
attestationResponse, err := i.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
logging.OnError(err).Panic("create user u2f")
_, err = s.Client.UserV2.VerifyU2FRegistration(ctx, &user_v2.VerifyU2FRegistrationRequest{
_, err = i.Client.UserV2.VerifyU2FRegistration(ctx, &user_v2.VerifyU2FRegistrationRequest{
UserId: userID,
U2FId: pkr.GetU2FId(),
PublicKeyCredential: attestationResponse,
TokenName: "nice name",
})
logging.OnError(err).Fatal("create user u2f")
logging.OnError(err).Panic("create user u2f")
}
func (s *Tester) SetUserPassword(ctx context.Context, userID, password string, changeRequired bool) *object.Details {
resp, err := s.Client.UserV2.SetPassword(ctx, &user_v2.SetPasswordRequest{
func (i *Instance) SetUserPassword(ctx context.Context, userID, password string, changeRequired bool) *object.Details {
resp, err := i.Client.UserV2.SetPassword(ctx, &user_v2.SetPasswordRequest{
UserId: userID,
NewPassword: &user_v2.Password{
Password: password,
ChangeRequired: changeRequired,
},
})
logging.OnError(err).Fatal("set user password")
logging.OnError(err).Panic("set user password")
return resp.GetDetails()
}
func (s *Tester) AddGenericOAuthIDP(ctx context.Context, name string) *admin.AddGenericOAuthProviderResponse {
resp, err := s.Client.Admin.AddGenericOAuthProvider(ctx, &admin.AddGenericOAuthProviderRequest{
func (i *Instance) AddGenericOAuthProvider(ctx context.Context, name string) *admin.AddGenericOAuthProviderResponse {
resp, err := i.Client.Admin.AddGenericOAuthProvider(ctx, &admin.AddGenericOAuthProviderRequest{
Name: name,
ClientId: "clientID",
ClientSecret: "clientSecret",
@@ -427,136 +397,126 @@ func (s *Tester) AddGenericOAuthIDP(ctx context.Context, name string) *admin.Add
AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME,
},
})
logging.OnError(err).Fatal("create generic OAuth idp")
return resp
}
logging.OnError(err).Panic("create generic OAuth idp")
func (s *Tester) AddGenericOAuthProvider(t *testing.T, ctx context.Context) string {
ctx = authz.WithInstance(ctx, s.Instance)
id, _, err := s.Commands.AddInstanceGenericOAuthProvider(ctx, command.GenericOAuthProvider{
Name: "idp",
ClientID: "clientID",
ClientSecret: "clientSecret",
AuthorizationEndpoint: "https://example.com/oauth/v2/authorize",
TokenEndpoint: "https://example.com/oauth/v2/token",
UserEndpoint: "https://api.example.com/user",
Scopes: []string{"openid", "profile", "email"},
IDAttribute: "id",
IDPOptions: idp_rp.Options{
IsLinkingAllowed: true,
IsCreationAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
})
require.NoError(t, err)
return id
}
func (s *Tester) AddOrgGenericOAuthIDP(ctx context.Context, name string) *mgmt.AddGenericOAuthProviderResponse {
resp, err := s.Client.Mgmt.AddGenericOAuthProvider(ctx, &mgmt.AddGenericOAuthProviderRequest{
Name: name,
ClientId: "clientID",
ClientSecret: "clientSecret",
AuthorizationEndpoint: "https://example.com/oauth/v2/authorize",
TokenEndpoint: "https://example.com/oauth/v2/token",
UserEndpoint: "https://api.example.com/user",
Scopes: []string{"openid", "profile", "email"},
IdAttribute: "id",
ProviderOptions: &idp.Options{
IsLinkingAllowed: true,
IsCreationAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME,
},
})
logging.OnError(err).Fatal("create generic OAuth idp")
return resp
}
func (s *Tester) AddOrgGenericOAuthProvider(t *testing.T, ctx context.Context, orgID string) string {
ctx = authz.WithInstance(ctx, s.Instance)
id, _, err := s.Commands.AddOrgGenericOAuthProvider(ctx, orgID,
command.GenericOAuthProvider{
Name: "idp",
ClientID: "clientID",
ClientSecret: "clientSecret",
AuthorizationEndpoint: "https://example.com/oauth/v2/authorize",
TokenEndpoint: "https://example.com/oauth/v2/token",
UserEndpoint: "https://api.example.com/user",
Scopes: []string{"openid", "profile", "email"},
IDAttribute: "id",
IDPOptions: idp_rp.Options{
IsLinkingAllowed: true,
IsCreationAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
mustAwait(func() error {
_, err := i.Client.Admin.GetProviderByID(ctx, &admin.GetProviderByIDRequest{
Id: resp.GetId(),
})
require.NoError(t, err)
return id
return err
})
return resp
}
func (s *Tester) AddSAMLProvider(t *testing.T, ctx context.Context) string {
ctx = authz.WithInstance(ctx, s.Instance)
id, _, err := s.Server.Commands.AddInstanceSAMLProvider(ctx, command.SAMLProvider{
Name: "saml-idp",
Metadata: []byte("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-09-16T09:00:32.986Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
IDPOptions: idp_rp.Options{
func (i *Instance) AddOrgGenericOAuthProvider(ctx context.Context, name string) *mgmt.AddGenericOAuthProviderResponse {
resp, err := i.Client.Mgmt.AddGenericOAuthProvider(ctx, &mgmt.AddGenericOAuthProviderRequest{
Name: name,
ClientId: "clientID",
ClientSecret: "clientSecret",
AuthorizationEndpoint: "https://example.com/oauth/v2/authorize",
TokenEndpoint: "https://example.com/oauth/v2/token",
UserEndpoint: "https://api.example.com/user",
Scopes: []string{"openid", "profile", "email"},
IdAttribute: "id",
ProviderOptions: &idp.Options{
IsLinkingAllowed: true,
IsCreationAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME,
},
})
logging.OnError(err).Panic("create generic OAuth idp")
/*
mustAwait(func() error {
_, err := i.Client.Mgmt.GetProviderByID(ctx, &mgmt.GetProviderByIDRequest{
Id: resp.GetId(),
})
return err
})
*/
return resp
}
func (i *Instance) AddSAMLProvider(ctx context.Context) string {
resp, err := i.Client.Admin.AddSAMLProvider(ctx, &admin.AddSAMLProviderRequest{
Name: "saml-idp",
Metadata: &admin.AddSAMLProviderRequest_MetadataXml{
MetadataXml: []byte("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-09-16T09:00:32.986Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
},
ProviderOptions: &idp.Options{
IsLinkingAllowed: true,
IsCreationAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
})
require.NoError(t, err)
return id
logging.OnError(err).Panic("create saml idp")
return resp.GetId()
}
func (s *Tester) AddSAMLRedirectProvider(t *testing.T, ctx context.Context, transientMappingAttributeName string) string {
ctx = authz.WithInstance(ctx, s.Instance)
id, _, err := s.Server.Commands.AddInstanceSAMLProvider(ctx, command.SAMLProvider{
Name: "saml-idp-redirect",
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
Metadata: []byte("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-09-16T09:00:32.986Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
TransientMappingAttributeName: transientMappingAttributeName,
IDPOptions: idp_rp.Options{
func (i *Instance) AddSAMLRedirectProvider(ctx context.Context, transientMappingAttributeName string) string {
resp, err := i.Client.Admin.AddSAMLProvider(ctx, &admin.AddSAMLProviderRequest{
Name: "saml-idp-redirect",
Binding: idp.SAMLBinding_SAML_BINDING_REDIRECT,
Metadata: &admin.AddSAMLProviderRequest_MetadataXml{
MetadataXml: []byte("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-09-16T09:00:32.986Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
},
TransientMappingAttributeName: &transientMappingAttributeName,
ProviderOptions: &idp.Options{
IsLinkingAllowed: true,
IsCreationAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
})
require.NoError(t, err)
return id
logging.OnError(err).Panic("create saml idp")
return resp.GetId()
}
func (s *Tester) AddSAMLPostProvider(t *testing.T, ctx context.Context) string {
ctx = authz.WithInstance(ctx, s.Instance)
id, _, err := s.Server.Commands.AddInstanceSAMLProvider(ctx, command.SAMLProvider{
Name: "saml-idp-post",
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
Metadata: []byte("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-09-16T09:00:32.986Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
IDPOptions: idp_rp.Options{
func (i *Instance) AddSAMLPostProvider(ctx context.Context) string {
resp, err := i.Client.Admin.AddSAMLProvider(ctx, &admin.AddSAMLProviderRequest{
Name: "saml-idp-post",
Binding: idp.SAMLBinding_SAML_BINDING_POST,
Metadata: &admin.AddSAMLProviderRequest_MetadataXml{
MetadataXml: []byte("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-09-16T09:00:32.986Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
},
ProviderOptions: &idp.Options{
IsLinkingAllowed: true,
IsCreationAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
})
require.NoError(t, err)
return id
logging.OnError(err).Panic("create saml idp")
return resp.GetId()
}
func (s *Tester) CreateIntent(t *testing.T, ctx context.Context, idpID string) string {
/*
func (s *Instance) CreateIntent(t *testing.T, ctx context.Context, idpID string) string {
resp, err := i.Client.UserV2.StartIdentityProviderIntent(ctx, &user.StartIdentityProviderIntentRequest{
IdpId: idpID,
Content: &user.StartIdentityProviderIntentRequest_Urls{
Urls: &user.RedirectURLs{
SuccessUrl: "https://example.com/success",
FailureUrl: "https://example.com/failure",
},
AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME,
},
})
logging.OnError(err).Fatal("create generic OAuth idp")
return resp
}
func (i *Instance) CreateIntent(t *testing.T, ctx context.Context, idpID string) string {
ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance)
writeModel, _, err := s.Commands.CreateIntent(ctx, idpID, "https://example.com/success", "https://example.com/failure", s.Instance.InstanceID())
require.NoError(t, err)
return writeModel.AggregateID
}
func (s *Tester) CreateSuccessfulOAuthIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) {
func (i *Instance) CreateSuccessfulOAuthIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) {
ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance)
intentID := s.CreateIntent(t, ctx, idpID)
writeModel, err := s.Commands.GetIntentWriteModel(ctx, intentID, s.Instance.InstanceID())
@@ -582,7 +542,7 @@ func (s *Tester) CreateSuccessfulOAuthIntent(t *testing.T, ctx context.Context,
return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence
}
func (s *Tester) CreateSuccessfulLDAPIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) {
func (s *Instance) CreateSuccessfulLDAPIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) {
ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance)
intentID := s.CreateIntent(t, ctx, idpID)
writeModel, err := s.Commands.GetIntentWriteModel(ctx, intentID, s.Instance.InstanceID())
@@ -610,7 +570,7 @@ func (s *Tester) CreateSuccessfulLDAPIntent(t *testing.T, ctx context.Context, i
return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence
}
func (s *Tester) CreateSuccessfulSAMLIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) {
func (s *Instance) CreateSuccessfulSAMLIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) {
ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance)
intentID := s.CreateIntent(t, ctx, idpID)
writeModel, err := s.Server.Commands.GetIntentWriteModel(ctx, intentID, s.Instance.InstanceID())
@@ -626,17 +586,18 @@ func (s *Tester) CreateSuccessfulSAMLIntent(t *testing.T, ctx context.Context, i
require.NoError(t, err)
return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence
}
*/
func (s *Tester) CreateVerifiedWebAuthNSession(t *testing.T, ctx context.Context, userID string) (id, token string, start, change time.Time) {
return s.CreateVerifiedWebAuthNSessionWithLifetime(t, ctx, userID, 0)
func (i *Instance) CreateVerifiedWebAuthNSession(t *testing.T, ctx context.Context, userID string) (id, token string, start, change time.Time) {
return i.CreateVerifiedWebAuthNSessionWithLifetime(t, ctx, userID, 0)
}
func (s *Tester) CreateVerifiedWebAuthNSessionWithLifetime(t *testing.T, ctx context.Context, userID string, lifetime time.Duration) (id, token string, start, change time.Time) {
func (i *Instance) CreateVerifiedWebAuthNSessionWithLifetime(t *testing.T, ctx context.Context, userID string, lifetime time.Duration) (id, token string, start, change time.Time) {
var sessionLifetime *durationpb.Duration
if lifetime > 0 {
sessionLifetime = durationpb.New(lifetime)
}
createResp, err := s.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
createResp, err := i.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{UserId: userID},
@@ -644,7 +605,7 @@ func (s *Tester) CreateVerifiedWebAuthNSessionWithLifetime(t *testing.T, ctx con
},
Challenges: &session.RequestChallenges{
WebAuthN: &session.RequestChallenges_WebAuthN{
Domain: s.Config.ExternalDomain,
Domain: i.Domain,
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
},
},
@@ -652,10 +613,10 @@ func (s *Tester) CreateVerifiedWebAuthNSessionWithLifetime(t *testing.T, ctx con
})
require.NoError(t, err)
assertion, err := s.WebAuthN.CreateAssertionResponse(createResp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
assertion, err := i.WebAuthN.CreateAssertionResponse(createResp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
require.NoError(t, err)
updateResp, err := s.Client.SessionV2.SetSession(ctx, &session.SetSessionRequest{
updateResp, err := i.Client.SessionV2.SetSession(ctx, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
WebAuthN: &session.CheckWebAuthN{
@@ -668,8 +629,8 @@ func (s *Tester) CreateVerifiedWebAuthNSessionWithLifetime(t *testing.T, ctx con
createResp.GetDetails().GetChangeDate().AsTime(), updateResp.GetDetails().GetChangeDate().AsTime()
}
func (s *Tester) CreatePasswordSession(t *testing.T, ctx context.Context, userID, password string) (id, token string, start, change time.Time) {
createResp, err := s.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
func (i *Instance) CreatePasswordSession(t *testing.T, ctx context.Context, userID, password string) (id, token string, start, change time.Time) {
createResp, err := i.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{UserId: userID},
@@ -684,8 +645,8 @@ func (s *Tester) CreatePasswordSession(t *testing.T, ctx context.Context, userID
createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime()
}
func (s *Tester) CreateProjectUserGrant(t *testing.T, ctx context.Context, projectID, userID string) string {
resp, err := s.Client.Mgmt.AddUserGrant(ctx, &mgmt.AddUserGrantRequest{
func (i *Instance) CreateProjectUserGrant(t *testing.T, ctx context.Context, projectID, userID string) string {
resp, err := i.Client.Mgmt.AddUserGrant(ctx, &mgmt.AddUserGrantRequest{
UserId: userID,
ProjectId: projectID,
})
@@ -693,16 +654,16 @@ func (s *Tester) CreateProjectUserGrant(t *testing.T, ctx context.Context, proje
return resp.GetUserGrantId()
}
func (s *Tester) CreateOrgMembership(t *testing.T, ctx context.Context, userID string) {
_, err := s.Client.Mgmt.AddOrgMember(ctx, &mgmt.AddOrgMemberRequest{
func (i *Instance) CreateOrgMembership(t *testing.T, ctx context.Context, userID string) {
_, err := i.Client.Mgmt.AddOrgMember(ctx, &mgmt.AddOrgMemberRequest{
UserId: userID,
Roles: []string{domain.RoleOrgOwner},
})
require.NoError(t, err)
}
func (s *Tester) CreateProjectMembership(t *testing.T, ctx context.Context, projectID, userID string) {
_, err := s.Client.Mgmt.AddProjectMember(ctx, &mgmt.AddProjectMemberRequest{
func (i *Instance) CreateProjectMembership(t *testing.T, ctx context.Context, projectID, userID string) {
_, err := i.Client.Mgmt.AddProjectMember(ctx, &mgmt.AddProjectMemberRequest{
ProjectId: projectID,
UserId: userID,
Roles: []string{domain.RoleProjectOwner},
@@ -710,13 +671,12 @@ func (s *Tester) CreateProjectMembership(t *testing.T, ctx context.Context, proj
require.NoError(t, err)
}
func (s *Tester) CreateTarget(ctx context.Context, t *testing.T, name, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse {
nameSet := fmt.Sprint(time.Now().UnixNano() + 1)
if name != "" {
nameSet = name
func (i *Instance) CreateTarget(ctx context.Context, t *testing.T, name, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse {
if name == "" {
name = gofakeit.Name()
}
reqTarget := &action.Target{
Name: nameSet,
Name: name,
Endpoint: endpoint,
Timeout: durationpb.New(10 * time.Second),
}
@@ -738,20 +698,20 @@ func (s *Tester) CreateTarget(ctx context.Context, t *testing.T, name, endpoint
RestAsync: &action.SetRESTAsync{},
}
}
target, err := s.Client.ActionV3Alpha.CreateTarget(ctx, &action.CreateTargetRequest{Target: reqTarget})
target, err := i.Client.ActionV3Alpha.CreateTarget(ctx, &action.CreateTargetRequest{Target: reqTarget})
require.NoError(t, err)
return target
}
func (s *Tester) DeleteExecution(ctx context.Context, t *testing.T, cond *action.Condition) {
_, err := s.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{
func (i *Instance) DeleteExecution(ctx context.Context, t *testing.T, cond *action.Condition) {
_, err := i.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{
Condition: cond,
})
require.NoError(t, err)
}
func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []*action.ExecutionTargetType) *action.SetExecutionResponse {
target, err := s.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{
func (i *Instance) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []*action.ExecutionTargetType) *action.SetExecutionResponse {
target, err := i.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{
Condition: cond,
Execution: &action.Execution{
Targets: targets,
@@ -761,15 +721,15 @@ func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Co
return target
}
func (s *Tester) CreateUserSchemaEmpty(ctx context.Context) *userschema_v3alpha.CreateUserSchemaResponse {
return s.CreateUserSchemaEmptyWithType(ctx, fmt.Sprint(time.Now().UnixNano()+1))
func (i *Instance) CreateUserSchemaEmpty(ctx context.Context) *userschema_v3alpha.CreateUserSchemaResponse {
return i.CreateUserSchemaEmptyWithType(ctx, fmt.Sprint(time.Now().UnixNano()+1))
}
func (s *Tester) CreateUserSchema(ctx context.Context, schemaData []byte) *userschema_v3alpha.CreateUserSchemaResponse {
func (i *Instance) CreateUserSchema(ctx context.Context, schemaData []byte) *userschema_v3alpha.CreateUserSchemaResponse {
userSchema := new(structpb.Struct)
err := userSchema.UnmarshalJSON(schemaData)
logging.OnError(err).Fatal("create userschema unmarshal")
schema, err := s.Client.UserSchemaV3.CreateUserSchema(ctx, &userschema_v3alpha.CreateUserSchemaRequest{
schema, err := i.Client.UserSchemaV3.CreateUserSchema(ctx, &userschema_v3alpha.CreateUserSchemaRequest{
UserSchema: &userschema_v3alpha.UserSchema{
Type: fmt.Sprint(time.Now().UnixNano() + 1),
DataType: &userschema_v3alpha.UserSchema_Schema{
@@ -781,7 +741,7 @@ func (s *Tester) CreateUserSchema(ctx context.Context, schemaData []byte) *users
return schema
}
func (s *Tester) CreateUserSchemaEmptyWithType(ctx context.Context, schemaType string) *userschema_v3alpha.CreateUserSchemaResponse {
func (i *Instance) CreateUserSchemaEmptyWithType(ctx context.Context, schemaType string) *userschema_v3alpha.CreateUserSchemaResponse {
userSchema := new(structpb.Struct)
err := userSchema.UnmarshalJSON([]byte(`{
"$schema": "urn:zitadel:schema:v1",
@@ -789,7 +749,7 @@ func (s *Tester) CreateUserSchemaEmptyWithType(ctx context.Context, schemaType s
"properties": {}
}`))
logging.OnError(err).Fatal("create userschema unmarshal")
schema, err := s.Client.UserSchemaV3.CreateUserSchema(ctx, &userschema_v3alpha.CreateUserSchemaRequest{
schema, err := i.Client.UserSchemaV3.CreateUserSchema(ctx, &userschema_v3alpha.CreateUserSchemaRequest{
UserSchema: &userschema_v3alpha.UserSchema{
Type: schemaType,
DataType: &userschema_v3alpha.UserSchema_Schema{
@@ -801,11 +761,11 @@ func (s *Tester) CreateUserSchemaEmptyWithType(ctx context.Context, schemaType s
return schema
}
func (s *Tester) CreateSchemaUser(ctx context.Context, orgID string, schemaID string, data []byte) *user_v3alpha.CreateUserResponse {
func (i *Instance) CreateSchemaUser(ctx context.Context, orgID string, schemaID string, data []byte) *user_v3alpha.CreateUserResponse {
userData := new(structpb.Struct)
err := userData.UnmarshalJSON(data)
logging.OnError(err).Fatal("create user unmarshal")
user, err := s.Client.UserV3Alpha.CreateUser(ctx, &user_v3alpha.CreateUserRequest{
user, err := i.Client.UserV3Alpha.CreateUser(ctx, &user_v3alpha.CreateUserRequest{
Organization: &object_v3alpha.Organization{Property: &object_v3alpha.Organization_OrgId{OrgId: orgID}},
User: &user_v3alpha.CreateUser{
SchemaId: schemaID,

View File

@@ -0,0 +1,53 @@
package integration
import (
"bytes"
_ "embed"
"os/exec"
"path/filepath"
"github.com/zitadel/logging"
"sigs.k8s.io/yaml"
)
type Config struct {
Log *logging.Config
Hostname string
Port uint16
Secure bool
LoginURLV2 string
LogoutURLV2 string
WebAuthNName string
}
var (
//go:embed config/client.yaml
clientYAML []byte
)
var (
tmpDir string
loadedConfig Config
)
// TmpDir returns the absolute path to the projects's temp directory.
func TmpDir() string {
return tmpDir
}
func init() {
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
out, err := cmd.Output()
if err != nil {
panic(err)
}
tmpDir = filepath.Join(string(bytes.TrimSpace(out)), "tmp")
if err := yaml.Unmarshal(clientYAML, &loadedConfig); err != nil {
panic(err)
}
if err := loadedConfig.Log.SetLogger(); err != nil {
panic(err)
}
SystemToken = systemUserToken()
}

View File

@@ -0,0 +1,10 @@
Log:
Level: info
Formatter:
Format: text
Hostname: localhost
Port: 8080
Secure: false
LoginURLV2: "/login?authRequest="
LogoutURLV2: "/logout?post_logout_redirect="
WebAuthNName: ZITADEL

View File

@@ -14,7 +14,7 @@ services:
- PGUSER=zitadel
- POSTGRES_DB=zitadel
- POSTGRES_HOST_AUTH_METHOD=trust
command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all
command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all -c shared_buffers=1GB -c work_mem=16MB -c effective_io_concurrency=100 -c wal_level=minimal -c archive_mode=off -c max_wal_senders=0
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: '10s'

View File

@@ -1,10 +1,12 @@
Database:
EventPushConnRatio: 0.2 # 4
ProjectionSpoolerConnRatio: 0.3 # 6
postgres:
Host: localhost
Port: 5432
Database: zitadel
MaxOpenConns: 15
MaxIdleConns: 10
MaxOpenConns: 20
MaxIdleConns: 20
User:
Username: zitadel
SSL:

View File

@@ -0,0 +1,13 @@
FirstInstance:
Skip: false
PatPath: tmp/admin-pat.txt
InstanceName: ZITADEL
DefaultLanguage: en
Org:
Name: ZITADEL
Machine:
Machine:
Username: boss
Name: boss
Pat:
ExpirationDate: 2099-01-01T00:00:00Z

View File

@@ -1,15 +1,19 @@
Log:
Level: debug
Level: info
ExternalSecure: false
TLS:
Enabled: false
Quotas:
Access:
Enabled: true
Telemetry:
Enabled: true
Endpoints:
- http://localhost:8081
- http://localhost:8081/milestone
Headers:
single-value: "single-value"
multi-value:
@@ -27,14 +31,15 @@ LogStore:
Enabled: true
Projections:
HandleActiveInstances: 60s
HandleActiveInstances: 30m
RequeueEvery: 5s
TransactionDuration: 1m
Customizations:
NotificationsQuotas:
RequeueEvery: 1s
telemetry:
HandleActiveInstances: 60s
Telemetry:
RequeueEvery: 5s
HandleActiveInstances: 60s
RequeueEvery: 1s
DefaultInstance:
LoginPolicy:

View File

@@ -0,0 +1,354 @@
// Package integration provides helpers for integration testing.
package integration
import (
"bytes"
"context"
_ "embed"
"errors"
"fmt"
"os"
"path/filepath"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/zitadel/logging"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/proto"
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/webauthn"
"github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/auth"
"github.com/zitadel/zitadel/pkg/grpc/instance"
"github.com/zitadel/zitadel/pkg/grpc/management"
"github.com/zitadel/zitadel/pkg/grpc/org"
"github.com/zitadel/zitadel/pkg/grpc/system"
"github.com/zitadel/zitadel/pkg/grpc/user"
user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
// NotEmpty can be used as placeholder, when the returned values is unknown.
// It can be used in tests to assert whether a value should be empty or not.
const NotEmpty = "not empty"
const (
adminPATFile = "admin-pat.txt"
)
// UserType provides constants that give
// a short explanation with the purpose
// a service user.
// This allows to pre-create users with
// different permissions and reuse them.
type UserType int
//go:generate enumer -type UserType -transform snake -trimprefix UserType
const (
UserTypeUnspecified UserType = iota
UserTypeIAMOwner
UserTypeOrgOwner
UserTypeLogin
)
const (
UserPassword = "VeryS3cret!"
)
const (
PortMilestoneServer = "8081"
PortQuotaServer = "8082"
)
// User information with a Personal Access Token.
type User struct {
ID string
Username string
Token string
}
type UserMap map[UserType]*User
func (m UserMap) Set(typ UserType, user *User) {
m[typ] = user
}
func (m UserMap) Get(typ UserType) *User {
return m[typ]
}
// Host returns the primary host of zitadel, on which the first instance is served.
// http://localhost:8080 by default
func (c *Config) Host() string {
return fmt.Sprintf("%s:%d", c.Hostname, c.Port)
}
// Instance is a Zitadel server and client with all resources available for testing.
type Instance struct {
Config Config
Domain string
Instance *instance.InstanceDetail
DefaultOrg *org.Org
Users UserMap
AdminUserID string // First human user for password login
Client *Client
WebAuthN *webauthn.Client
}
// GetFirstInstance returns the default instance and org information,
// with authorized machine users.
// Using the first instance is not recommended as parallel test might
// interfere with each other.
// It is recommended to use [NewInstance] instead.
func GetFirstInstance(ctx context.Context) *Instance {
i := &Instance{
Config: loadedConfig,
Domain: loadedConfig.Hostname,
}
token := loadInstanceOwnerPAT()
i.setClient(ctx)
i.setupInstance(ctx, token)
return i
}
// NewInstance returns a new instance that can be used for integration tests.
// The instance contains a gRPC client connected to the domain of this instance.
// The included users are the IAM_OWNER, ORG_OWNER of the default org and
// a Login client user.
//
// The instance is isolated and is safe for parallel testing.
func NewInstance(ctx context.Context) *Instance {
primaryDomain := RandString(5) + ".integration.localhost"
ctx = WithSystemAuthorization(ctx)
resp, err := SystemClient().CreateInstance(ctx, &system.CreateInstanceRequest{
InstanceName: "testinstance",
CustomDomain: primaryDomain,
Owner: &system.CreateInstanceRequest_Machine_{
Machine: &system.CreateInstanceRequest_Machine{
UserName: "owner",
Name: "owner",
PersonalAccessToken: &system.CreateInstanceRequest_PersonalAccessToken{},
},
},
})
if err != nil {
panic(err)
}
i := &Instance{
Config: loadedConfig,
Domain: primaryDomain,
}
i.setClient(ctx)
i.awaitFirstUser(WithAuthorizationToken(ctx, resp.GetPat()))
i.setupInstance(ctx, resp.GetPat())
return i
}
func (i *Instance) ID() string {
return i.Instance.GetId()
}
func (i *Instance) awaitFirstUser(ctx context.Context) {
var allErrs []error
for {
resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{
Username: proto.String("zitadel-admin@zitadel.localhost"),
Profile: &user_v2.SetHumanProfile{
GivenName: "hodor",
FamilyName: "hodor",
NickName: proto.String("hodor"),
},
Email: &user_v2.SetHumanEmail{
Email: "zitadel-admin@zitadel.localhost",
Verification: &user_v2.SetHumanEmail_IsVerified{
IsVerified: true,
},
},
PasswordType: &user_v2.AddHumanUserRequest_Password{
Password: &user_v2.Password{
Password: "Password1!",
ChangeRequired: false,
},
},
})
if err == nil {
i.AdminUserID = resp.GetUserId()
return
}
logging.WithError(err).Debug("await first instance user")
allErrs = append(allErrs, err)
select {
case <-ctx.Done():
panic(errors.Join(append(allErrs, ctx.Err())...))
case <-time.After(time.Second):
continue
}
}
}
func (i *Instance) setupInstance(ctx context.Context, token string) {
i.Users = make(UserMap)
ctx = WithAuthorizationToken(ctx, token)
i.setInstance(ctx)
i.setOrganization(ctx)
i.createMachineUserInstanceOwner(ctx, token)
i.createMachineUserOrgOwner(ctx)
i.createLoginClient(ctx)
i.createWebAuthNClient()
}
// Host returns the primary Domain of the instance with the port.
func (i *Instance) Host() string {
return fmt.Sprintf("%s:%d", i.Domain, i.Config.Port)
}
func loadInstanceOwnerPAT() string {
data, err := os.ReadFile(filepath.Join(tmpDir, adminPATFile))
if err != nil {
panic(err)
}
return string(bytes.TrimSpace(data))
}
func (i *Instance) createMachineUserInstanceOwner(ctx context.Context, token string) {
mustAwait(func() error {
user, err := i.Client.Auth.GetMyUser(WithAuthorizationToken(ctx, token), &auth.GetMyUserRequest{})
if err != nil {
return err
}
i.Users.Set(UserTypeIAMOwner, &User{
ID: user.GetUser().GetId(),
Username: user.GetUser().GetUserName(),
Token: token,
})
return nil
})
}
func (i *Instance) createMachineUserOrgOwner(ctx context.Context) {
_, err := i.Client.Mgmt.AddOrgMember(ctx, &management.AddOrgMemberRequest{
UserId: i.createMachineUser(ctx, UserTypeOrgOwner),
Roles: []string{"ORG_OWNER"},
})
if err != nil {
panic(err)
}
}
func (i *Instance) createLoginClient(ctx context.Context) {
i.createMachineUser(ctx, UserTypeLogin)
}
func (i *Instance) setClient(ctx context.Context) {
client, err := newClient(ctx, i.Host())
if err != nil {
panic(err)
}
i.Client = client
}
func (i *Instance) setInstance(ctx context.Context) {
mustAwait(func() error {
instance, err := i.Client.Admin.GetMyInstance(ctx, &admin.GetMyInstanceRequest{})
i.Instance = instance.GetInstance()
return err
})
}
func (i *Instance) setOrganization(ctx context.Context) {
mustAwait(func() error {
resp, err := i.Client.Mgmt.GetMyOrg(ctx, &management.GetMyOrgRequest{})
i.DefaultOrg = resp.GetOrg()
return err
})
}
func (i *Instance) createMachineUser(ctx context.Context, userType UserType) (userID string) {
mustAwait(func() error {
username := gofakeit.Username()
userResp, err := i.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{
UserName: username,
Name: username,
Description: userType.String(),
AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT,
})
if err != nil {
return err
}
userID = userResp.GetUserId()
patResp, err := i.Client.Mgmt.AddPersonalAccessToken(ctx, &management.AddPersonalAccessTokenRequest{
UserId: userID,
})
if err != nil {
return err
}
i.Users.Set(userType, &User{
ID: userID,
Username: username,
Token: patResp.GetToken(),
})
return nil
})
return userID
}
func (i *Instance) createWebAuthNClient() {
i.WebAuthN = webauthn.NewClient(i.Config.WebAuthNName, i.Domain, http_util.BuildOrigin(i.Host(), i.Config.Secure))
}
func (i *Instance) WithAuthorization(ctx context.Context, u UserType) context.Context {
return i.WithInstanceAuthorization(ctx, u)
}
func (i *Instance) WithInstanceAuthorization(ctx context.Context, u UserType) context.Context {
return WithAuthorizationToken(ctx, i.Users.Get(u).Token)
}
func (i *Instance) GetUserID(u UserType) string {
return i.Users.Get(u).ID
}
func WithAuthorizationToken(ctx context.Context, token string) context.Context {
md, ok := metadata.FromOutgoingContext(ctx)
if !ok {
md = make(metadata.MD)
}
md.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return metadata.NewOutgoingContext(ctx, md)
}
func (i *Instance) BearerToken(ctx context.Context) string {
md, ok := metadata.FromOutgoingContext(ctx)
if !ok {
return ""
}
return md.Get("Authorization")[0]
}
func (i *Instance) WithSystemAuthorizationHTTP(u UserType) map[string]string {
return map[string]string{"Authorization": fmt.Sprintf("Bearer %s", i.Users.Get(u).Token)}
}
func await(af func() error) error {
maxTimer := time.NewTimer(15 * time.Minute)
for {
err := af()
if err == nil {
return nil
}
select {
case <-maxTimer.C:
return err
case <-time.After(time.Second):
continue
}
}
}
func mustAwait(af func() error) {
if err := await(af); err != nil {
panic(err)
}
}

View File

@@ -1,449 +0,0 @@
// Package integration provides helpers for integration testing.
package integration
import (
"bytes"
"context"
"database/sql"
_ "embed"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strings"
"sync"
"time"
"github.com/spf13/viper"
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v3/pkg/client"
"github.com/zitadel/oidc/v3/pkg/oidc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"github.com/zitadel/zitadel/cmd"
"github.com/zitadel/zitadel/cmd/start"
"github.com/zitadel/zitadel/internal/api/authz"
http_util "github.com/zitadel/zitadel/internal/api/http"
z_oidc "github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/net"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/webauthn"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/grpc/admin"
)
var (
//go:embed config/zitadel.yaml
zitadelYAML []byte
//go:embed config/cockroach.yaml
cockroachYAML []byte
//go:embed config/postgres.yaml
postgresYAML []byte
//go:embed config/system-user-key.pem
systemUserKey []byte
)
// NotEmpty can be used as placeholder, when the returned values is unknown.
// It can be used in tests to assert whether a value should be empty or not.
const NotEmpty = "not empty"
// UserType provides constants that give
// a short explinanation with the purpose
// a serverice user.
// This allows to pre-create users with
// different permissions and reuse them.
type UserType int
//go:generate stringer -type=UserType
const (
Unspecified UserType = iota
OrgOwner
Login
IAMOwner
SystemUser // SystemUser is a user with access to the system service.
)
const (
FirstInstanceUsersKey = "first"
UserPassword = "VeryS3cret!"
)
const (
PortMilestoneServer = "8081"
PortQuotaServer = "8082"
)
// User information with a Personal Access Token.
type User struct {
*query.User
Token string
}
type InstanceUserMap map[string]map[UserType]*User
func (m InstanceUserMap) Set(instanceID string, typ UserType, user *User) {
if m[instanceID] == nil {
m[instanceID] = make(map[UserType]*User)
}
m[instanceID][typ] = user
}
func (m InstanceUserMap) Get(instanceID string, typ UserType) *User {
if users, ok := m[instanceID]; ok {
return users[typ]
}
return nil
}
// Tester is a Zitadel server and client with all resources available for testing.
type Tester struct {
*start.Server
Instance authz.Instance
Organisation *query.Org
Users InstanceUserMap
MilestoneChan chan []byte
milestoneServer *httptest.Server
QuotaNotificationChan chan []byte
quotaNotificationServer *httptest.Server
Client Client
WebAuthN *webauthn.Client
wg sync.WaitGroup // used for shutdown
}
const commandLine = `start --masterkeyFromEnv`
func (s *Tester) Host() string {
return fmt.Sprintf("%s:%d", s.Config.ExternalDomain, s.Config.Port)
}
func (s *Tester) createClientConn(ctx context.Context, target string) {
cc, err := grpc.DialContext(ctx, target,
grpc.WithBlock(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
s.Shutdown <- os.Interrupt
s.wg.Wait()
}
logging.OnError(err).Fatal("integration tester client dial")
logging.New().WithField("target", target).Info("finished dialing grpc client conn")
s.Client = newClient(cc)
err = s.pollHealth(ctx)
logging.OnError(err).Fatal("integration tester health")
}
// pollHealth waits until a healthy status is reported.
// TODO: remove when we make the setup blocking on all
// projections completed.
func (s *Tester) pollHealth(ctx context.Context) (err error) {
for {
err = func(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
_, err := s.Client.Admin.Healthz(ctx, &admin.HealthzRequest{})
return err
}(ctx)
if err == nil {
return nil
}
logging.WithError(err).Info("poll healthz")
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Second):
continue
}
}
}
const (
LoginUser = "loginClient"
MachineUserOrgOwner = "integrationOrgOwner"
MachineUserInstanceOwner = "integrationInstanceOwner"
)
func (s *Tester) createMachineUserOrgOwner(ctx context.Context) {
var err error
ctx, user := s.createMachineUser(ctx, MachineUserOrgOwner, OrgOwner)
_, err = s.Commands.AddOrgMember(ctx, user.ResourceOwner, user.ID, "ORG_OWNER")
target := new(zerrors.AlreadyExistsError)
if !errors.As(err, &target) {
logging.OnError(err).Fatal("add org member")
}
}
func (s *Tester) createMachineUserInstanceOwner(ctx context.Context) {
var err error
ctx, user := s.createMachineUser(ctx, MachineUserInstanceOwner, IAMOwner)
_, err = s.Commands.AddInstanceMember(ctx, user.ID, "IAM_OWNER")
target := new(zerrors.AlreadyExistsError)
if !errors.As(err, &target) {
logging.OnError(err).Fatal("add instance member")
}
}
func (s *Tester) createLoginClient(ctx context.Context) {
s.createMachineUser(ctx, LoginUser, Login)
}
func (s *Tester) createMachineUser(ctx context.Context, username string, userType UserType) (context.Context, *query.User) {
var err error
ctx = s.updateInstanceAndOrg(ctx, s.Host())
usernameQuery, err := query.NewUserUsernameSearchQuery(username, query.TextEquals)
logging.OnError(err).Fatal("user query")
user, err := s.Queries.GetUser(ctx, true, usernameQuery)
if errors.Is(err, sql.ErrNoRows) {
_, err = s.Commands.AddMachine(ctx, &command.Machine{
ObjectRoot: models.ObjectRoot{
ResourceOwner: s.Organisation.ID,
},
Username: username,
Name: username,
Description: "who cares?",
AccessTokenType: domain.OIDCTokenTypeJWT,
})
logging.WithFields("username", username).OnError(err).Fatal("add machine user")
user, err = s.Queries.GetUser(ctx, true, usernameQuery)
}
logging.WithFields("username", username).OnError(err).Fatal("get user")
scopes := []string{oidc.ScopeOpenID, oidc.ScopeProfile, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner}
pat := command.NewPersonalAccessToken(user.ResourceOwner, user.ID, time.Now().Add(time.Hour), scopes, domain.UserTypeMachine)
_, err = s.Commands.AddPersonalAccessToken(ctx, pat)
logging.WithFields("username", SystemUser).OnError(err).Fatal("add pat")
s.Users.Set(FirstInstanceUsersKey, userType, &User{
User: user,
Token: pat.Token,
})
return ctx, user
}
func (s *Tester) WithAuthorization(ctx context.Context, u UserType) context.Context {
return s.WithInstanceAuthorization(ctx, u, FirstInstanceUsersKey)
}
func (s *Tester) WithInstanceAuthorization(ctx context.Context, u UserType, instanceID string) context.Context {
if u == SystemUser {
s.ensureSystemUser()
}
return s.WithAuthorizationToken(ctx, s.Users.Get(instanceID, u).Token)
}
func (s *Tester) GetUserID(u UserType) string {
if u == SystemUser {
s.ensureSystemUser()
}
return s.Users.Get(FirstInstanceUsersKey, u).ID
}
func (s *Tester) WithAuthorizationToken(ctx context.Context, token string) context.Context {
md, ok := metadata.FromOutgoingContext(ctx)
if !ok {
md = make(metadata.MD)
}
md.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return metadata.NewOutgoingContext(ctx, md)
}
func (s *Tester) BearerToken(ctx context.Context) string {
md, ok := metadata.FromOutgoingContext(ctx)
if !ok {
return ""
}
return md.Get("Authorization")[0]
}
func (s *Tester) ensureSystemUser() {
const ISSUER = "tester"
if s.Users.Get(FirstInstanceUsersKey, SystemUser) != nil {
return
}
audience := http_util.BuildOrigin(s.Host(), s.Server.Config.ExternalSecure)
signer, err := client.NewSignerFromPrivateKeyByte(systemUserKey, "")
logging.OnError(err).Fatal("system key signer")
jwt, err := client.SignedJWTProfileAssertion(ISSUER, []string{audience}, time.Hour, signer)
logging.OnError(err).Fatal("system key jwt")
s.Users.Set(FirstInstanceUsersKey, SystemUser, &User{Token: jwt})
}
func (s *Tester) WithSystemAuthorizationHTTP(u UserType) map[string]string {
return map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.Users.Get(FirstInstanceUsersKey, u).Token)}
}
// Done send an interrupt signal to cleanly shutdown the server.
func (s *Tester) Done() {
err := s.Client.CC.Close()
logging.OnError(err).Error("integration tester client close")
s.Shutdown <- os.Interrupt
s.wg.Wait()
s.milestoneServer.Close()
s.quotaNotificationServer.Close()
}
// NewTester start a new Zitadel server by passing the default commandline.
// The server will listen on the configured port.
// The database configuration that will be used can be set by the
// INTEGRATION_DB_FLAVOR environment variable and can have the values "cockroach"
// or "postgres". Defaults to "cockroach".
//
// The default Instance and Organisation are read from the DB and system
// users are created as needed.
//
// After the server is started, a [grpc.ClientConn] will be created and
// the server is polled for it's health status.
//
// Note: the database must already be setup and initialized before
// using NewTester. See the CONTRIBUTING.md document for details.
func NewTester(ctx context.Context, zitadelConfigYAML ...string) *Tester {
args := strings.Split(commandLine, " ")
sc := make(chan *start.Server)
//nolint:contextcheck
cmd := cmd.New(os.Stdout, os.Stdin, args, sc)
cmd.SetArgs(args)
for _, yaml := range append([]string{string(zitadelYAML)}, zitadelConfigYAML...) {
err := viper.MergeConfig(bytes.NewBuffer([]byte(yaml)))
logging.OnError(err).Fatal()
}
var err error
flavor := os.Getenv("INTEGRATION_DB_FLAVOR")
switch flavor {
case "cockroach", "":
err = viper.MergeConfig(bytes.NewBuffer(cockroachYAML))
case "postgres":
err = viper.MergeConfig(bytes.NewBuffer(postgresYAML))
default:
logging.New().WithField("flavor", flavor).Fatal("unknown db flavor set in INTEGRATION_DB_FLAVOR")
}
logging.OnError(err).Fatal()
tester := Tester{
Users: make(InstanceUserMap),
}
tester.MilestoneChan = make(chan []byte, 100)
tester.milestoneServer, err = runMilestoneServer(ctx, tester.MilestoneChan)
logging.OnError(err).Fatal()
tester.QuotaNotificationChan = make(chan []byte, 100)
tester.quotaNotificationServer, err = runQuotaServer(ctx, tester.QuotaNotificationChan)
logging.OnError(err).Fatal()
tester.wg.Add(1)
go func(wg *sync.WaitGroup) {
logging.OnError(cmd.Execute()).Fatal()
wg.Done()
}(&tester.wg)
select {
case tester.Server = <-sc:
case <-ctx.Done():
logging.OnError(ctx.Err()).Fatal("waiting for integration tester server")
}
host := tester.Host()
tester.createClientConn(ctx, host)
tester.createLoginClient(ctx)
tester.WebAuthN = webauthn.NewClient(tester.Config.WebAuthNName, tester.Config.ExternalDomain, http_util.BuildOrigin(host, tester.Config.ExternalSecure))
tester.createMachineUserOrgOwner(ctx)
tester.createMachineUserInstanceOwner(ctx)
tester.WebAuthN = webauthn.NewClient(tester.Config.WebAuthNName, tester.Config.ExternalDomain, "https://"+tester.Host())
return &tester
}
func Contexts(timeout time.Duration) (ctx, errCtx context.Context, cancel context.CancelFunc) {
errCtx, cancel = context.WithCancel(context.Background())
cancel()
ctx, cancel = context.WithTimeout(context.Background(), timeout)
return ctx, errCtx, cancel
}
func runMilestoneServer(ctx context.Context, bodies chan []byte) (*httptest.Server, error) {
mockServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if r.Header.Get("single-value") != "single-value" {
http.Error(w, "single-value header not set", http.StatusInternalServerError)
return
}
if reflect.DeepEqual(r.Header.Get("multi-value"), "multi-value-1,multi-value-2") {
http.Error(w, "single-value header not set", http.StatusInternalServerError)
return
}
bodies <- body
w.WriteHeader(http.StatusOK)
}))
config := net.ListenConfig()
listener, err := config.Listen(ctx, "tcp", ":"+PortMilestoneServer)
if err != nil {
return nil, err
}
mockServer.Listener = listener
mockServer.Start()
return mockServer, nil
}
func runQuotaServer(ctx context.Context, bodies chan []byte) (*httptest.Server, error) {
mockServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
bodies <- body
w.WriteHeader(http.StatusOK)
}))
config := net.ListenConfig()
listener, err := config.Listen(ctx, "tcp", ":"+PortQuotaServer)
if err != nil {
return nil, err
}
mockServer.Listener = listener
mockServer.Start()
return mockServer, nil
}
func (s *Tester) updateInstanceAndOrg(ctx context.Context, domain string) context.Context {
var err error
s.Instance, err = s.Queries.InstanceByHost(ctx, domain, "")
logging.OnError(err).Fatal("query instance")
ctx = authz.WithInstance(ctx, s.Instance)
s.Organisation, err = s.Queries.OrgByID(ctx, true, s.Instance.DefaultOrganisationID())
logging.OnError(err).Fatal("query organisation")
return ctx
}
func await(af func() error) error {
maxTimer := time.NewTimer(15 * time.Minute)
for {
err := af()
if err == nil {
return nil
}
select {
case <-maxTimer.C:
return err
case <-time.After(time.Second / 10):
continue
}
}
}

View File

@@ -1,16 +0,0 @@
//go:build integration
package integration
import (
"testing"
"time"
)
func TestNewTester(t *testing.T) {
ctx, _, cancel := Contexts(time.Hour)
defer cancel()
s := NewTester(ctx)
defer s.Done()
}

View File

@@ -24,11 +24,11 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/user"
)
func (s *Tester) CreateOIDCClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, appType app.OIDCAppType, authMethod app.OIDCAuthMethodType, devMode bool, grantTypes ...app.OIDCGrantType) (*management.AddOIDCAppResponse, error) {
func (i *Instance) CreateOIDCClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, appType app.OIDCAppType, authMethod app.OIDCAuthMethodType, devMode bool, grantTypes ...app.OIDCGrantType) (*management.AddOIDCAppResponse, error) {
if len(grantTypes) == 0 {
grantTypes = []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN}
}
resp, err := s.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{
resp, err := i.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{
ProjectId: projectID,
Name: fmt.Sprintf("app-%d", time.Now().UnixNano()),
RedirectUris: []string{redirectURI},
@@ -51,7 +51,7 @@ func (s *Tester) CreateOIDCClient(ctx context.Context, redirectURI, logoutRedire
return nil, err
}
return resp, await(func() error {
_, err := s.Client.Mgmt.GetAppByID(ctx, &management.GetAppByIDRequest{
_, err := i.Client.Mgmt.GetAppByID(ctx, &management.GetAppByIDRequest{
ProjectId: projectID,
AppId: resp.GetAppId(),
})
@@ -59,20 +59,20 @@ func (s *Tester) CreateOIDCClient(ctx context.Context, redirectURI, logoutRedire
})
}
func (s *Tester) CreateOIDCNativeClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, devMode bool) (*management.AddOIDCAppResponse, error) {
return s.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, devMode)
func (i *Instance) CreateOIDCNativeClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, devMode bool) (*management.AddOIDCAppResponse, error) {
return i.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, devMode)
}
func (s *Tester) CreateOIDCWebClientBasic(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (*management.AddOIDCAppResponse, error) {
return s.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_WEB, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, false)
func (i *Instance) CreateOIDCWebClientBasic(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (*management.AddOIDCAppResponse, error) {
return i.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_WEB, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, false)
}
func (s *Tester) CreateOIDCWebClientJWT(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, grantTypes ...app.OIDCGrantType) (client *management.AddOIDCAppResponse, keyData []byte, err error) {
client, err = s.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_WEB, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, false, grantTypes...)
func (i *Instance) CreateOIDCWebClientJWT(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, grantTypes ...app.OIDCGrantType) (client *management.AddOIDCAppResponse, keyData []byte, err error) {
client, err = i.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_WEB, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, false, grantTypes...)
if err != nil {
return nil, nil, err
}
key, err := s.Client.Mgmt.AddAppKey(ctx, &management.AddAppKeyRequest{
key, err := i.Client.Mgmt.AddAppKey(ctx, &management.AddAppKeyRequest{
ProjectId: projectID,
AppId: client.GetAppId(),
Type: authn.KeyType_KEY_TYPE_JSON,
@@ -81,15 +81,23 @@ func (s *Tester) CreateOIDCWebClientJWT(ctx context.Context, redirectURI, logout
if err != nil {
return nil, nil, err
}
mustAwait(func() error {
_, err := i.Client.Mgmt.GetAppByID(ctx, &management.GetAppByIDRequest{
ProjectId: projectID,
AppId: client.GetAppId(),
})
return err
})
return client, key.GetKeyDetails(), nil
}
func (s *Tester) CreateOIDCInactivateClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (*management.AddOIDCAppResponse, error) {
client, err := s.CreateOIDCNativeClient(ctx, redirectURI, logoutRedirectURI, projectID, false)
func (i *Instance) CreateOIDCInactivateClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (*management.AddOIDCAppResponse, error) {
client, err := i.CreateOIDCNativeClient(ctx, redirectURI, logoutRedirectURI, projectID, false)
if err != nil {
return nil, err
}
_, err = s.Client.Mgmt.DeactivateApp(ctx, &management.DeactivateAppRequest{
_, err = i.Client.Mgmt.DeactivateApp(ctx, &management.DeactivateAppRequest{
ProjectId: projectID,
AppId: client.GetAppId(),
})
@@ -99,14 +107,14 @@ func (s *Tester) CreateOIDCInactivateClient(ctx context.Context, redirectURI, lo
return client, err
}
func (s *Tester) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI string) (*management.AddOIDCAppResponse, error) {
project, err := s.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
func (i *Instance) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI string) (*management.AddOIDCAppResponse, error) {
project, err := i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),
})
if err != nil {
return nil, err
}
resp, err := s.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{
resp, err := i.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{
ProjectId: project.GetId(),
Name: fmt.Sprintf("app-%d", time.Now().UnixNano()),
RedirectUris: []string{redirectURI},
@@ -129,7 +137,7 @@ func (s *Tester) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI s
return nil, err
}
return resp, await(func() error {
_, err := s.Client.Mgmt.GetAppByID(ctx, &management.GetAppByIDRequest{
_, err := i.Client.Mgmt.GetAppByID(ctx, &management.GetAppByIDRequest{
ProjectId: project.GetId(),
AppId: resp.GetAppId(),
})
@@ -137,32 +145,32 @@ func (s *Tester) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI s
})
}
func (s *Tester) CreateOIDCTokenExchangeClient(ctx context.Context) (client *management.AddOIDCAppResponse, keyData []byte, err error) {
project, err := s.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
func (i *Instance) CreateOIDCTokenExchangeClient(ctx context.Context) (client *management.AddOIDCAppResponse, keyData []byte, err error) {
project, err := i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),
})
if err != nil {
return nil, nil, err
}
return s.CreateOIDCWebClientJWT(ctx, "", "", project.GetId(), app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE, app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN)
return i.CreateOIDCWebClientJWT(ctx, "", "", project.GetId(), app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE, app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN)
}
func (s *Tester) CreateProject(ctx context.Context) (*management.AddProjectResponse, error) {
return s.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
func (i *Instance) CreateProject(ctx context.Context) (*management.AddProjectResponse, error) {
return i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),
})
}
func (s *Tester) CreateAPIClientJWT(ctx context.Context, projectID string) (*management.AddAPIAppResponse, error) {
return s.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{
func (i *Instance) CreateAPIClientJWT(ctx context.Context, projectID string) (*management.AddAPIAppResponse, error) {
return i.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{
ProjectId: projectID,
Name: fmt.Sprintf("api-%d", time.Now().UnixNano()),
AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
})
}
func (s *Tester) CreateAPIClientBasic(ctx context.Context, projectID string) (*management.AddAPIAppResponse, error) {
return s.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{
func (i *Instance) CreateAPIClientBasic(ctx context.Context, projectID string) (*management.AddAPIAppResponse, error) {
return i.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{
ProjectId: projectID,
Name: fmt.Sprintf("api-%d", time.Now().UnixNano()),
AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC,
@@ -171,36 +179,36 @@ func (s *Tester) CreateAPIClientBasic(ctx context.Context, projectID string) (*m
const CodeVerifier = "codeVerifier"
func (s *Tester) CreateOIDCAuthRequest(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) {
return s.CreateOIDCAuthRequestWithDomain(ctx, s.Config.ExternalDomain, clientID, loginClient, redirectURI, scope...)
func (i *Instance) CreateOIDCAuthRequest(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) {
return i.CreateOIDCAuthRequestWithDomain(ctx, i.Domain, clientID, loginClient, redirectURI, scope...)
}
func (s *Tester) CreateOIDCAuthRequestWithDomain(ctx context.Context, domain, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) {
provider, err := s.CreateRelyingPartyForDomain(ctx, domain, clientID, redirectURI, scope...)
func (i *Instance) CreateOIDCAuthRequestWithDomain(ctx context.Context, domain, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) {
provider, err := i.CreateRelyingPartyForDomain(ctx, domain, clientID, redirectURI, scope...)
if err != nil {
return "", err
return "", fmt.Errorf("create relying party: %w", err)
}
codeChallenge := oidc.NewSHACodeChallenge(CodeVerifier)
authURL := rp.AuthURL("state", provider, rp.WithCodeChallenge(codeChallenge))
req, err := GetRequest(authURL, map[string]string{oidc_internal.LoginClientHeader: loginClient})
if err != nil {
return "", err
return "", fmt.Errorf("get request: %w", err)
}
loc, err := CheckRedirect(req)
if err != nil {
return "", err
return "", fmt.Errorf("check redirect: %w", err)
}
prefixWithHost := provider.Issuer() + s.Config.OIDC.DefaultLoginURLV2
prefixWithHost := provider.Issuer() + i.Config.LoginURLV2
if !strings.HasPrefix(loc.String(), prefixWithHost) {
return "", fmt.Errorf("login location has not prefix %s, but is %s", prefixWithHost, loc.String())
}
return strings.TrimPrefix(loc.String(), prefixWithHost), nil
}
func (s *Tester) CreateOIDCAuthRequestImplicit(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) {
provider, err := s.CreateRelyingParty(ctx, clientID, redirectURI, scope...)
func (i *Instance) CreateOIDCAuthRequestImplicit(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) {
provider, err := i.CreateRelyingParty(ctx, clientID, redirectURI, scope...)
if err != nil {
return "", err
}
@@ -224,48 +232,49 @@ func (s *Tester) CreateOIDCAuthRequestImplicit(ctx context.Context, clientID, lo
return "", err
}
prefixWithHost := provider.Issuer() + s.Config.OIDC.DefaultLoginURLV2
prefixWithHost := provider.Issuer() + i.Config.LoginURLV2
if !strings.HasPrefix(loc.String(), prefixWithHost) {
return "", fmt.Errorf("login location has not prefix %s, but is %s", prefixWithHost, loc.String())
}
return strings.TrimPrefix(loc.String(), prefixWithHost), nil
}
func (s *Tester) OIDCIssuer() string {
return http_util.BuildHTTP(s.Config.ExternalDomain, s.Config.Port, s.Config.ExternalSecure)
func (i *Instance) OIDCIssuer() string {
return http_util.BuildHTTP(i.Domain, i.Config.Port, i.Config.Secure)
}
func (s *Tester) CreateRelyingParty(ctx context.Context, clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) {
return s.CreateRelyingPartyForDomain(ctx, s.Config.ExternalDomain, clientID, redirectURI, scope...)
func (i *Instance) CreateRelyingParty(ctx context.Context, clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) {
return i.CreateRelyingPartyForDomain(ctx, i.Domain, clientID, redirectURI, scope...)
}
func (s *Tester) CreateRelyingPartyForDomain(ctx context.Context, domain, clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) {
func (i *Instance) CreateRelyingPartyForDomain(ctx context.Context, domain, clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) {
if len(scope) == 0 {
scope = []string{oidc.ScopeOpenID}
}
loginClient := &http.Client{Transport: &loginRoundTripper{http.DefaultTransport}}
return rp.NewRelyingPartyOIDC(ctx, http_util.BuildHTTP(domain, s.Config.Port, s.Config.ExternalSecure), clientID, "", redirectURI, scope, rp.WithHTTPClient(loginClient))
loginClient := &http.Client{Transport: &loginRoundTripper{http.DefaultTransport, i.Users.Get(UserTypeLogin).Username}}
return rp.NewRelyingPartyOIDC(ctx, http_util.BuildHTTP(domain, i.Config.Port, i.Config.Secure), clientID, "", redirectURI, scope, rp.WithHTTPClient(loginClient))
}
type loginRoundTripper struct {
http.RoundTripper
loginUsername string
}
func (c *loginRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set(oidc_internal.LoginClientHeader, LoginUser)
req.Header.Set(oidc_internal.LoginClientHeader, c.loginUsername)
return c.RoundTripper.RoundTrip(req)
}
func (s *Tester) CreateResourceServerJWTProfile(ctx context.Context, keyFileData []byte) (rs.ResourceServer, error) {
func (i *Instance) CreateResourceServerJWTProfile(ctx context.Context, keyFileData []byte) (rs.ResourceServer, error) {
keyFile, err := client.ConfigFromKeyFileData(keyFileData)
if err != nil {
return nil, err
}
return rs.NewResourceServerJWTProfile(ctx, s.OIDCIssuer(), keyFile.ClientID, keyFile.KeyID, []byte(keyFile.Key))
return rs.NewResourceServerJWTProfile(ctx, i.OIDCIssuer(), keyFile.ClientID, keyFile.KeyID, []byte(keyFile.Key))
}
func (s *Tester) CreateResourceServerClientCredentials(ctx context.Context, clientID, clientSecret string) (rs.ResourceServer, error) {
return rs.NewResourceServerClientCredentials(ctx, s.OIDCIssuer(), clientID, clientSecret)
func (i *Instance) CreateResourceServerClientCredentials(ctx context.Context, clientID, clientSecret string) (rs.ResourceServer, error) {
return rs.NewResourceServerClientCredentials(ctx, i.OIDCIssuer(), clientID, clientSecret)
}
func GetRequest(url string, headers map[string]string) (*http.Request, error) {
@@ -313,9 +322,9 @@ func CheckRedirect(req *http.Request) (*url.URL, error) {
return resp.Location()
}
func (s *Tester) CreateOIDCCredentialsClient(ctx context.Context) (machine *management.AddMachineUserResponse, name, clientID, clientSecret string, err error) {
func (i *Instance) CreateOIDCCredentialsClient(ctx context.Context) (machine *management.AddMachineUserResponse, name, clientID, clientSecret string, err error) {
name = gofakeit.Username()
machine, err = s.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{
machine, err = i.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{
Name: name,
UserName: name,
AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT,
@@ -323,7 +332,7 @@ func (s *Tester) CreateOIDCCredentialsClient(ctx context.Context) (machine *mana
if err != nil {
return nil, "", "", "", err
}
secret, err := s.Client.Mgmt.GenerateMachineSecret(ctx, &management.GenerateMachineSecretRequest{
secret, err := i.Client.Mgmt.GenerateMachineSecret(ctx, &management.GenerateMachineSecretRequest{
UserId: machine.GetUserId(),
})
if err != nil {
@@ -332,9 +341,9 @@ func (s *Tester) CreateOIDCCredentialsClient(ctx context.Context) (machine *mana
return machine, name, secret.GetClientId(), secret.GetClientSecret(), nil
}
func (s *Tester) CreateOIDCJWTProfileClient(ctx context.Context) (machine *management.AddMachineUserResponse, name string, keyData []byte, err error) {
func (i *Instance) CreateOIDCJWTProfileClient(ctx context.Context) (machine *management.AddMachineUserResponse, name string, keyData []byte, err error) {
name = gofakeit.Username()
machine, err = s.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{
machine, err = i.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{
Name: name,
UserName: name,
AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT,
@@ -342,7 +351,7 @@ func (s *Tester) CreateOIDCJWTProfileClient(ctx context.Context) (machine *manag
if err != nil {
return nil, "", nil, err
}
keyResp, err := s.Client.Mgmt.AddMachineKey(ctx, &management.AddMachineKeyRequest{
keyResp, err := i.Client.Mgmt.AddMachineKey(ctx, &management.AddMachineKeyRequest{
UserId: machine.GetUserId(),
Type: authn.KeyType_KEY_TYPE_JSON,
ExpirationDate: timestamppb.New(time.Now().Add(time.Hour)),

View File

@@ -0,0 +1,9 @@
package sink
//go:generate enumer -type Channel -trimprefix Channel -transform snake
type Channel int
const (
ChannelMilestone Channel = iota
ChannelQuota
)

View File

@@ -0,0 +1,78 @@
// Code generated by "enumer -type Channel -trimprefix Channel -transform snake"; DO NOT EDIT.
package sink
import (
"fmt"
"strings"
)
const _ChannelName = "milestonequota"
var _ChannelIndex = [...]uint8{0, 9, 14}
const _ChannelLowerName = "milestonequota"
func (i Channel) String() string {
if i < 0 || i >= Channel(len(_ChannelIndex)-1) {
return fmt.Sprintf("Channel(%d)", i)
}
return _ChannelName[_ChannelIndex[i]:_ChannelIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _ChannelNoOp() {
var x [1]struct{}
_ = x[ChannelMilestone-(0)]
_ = x[ChannelQuota-(1)]
}
var _ChannelValues = []Channel{ChannelMilestone, ChannelQuota}
var _ChannelNameToValueMap = map[string]Channel{
_ChannelName[0:9]: ChannelMilestone,
_ChannelLowerName[0:9]: ChannelMilestone,
_ChannelName[9:14]: ChannelQuota,
_ChannelLowerName[9:14]: ChannelQuota,
}
var _ChannelNames = []string{
_ChannelName[0:9],
_ChannelName[9:14],
}
// ChannelString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func ChannelString(s string) (Channel, error) {
if val, ok := _ChannelNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _ChannelNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to Channel values", s)
}
// ChannelValues returns all values of the enum
func ChannelValues() []Channel {
return _ChannelValues
}
// ChannelStrings returns a slice of all String values of the enum
func ChannelStrings() []string {
strs := make([]string, len(_ChannelNames))
copy(strs, _ChannelNames)
return strs
}
// IsAChannel returns "true" if the value is listed in the enum definition. "false" otherwise
func (i Channel) IsAChannel() bool {
for _, v := range _ChannelValues {
if i == v {
return true
}
}
return false
}

View File

@@ -0,0 +1,167 @@
//go:build integration
package sink
import (
"errors"
"io"
"net/http"
"net/url"
"path"
"sync"
"sync/atomic"
"github.com/go-chi/chi/v5"
"github.com/gorilla/websocket"
"github.com/sirupsen/logrus"
"github.com/zitadel/logging"
)
const (
port = "8081"
listenAddr = "127.0.0.1:" + port
host = "localhost:" + port
)
// CallURL returns the full URL to the handler of a [Channel].
func CallURL(ch Channel) string {
u := url.URL{
Scheme: "http",
Host: host,
Path: rootPath(ch),
}
return u.String()
}
// StartServer starts a simple HTTP server on localhost:8081
// ZITADEL can use the server to send HTTP requests which can be
// used to validate tests through [Subscribe]rs.
// For each [Channel] a route is registered on http://localhost:8081/<channel_name>.
// The route must be used to send the HTTP request to be validated.
// [CallURL] can be used to obtain the full URL for a given Channel.
//
// This function is only active when the `integration` build tag is enabled
func StartServer() (close func()) {
router := chi.NewRouter()
for _, ch := range ChannelValues() {
fwd := &forwarder{
channelID: ch,
subscribers: make(map[int64]chan<- *Request),
}
router.HandleFunc(rootPath(ch), fwd.receiveHandler)
router.HandleFunc(subscribePath(ch), fwd.subscriptionHandler)
}
s := &http.Server{
Addr: listenAddr,
Handler: router,
}
logging.WithFields("listen_addr", listenAddr).Warn("!!!! A sink server is started which may expose sensitive data on a public endpoint. Make sure the `integration` build tag is disabled for production builds. !!!!")
go func() {
err := s.ListenAndServe()
if !errors.Is(err, http.ErrServerClosed) {
logging.WithError(err).Fatal("sink server")
}
}()
return func() {
logging.OnError(s.Close()).Error("sink server")
}
}
func rootPath(c Channel) string {
return path.Join("/", c.String())
}
func subscribePath(c Channel) string {
return path.Join("/", c.String(), "subscribe")
}
// forwarder handles incoming HTTP requests from ZITADEL and
// forwards them to all subscribed web sockets.
type forwarder struct {
channelID Channel
id atomic.Int64
mtx sync.RWMutex
subscribers map[int64]chan<- *Request
upgrader websocket.Upgrader
}
// receiveHandler receives a simple HTTP for a single [Channel]
// and forwards them on all active subscribers of that Channel.
func (c *forwarder) receiveHandler(w http.ResponseWriter, r *http.Request) {
req := &Request{
Header: r.Header.Clone(),
}
var err error
req.Body, err = io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
c.mtx.RLock()
for _, reqChan := range c.subscribers {
reqChan <- req
}
c.mtx.RUnlock()
w.WriteHeader(http.StatusOK)
}
// subscriptionHandler upgrades HTTP request to a websocket connection for subscribers.
// All received HTTP requests on a subscriber's channel are send on the websocket to the client.
func (c *forwarder) subscriptionHandler(w http.ResponseWriter, r *http.Request) {
ws, err := c.upgrader.Upgrade(w, r, nil)
logging.OnError(err).Error("websocket upgrade")
if err != nil {
return
}
done := readLoop(ws)
id := c.id.Add(1)
reqChannel := make(chan *Request, 100)
c.mtx.Lock()
c.subscribers[id] = reqChannel
c.mtx.Unlock()
logging.WithFields("id", id, "channel", c.channelID).Info("websocket opened")
defer func() {
c.mtx.Lock()
delete(c.subscribers, id)
c.mtx.Unlock()
ws.Close()
close(reqChannel)
}()
for {
select {
case err := <-done:
logging.WithError(err).WithFields(logrus.Fields{"id": id, "channel": c.channelID}).Info("websocket closed")
return
case req := <-reqChannel:
if err := ws.WriteJSON(req); err != nil {
logging.WithError(err).WithFields(logrus.Fields{"id": id, "channel": c.channelID}).Error("websocket write json")
return
}
}
}
}
// readLoop makes sure we can receive close messages
func readLoop(ws *websocket.Conn) (done chan error) {
done = make(chan error, 1)
go func(done chan<- error) {
for {
_, _, err := ws.NextReader()
if err != nil {
done <- err
break
}
}
close(done)
}(done)
return done
}

View File

@@ -0,0 +1,4 @@
// Package sink provides a simple HTTP server where Zitadel can send HTTP based messages,
// which are then possible to be observed using observers on websockets.
// The contents of this package become available when the `integration` build tag is enabled.
package sink

View File

@@ -0,0 +1,9 @@
//go:build !integration
package sink
// StartServer and its returned close function are a no-op
// when the `integration` build tag is disabled.
func StartServer() (close func()) {
return func() {}
}

View File

@@ -0,0 +1,90 @@
//go:build integration
package sink
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"sync/atomic"
"github.com/gorilla/websocket"
"github.com/zitadel/logging"
)
// Request is a message forwarded from the handler to [Subscription]s.
type Request struct {
Header http.Header
Body json.RawMessage
}
// Subscription is a websocket client to which [Request]s are forwarded by the server.
type Subscription struct {
conn *websocket.Conn
closed atomic.Bool
reqChannel chan *Request
}
// Subscribe to a channel.
// The subscription forwards all requests it received on the channel's
// handler, after Subscribe has returned.
// Multiple subscription may be active on a single channel.
// Each request is always forwarded to each Subscription.
// Close must be called to cleanup up the Subscription's channel and go routine.
func Subscribe(ctx context.Context, ch Channel) *Subscription {
u := url.URL{
Scheme: "ws",
Host: listenAddr,
Path: subscribePath(ch),
}
conn, resp, err := websocket.DefaultDialer.DialContext(ctx, u.String(), nil)
if err != nil {
if resp != nil {
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
err = fmt.Errorf("subscribe: %w, status: %s, body: %s", err, resp.Status, body)
}
panic(err)
}
sub := &Subscription{
conn: conn,
reqChannel: make(chan *Request, 10),
}
go sub.readToChan()
return sub
}
func (s *Subscription) readToChan() {
for {
if s.closed.Load() {
break
}
req := new(Request)
if err := s.conn.ReadJSON(req); err != nil {
opErr := new(net.OpError)
if errors.As(err, &opErr) {
break
}
logging.WithError(err).Error("subscription read")
break
}
s.reqChannel <- req
}
close(s.reqChannel)
}
// Recv returns the channel over which [Request]s are send.
func (s *Subscription) Recv() <-chan *Request {
return s.reqChannel
}
func (s *Subscription) Close() error {
s.closed.Store(true)
return s.conn.Close()
}

View File

@@ -0,0 +1,59 @@
package integration
import (
"context"
_ "embed"
"sync"
"time"
"github.com/zitadel/oidc/v3/pkg/client"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/pkg/grpc/system"
)
var (
//go:embed config/system-user-key.pem
systemUserKey []byte
)
var (
// SystemClient creates a system connection once and reuses it on every use.
// Each client call automatically gets the authorization context for the system user.
SystemClient = sync.OnceValue[system.SystemServiceClient](systemClient)
SystemToken string
)
func systemClient() system.SystemServiceClient {
cc, err := grpc.NewClient(loadedConfig.Host(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithChainUnaryInterceptor(func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
ctx = WithSystemAuthorization(ctx)
return invoker(ctx, method, req, reply, cc, opts...)
}),
)
if err != nil {
panic(err)
}
return system.NewSystemServiceClient(cc)
}
func systemUserToken() string {
const ISSUER = "tester"
audience := http_util.BuildOrigin(loadedConfig.Host(), loadedConfig.Secure)
signer, err := client.NewSignerFromPrivateKeyByte(systemUserKey, "")
if err != nil {
panic(err)
}
token, err := client.SignedJWTProfileAssertion(ISSUER, []string{audience}, time.Hour, signer)
if err != nil {
panic(err)
}
return token
}
func WithSystemAuthorization(ctx context.Context) context.Context {
return WithAuthorizationToken(ctx, SystemToken)
}

View File

@@ -11,10 +11,10 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/management"
)
func (s *Tester) CreateMachineUserPATWithMembership(ctx context.Context, roles ...string) (id, pat string, err error) {
user := s.CreateMachineUser(ctx)
func (i *Instance) CreateMachineUserPATWithMembership(ctx context.Context, roles ...string) (id, pat string, err error) {
user := i.CreateMachineUser(ctx)
patResp, err := s.Client.Mgmt.AddPersonalAccessToken(ctx, &management.AddPersonalAccessTokenRequest{
patResp, err := i.Client.Mgmt.AddPersonalAccessToken(ctx, &management.AddPersonalAccessTokenRequest{
UserId: user.GetUserId(),
ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour)),
})
@@ -35,7 +35,7 @@ func (s *Tester) CreateMachineUserPATWithMembership(ctx context.Context, roles .
}
if len(orgRoles) > 0 {
_, err := s.Client.Mgmt.AddOrgMember(ctx, &management.AddOrgMemberRequest{
_, err := i.Client.Mgmt.AddOrgMember(ctx, &management.AddOrgMemberRequest{
UserId: user.GetUserId(),
Roles: orgRoles,
})
@@ -44,7 +44,7 @@ func (s *Tester) CreateMachineUserPATWithMembership(ctx context.Context, roles .
}
}
if len(iamRoles) > 0 {
_, err := s.Client.Admin.AddIAMMember(ctx, &admin.AddIAMMemberRequest{
_, err := i.Client.Admin.AddIAMMember(ctx, &admin.AddIAMMemberRequest{
UserId: user.GetUserId(),
Roles: iamRoles,
})

View File

@@ -0,0 +1,86 @@
// Code generated by "enumer -type UserType -transform snake -trimprefix UserType"; DO NOT EDIT.
package integration
import (
"fmt"
"strings"
)
const _UserTypeName = "unspecifiediam_ownerorg_ownerlogin"
var _UserTypeIndex = [...]uint8{0, 11, 20, 29, 34}
const _UserTypeLowerName = "unspecifiediam_ownerorg_ownerlogin"
func (i UserType) String() string {
if i < 0 || i >= UserType(len(_UserTypeIndex)-1) {
return fmt.Sprintf("UserType(%d)", i)
}
return _UserTypeName[_UserTypeIndex[i]:_UserTypeIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _UserTypeNoOp() {
var x [1]struct{}
_ = x[UserTypeUnspecified-(0)]
_ = x[UserTypeIAMOwner-(1)]
_ = x[UserTypeOrgOwner-(2)]
_ = x[UserTypeLogin-(3)]
}
var _UserTypeValues = []UserType{UserTypeUnspecified, UserTypeIAMOwner, UserTypeOrgOwner, UserTypeLogin}
var _UserTypeNameToValueMap = map[string]UserType{
_UserTypeName[0:11]: UserTypeUnspecified,
_UserTypeLowerName[0:11]: UserTypeUnspecified,
_UserTypeName[11:20]: UserTypeIAMOwner,
_UserTypeLowerName[11:20]: UserTypeIAMOwner,
_UserTypeName[20:29]: UserTypeOrgOwner,
_UserTypeLowerName[20:29]: UserTypeOrgOwner,
_UserTypeName[29:34]: UserTypeLogin,
_UserTypeLowerName[29:34]: UserTypeLogin,
}
var _UserTypeNames = []string{
_UserTypeName[0:11],
_UserTypeName[11:20],
_UserTypeName[20:29],
_UserTypeName[29:34],
}
// UserTypeString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func UserTypeString(s string) (UserType, error) {
if val, ok := _UserTypeNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _UserTypeNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to UserType values", s)
}
// UserTypeValues returns all values of the enum
func UserTypeValues() []UserType {
return _UserTypeValues
}
// UserTypeStrings returns a slice of all String values of the enum
func UserTypeStrings() []string {
strs := make([]string, len(_UserTypeNames))
copy(strs, _UserTypeNames)
return strs
}
// IsAUserType returns "true" if the value is listed in the enum definition. "false" otherwise
func (i UserType) IsAUserType() bool {
for _, v := range _UserTypeValues {
if i == v {
return true
}
}
return false
}

View File

@@ -1,27 +0,0 @@
// Code generated by "stringer -type=UserType"; DO NOT EDIT.
package integration
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[Unspecified-0]
_ = x[OrgOwner-1]
_ = x[Login-2]
_ = x[IAMOwner-3]
_ = x[SystemUser-4]
}
const _UserType_name = "UnspecifiedOrgOwnerLoginIAMOwnerSystemUser"
var _UserType_index = [...]uint8{0, 11, 19, 24, 32, 42}
func (i UserType) String() string {
if i < 0 || i >= UserType(len(_UserType_index)-1) {
return "UserType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _UserType_name[_UserType_index[i]:_UserType_index[i+1]]
}