feat: impersonation roles (#7442)

* partial work done

* test IAM membership roles

* org membership tests

* console :(, translations and docs

* fix integration test

* fix tests

* add EnableImpersonation to security policy API

* fix integration test timestamp checking

* add security policy tests and fix projections

* add impersonation setting in console

* add security settings to the settings v2 API

* fix typo

* move impersonation to instance

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Tim Möhlmann
2024-02-28 12:21:11 +02:00
committed by GitHub
parent 68af4f59c9
commit 062d153cfe
60 changed files with 1624 additions and 144 deletions

View File

@@ -23,6 +23,7 @@ type Instance interface {
DefaultLanguage() language.Tag
DefaultOrganisationID() string
SecurityPolicyAllowedOrigins() []string
EnableImpersonation() bool
Block() *bool
AuditLogRetention() *time.Duration
Features() feature.Features
@@ -87,6 +88,10 @@ func (i *instance) SecurityPolicyAllowedOrigins() []string {
return nil
}
func (i *instance) EnableImpersonation() bool {
return false
}
func (i *instance) Features() feature.Features {
return i.features
}

View File

@@ -130,6 +130,10 @@ func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
return nil
}
func (m *mockInstance) EnableImpersonation() bool {
return false
}
func (m *mockInstance) Features() feature.Features {
return feature.Features{}
}

View File

@@ -0,0 +1,322 @@
//go:build integration
package admin_test
import (
"context"
"testing"
"github.com/brianvoe/gofakeit/v6"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/member"
"github.com/zitadel/zitadel/pkg/grpc/object"
)
var iamRoles = []string{
"IAM_OWNER",
"IAM_OWNER_VIEWER",
"IAM_ORG_MANAGER",
"IAM_USER_MANAGER",
"IAM_ADMIN_IMPERSONATOR",
"IAM_END_USER_IMPERSONATOR",
}
func TestServer_ListIAMMemberRoles(t *testing.T) {
got, err := Client.ListIAMMemberRoles(AdminCTX, &admin_pb.ListIAMMemberRolesRequest{})
require.NoError(t, err)
assert.ElementsMatch(t, iamRoles, got.GetRoles())
}
func TestServer_ListIAMMembers(t *testing.T) {
user := Tester.CreateHumanUserVerified(AdminCTX, Tester.Organisation.ID, gofakeit.Email())
_, err := Client.AddIAMMember(AdminCTX, &admin_pb.AddIAMMemberRequest{
UserId: user.GetUserId(),
Roles: iamRoles,
})
require.NoError(t, err)
type args struct {
ctx context.Context
req *admin_pb.ListIAMMembersRequest
}
tests := []struct {
name string
args args
want *admin_pb.ListIAMMembersResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: Tester.WithAuthorization(CTX, integration.OrgOwner),
req: &admin_pb.ListIAMMembersRequest{
Query: &object.ListQuery{},
Queries: []*member.SearchQuery{{
Query: &member.SearchQuery_UserIdQuery{
UserIdQuery: &member.UserIDQuery{
UserId: user.GetUserId(),
},
},
}},
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: AdminCTX,
req: &admin_pb.ListIAMMembersRequest{
Query: &object.ListQuery{},
Queries: []*member.SearchQuery{{
Query: &member.SearchQuery_UserIdQuery{
UserIdQuery: &member.UserIDQuery{
UserId: user.GetUserId(),
},
},
}},
},
},
want: &admin_pb.ListIAMMembersResponse{
Result: []*member.Member{{
UserId: user.GetUserId(),
Roles: iamRoles,
}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.ListIAMMembers(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
wantResult := tt.want.GetResult()
gotResult := got.GetResult()
require.Len(t, gotResult, len(wantResult))
for i, want := range wantResult {
assert.Equal(t, want.GetUserId(), gotResult[i].GetUserId())
assert.ElementsMatch(t, want.GetRoles(), gotResult[i].GetRoles())
}
})
}
}
func TestServer_AddIAMMember(t *testing.T) {
user := Tester.CreateHumanUserVerified(AdminCTX, Tester.Organisation.ID, gofakeit.Email())
type args struct {
ctx context.Context
req *admin_pb.AddIAMMemberRequest
}
tests := []struct {
name string
args args
want *admin_pb.AddIAMMemberResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: Tester.WithAuthorization(CTX, integration.OrgOwner),
req: &admin_pb.AddIAMMemberRequest{
UserId: user.GetUserId(),
Roles: iamRoles,
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: AdminCTX,
req: &admin_pb.AddIAMMemberRequest{
UserId: user.GetUserId(),
Roles: iamRoles,
},
},
want: &admin_pb.AddIAMMemberResponse{
Details: &object.ObjectDetails{
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "unknown roles error",
args: args{
ctx: AdminCTX,
req: &admin_pb.AddIAMMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"FOO", "BAR"},
},
},
wantErr: true,
},
{
name: "org role error",
args: args{
ctx: AdminCTX,
req: &admin_pb.AddIAMMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"ORG_OWNER"},
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.AddIAMMember(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_UpdateIAMMember(t *testing.T) {
user := Tester.CreateHumanUserVerified(AdminCTX, Tester.Organisation.ID, gofakeit.Email())
_, err := Client.AddIAMMember(AdminCTX, &admin_pb.AddIAMMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"IAM_OWNER"},
})
require.NoError(t, err)
type args struct {
ctx context.Context
req *admin_pb.UpdateIAMMemberRequest
}
tests := []struct {
name string
args args
want *admin_pb.UpdateIAMMemberResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: Tester.WithAuthorization(CTX, integration.OrgOwner),
req: &admin_pb.UpdateIAMMemberRequest{
UserId: user.GetUserId(),
Roles: iamRoles,
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: AdminCTX,
req: &admin_pb.UpdateIAMMemberRequest{
UserId: user.GetUserId(),
Roles: iamRoles,
},
},
want: &admin_pb.UpdateIAMMemberResponse{
Details: &object.ObjectDetails{
ResourceOwner: Tester.Instance.InstanceID(),
ChangeDate: timestamppb.Now(),
},
},
},
{
name: "unknown roles error",
args: args{
ctx: AdminCTX,
req: &admin_pb.UpdateIAMMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"FOO", "BAR"},
},
},
wantErr: true,
},
{
name: "org role error",
args: args{
ctx: AdminCTX,
req: &admin_pb.UpdateIAMMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"ORG_OWNER"},
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.UpdateIAMMember(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_RemoveIAMMember(t *testing.T) {
user := Tester.CreateHumanUserVerified(AdminCTX, Tester.Organisation.ID, gofakeit.Email())
_, err := Client.AddIAMMember(AdminCTX, &admin_pb.AddIAMMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"IAM_OWNER"},
})
require.NoError(t, err)
type args struct {
ctx context.Context
req *admin_pb.RemoveIAMMemberRequest
}
tests := []struct {
name string
args args
want *admin_pb.RemoveIAMMemberResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: Tester.WithAuthorization(CTX, integration.OrgOwner),
req: &admin_pb.RemoveIAMMemberRequest{
UserId: user.GetUserId(),
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: AdminCTX,
req: &admin_pb.RemoveIAMMemberRequest{
UserId: user.GetUserId(),
},
},
want: &admin_pb.RemoveIAMMemberResponse{
Details: &object.ObjectDetails{
ResourceOwner: Tester.Instance.InstanceID(),
ChangeDate: timestamppb.Now(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.RemoveIAMMember(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@@ -119,7 +119,7 @@ func (s *Server) GetSecurityPolicy(ctx context.Context, req *admin_pb.GetSecurit
}
func (s *Server) SetSecurityPolicy(ctx context.Context, req *admin_pb.SetSecurityPolicyRequest) (*admin_pb.SetSecurityPolicyResponse, error) {
details, err := s.command.SetSecurityPolicy(ctx, req.EnableIframeEmbedding, req.AllowedOrigins)
details, err := s.command.SetSecurityPolicy(ctx, securityPolicyToCommand(req))
if err != nil {
return nil, err
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/object"
obj_grpc "github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
@@ -173,7 +174,16 @@ func SMTPConfigToPb(smtp *query.SMTPConfig) *settings_pb.SMTPConfig {
func SecurityPolicyToPb(policy *query.SecurityPolicy) *settings_pb.SecurityPolicy {
return &settings_pb.SecurityPolicy{
Details: obj_grpc.ToViewDetailsPb(policy.Sequence, policy.CreationDate, policy.ChangeDate, policy.AggregateID),
EnableIframeEmbedding: policy.Enabled,
EnableIframeEmbedding: policy.EnableIframeEmbedding,
AllowedOrigins: policy.AllowedOrigins,
EnableImpersonation: policy.EnableImpersonation,
}
}
func securityPolicyToCommand(req *admin_pb.SetSecurityPolicyRequest) *command.SecurityPolicy {
return &command.SecurityPolicy{
EnableIframeEmbedding: req.GetEnableIframeEmbedding(),
AllowedOrigins: req.GetAllowedOrigins(),
EnableImpersonation: req.GetEnableImpersonation(),
}
}

View File

@@ -0,0 +1,163 @@
//go:build integration
package admin_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/object"
"github.com/zitadel/zitadel/pkg/grpc/settings"
)
func TestServer_GetSecurityPolicy(t *testing.T) {
_, err := Client.SetSecurityPolicy(AdminCTX, &admin_pb.SetSecurityPolicyRequest{
EnableIframeEmbedding: true,
AllowedOrigins: []string{"foo.com", "bar.com"},
EnableImpersonation: true,
})
require.NoError(t, err)
tests := []struct {
name string
ctx context.Context
want *admin_pb.GetSecurityPolicyResponse
wantErr bool
}{
{
name: "permission error",
ctx: Tester.WithAuthorization(CTX, integration.OrgOwner),
wantErr: true,
},
{
name: "success",
ctx: AdminCTX,
want: &admin_pb.GetSecurityPolicyResponse{
Policy: &settings.SecurityPolicy{
EnableIframeEmbedding: true,
AllowedOrigins: []string{"foo.com", "bar.com"},
EnableImpersonation: true,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp, err := Client.GetSecurityPolicy(tt.ctx, &admin_pb.GetSecurityPolicyRequest{})
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
got, want := resp.GetPolicy(), tt.want.GetPolicy()
assert.Equal(t, want.GetEnableIframeEmbedding(), got.GetEnableIframeEmbedding(), "enable iframe embedding")
assert.Equal(t, want.GetAllowedOrigins(), got.GetAllowedOrigins(), "allowed origins")
assert.Equal(t, want.GetEnableImpersonation(), got.GetEnableImpersonation(), "enable impersonation")
})
}
}
func TestServer_SetSecurityPolicy(t *testing.T) {
type args struct {
ctx context.Context
req *admin_pb.SetSecurityPolicyRequest
}
tests := []struct {
name string
args args
want *admin_pb.SetSecurityPolicyResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: Tester.WithAuthorization(CTX, integration.OrgOwner),
req: &admin_pb.SetSecurityPolicyRequest{
EnableIframeEmbedding: true,
AllowedOrigins: []string{"foo.com", "bar.com"},
EnableImpersonation: true,
},
},
wantErr: true,
},
{
name: "success allowed origins",
args: args{
ctx: AdminCTX,
req: &admin_pb.SetSecurityPolicyRequest{
AllowedOrigins: []string{"foo.com", "bar.com"},
},
},
want: &admin_pb.SetSecurityPolicyResponse{
Details: &object.ObjectDetails{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "success iframe embedding",
args: args{
ctx: AdminCTX,
req: &admin_pb.SetSecurityPolicyRequest{
EnableIframeEmbedding: true,
},
},
want: &admin_pb.SetSecurityPolicyResponse{
Details: &object.ObjectDetails{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "success impersonation",
args: args{
ctx: AdminCTX,
req: &admin_pb.SetSecurityPolicyRequest{
EnableImpersonation: true,
},
},
want: &admin_pb.SetSecurityPolicyResponse{
Details: &object.ObjectDetails{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "success all",
args: args{
ctx: AdminCTX,
req: &admin_pb.SetSecurityPolicyRequest{
EnableIframeEmbedding: true,
AllowedOrigins: []string{"foo.com", "bar.com"},
EnableImpersonation: true,
},
},
want: &admin_pb.SetSecurityPolicyResponse{
Details: &object.ObjectDetails{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.SetSecurityPolicy(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@@ -12,11 +12,13 @@ import (
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
var (
AdminCTX, SystemCTX context.Context
Tester *integration.Tester
CTX, AdminCTX, SystemCTX context.Context
Tester *integration.Tester
Client admin_pb.AdminServiceClient
)
func TestMain(m *testing.M) {
@@ -27,9 +29,10 @@ func TestMain(m *testing.M) {
Tester = integration.NewTester(ctx)
defer Tester.Done()
CTX = ctx
AdminCTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser)
Client = Tester.Client.Admin
return m.Run()
}())
}

View File

@@ -0,0 +1,327 @@
//go:build integration
package management_test
import (
"context"
"testing"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management"
"github.com/zitadel/zitadel/pkg/grpc/member"
"github.com/zitadel/zitadel/pkg/grpc/object"
)
var iamRoles = []string{
"SELF_MANAGEMENT_GLOBAL",
"ORG_OWNER",
"ORG_USER_MANAGER",
"ORG_OWNER_VIEWER",
"ORG_SETTINGS_MANAGER",
"ORG_USER_PERMISSION_EDITOR",
"ORG_PROJECT_PERMISSION_EDITOR",
"ORG_PROJECT_CREATOR",
"ORG_USER_SELF_MANAGER",
"ORG_ADMIN_IMPERSONATOR",
"ORG_END_USER_IMPERSONATOR",
}
func TestServer_ListOrgMemberRoles(t *testing.T) {
got, err := Client.ListOrgMemberRoles(OrgCTX, &mgmt_pb.ListOrgMemberRolesRequest{})
require.NoError(t, err)
assert.ElementsMatch(t, iamRoles, got.GetResult())
}
func TestServer_ListOrgMembers(t *testing.T) {
user := Tester.CreateHumanUserVerified(OrgCTX, Tester.Organisation.ID, gofakeit.Email())
_, err := Client.AddOrgMember(OrgCTX, &mgmt_pb.AddOrgMemberRequest{
UserId: user.GetUserId(),
Roles: iamRoles[1:],
})
require.NoError(t, err)
type args struct {
ctx context.Context
req *mgmt_pb.ListOrgMembersRequest
}
tests := []struct {
name string
args args
want *mgmt_pb.ListOrgMembersResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: CTX,
req: &mgmt_pb.ListOrgMembersRequest{
Query: &object.ListQuery{},
Queries: []*member.SearchQuery{{
Query: &member.SearchQuery_UserIdQuery{
UserIdQuery: &member.UserIDQuery{
UserId: user.GetUserId(),
},
},
}},
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: OrgCTX,
req: &mgmt_pb.ListOrgMembersRequest{
Query: &object.ListQuery{},
Queries: []*member.SearchQuery{{
Query: &member.SearchQuery_UserIdQuery{
UserIdQuery: &member.UserIDQuery{
UserId: user.GetUserId(),
},
},
}},
},
},
want: &mgmt_pb.ListOrgMembersResponse{
Result: []*member.Member{{
UserId: user.GetUserId(),
Roles: iamRoles[1:],
}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.ListOrgMembers(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
wantResult := tt.want.GetResult()
gotResult := got.GetResult()
require.Len(t, gotResult, len(wantResult))
for i, want := range wantResult {
assert.Equal(t, want.GetUserId(), gotResult[i].GetUserId())
assert.ElementsMatch(t, want.GetRoles(), gotResult[i].GetRoles())
}
})
}
}
func TestServer_AddOrgMember(t *testing.T) {
user := Tester.CreateHumanUserVerified(OrgCTX, Tester.Organisation.ID, gofakeit.Email())
type args struct {
ctx context.Context
req *mgmt_pb.AddOrgMemberRequest
}
tests := []struct {
name string
args args
want *mgmt_pb.AddOrgMemberResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: CTX,
req: &mgmt_pb.AddOrgMemberRequest{
UserId: user.GetUserId(),
Roles: iamRoles,
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: OrgCTX,
req: &mgmt_pb.AddOrgMemberRequest{
UserId: user.GetUserId(),
Roles: iamRoles[1:],
},
},
want: &mgmt_pb.AddOrgMemberResponse{
Details: &object.ObjectDetails{
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "unknown roles error",
args: args{
ctx: OrgCTX,
req: &mgmt_pb.AddOrgMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"FOO", "BAR"},
},
},
wantErr: true,
},
{
name: "iam role error",
args: args{
ctx: OrgCTX,
req: &mgmt_pb.AddOrgMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"IAM_OWNER"},
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.AddOrgMember(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_UpdateOrgMember(t *testing.T) {
user := Tester.CreateHumanUserVerified(OrgCTX, Tester.Organisation.ID, gofakeit.Email())
_, err := Client.AddOrgMember(OrgCTX, &mgmt_pb.AddOrgMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"ORG_OWNER"},
})
require.NoError(t, err)
type args struct {
ctx context.Context
req *mgmt_pb.UpdateOrgMemberRequest
}
tests := []struct {
name string
args args
want *mgmt_pb.UpdateOrgMemberResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: CTX,
req: &mgmt_pb.UpdateOrgMemberRequest{
UserId: user.GetUserId(),
Roles: iamRoles,
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: OrgCTX,
req: &mgmt_pb.UpdateOrgMemberRequest{
UserId: user.GetUserId(),
Roles: iamRoles[1:],
},
},
want: &mgmt_pb.UpdateOrgMemberResponse{
Details: &object.ObjectDetails{
ResourceOwner: Tester.Organisation.ID,
ChangeDate: timestamppb.Now(),
},
},
},
{
name: "unknown roles error",
args: args{
ctx: OrgCTX,
req: &mgmt_pb.UpdateOrgMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"FOO", "BAR"},
},
},
wantErr: true,
},
{
name: "iam role error",
args: args{
ctx: OrgCTX,
req: &mgmt_pb.UpdateOrgMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"IAM_OWNER"},
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.UpdateOrgMember(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_RemoveIAMMember(t *testing.T) {
user := Tester.CreateHumanUserVerified(OrgCTX, Tester.Organisation.ID, gofakeit.Email())
_, err := Client.AddOrgMember(OrgCTX, &mgmt_pb.AddOrgMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"ORG_OWNER"},
})
require.NoError(t, err)
type args struct {
ctx context.Context
req *mgmt_pb.RemoveOrgMemberRequest
}
tests := []struct {
name string
args args
want *mgmt_pb.RemoveOrgMemberResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: CTX,
req: &mgmt_pb.RemoveOrgMemberRequest{
UserId: user.GetUserId(),
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: OrgCTX,
req: &mgmt_pb.RemoveOrgMemberRequest{
UserId: user.GetUserId(),
},
},
want: &mgmt_pb.RemoveOrgMemberResponse{
Details: &object.ObjectDetails{
ResourceOwner: Tester.Organisation.ID,
ChangeDate: timestamppb.Now(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.RemoveOrgMember(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@@ -0,0 +1,34 @@
//go:build integration
package management_test
import (
"context"
"os"
"testing"
"time"
"github.com/zitadel/zitadel/internal/integration"
mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management"
)
var (
CTX, OrgCTX context.Context
Tester *integration.Tester
Client mgmt_pb.ManagementServiceClient
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, _, cancel := integration.Contexts(3 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
CTX = ctx
OrgCTX = Tester.WithAuthorization(ctx, integration.OrgOwner)
Client = Tester.Client.Mgmt
return m.Run()
}())
}

View File

@@ -3,8 +3,6 @@
package management_test
import (
"context"
"os"
"strconv"
"strings"
"testing"
@@ -20,26 +18,6 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/user"
)
var (
CTX context.Context
Tester *integration.Tester
Client management.ManagementServiceClient
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, errCtx, cancel := integration.Contexts(3 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
Client = Tester.Client.Mgmt
return m.Run()
}())
}
// TestImport_and_Get reproduces https://github.com/zitadel/zitadel/issues/5808
// which led to consistency issues due the call timestamp not being
// updated after a bulk Trigger.
@@ -57,7 +35,7 @@ func TestImport_and_Get(t *testing.T) {
userName := strings.Join([]string{firstName, lastName}, "_")
email := strings.Join([]string{userName, "example.com"}, "@")
res, err := Client.ImportHumanUser(CTX, &management.ImportHumanUserRequest{
res, err := Client.ImportHumanUser(OrgCTX, &management.ImportHumanUserRequest{
UserName: userName,
Profile: &management.ImportHumanUserRequest_Profile{
FirstName: firstName,
@@ -72,7 +50,7 @@ func TestImport_and_Get(t *testing.T) {
})
require.NoError(t, err)
_, err = Client.GetUserByID(CTX, &management.GetUserByIDRequest{Id: res.GetUserId()})
_, err = Client.GetUserByID(OrgCTX, &management.GetUserByIDRequest{Id: res.GetUserId()})
s, ok := status.FromError(err)
if ok && s != nil && s.Code() == codes.NotFound {
@@ -85,7 +63,7 @@ func TestImport_and_Get(t *testing.T) {
func TestImport_UnparsablePreferredLanguage(t *testing.T) {
random := integration.RandString(5)
_, err := Client.ImportHumanUser(CTX, &management.ImportHumanUserRequest{
_, err := Client.ImportHumanUser(OrgCTX, &management.ImportHumanUserRequest{
UserName: random,
Profile: &management.ImportHumanUserRequest_Profile{
FirstName: random,

View File

@@ -13,6 +13,7 @@ import (
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
@@ -183,6 +184,7 @@ func TestServer_CreateCallback(t *testing.T) {
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: regexp.QuoteMeta(`oidcintegrationtest://callback?error=access_denied&error_description=nope&error_uri=https%3A%2F%2Fexample.com%2Fdocs&state=state`),
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
@@ -206,6 +208,7 @@ func TestServer_CreateCallback(t *testing.T) {
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
@@ -231,6 +234,7 @@ func TestServer_CreateCallback(t *testing.T) {
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `http:\/\/localhost:9999\/callback#access_token=(.*)&expires_in=(.*)&id_token=(.*)&state=state&token_type=Bearer`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},

View File

@@ -210,6 +210,10 @@ func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
return nil
}
func (m *mockInstance) EnableImpersonation() bool {
return false
}
func (m *mockInstance) Features() feature.Features {
return feature.Features{}
}

View File

@@ -16,6 +16,7 @@ import (
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
@@ -164,6 +165,7 @@ func TestServer_CreateSession(t *testing.T) {
},
want: &session.CreateSessionResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
@@ -183,6 +185,7 @@ func TestServer_CreateSession(t *testing.T) {
},
want: &session.CreateSessionResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
@@ -211,6 +214,7 @@ func TestServer_CreateSession(t *testing.T) {
},
want: &session.CreateSessionResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
@@ -230,6 +234,7 @@ func TestServer_CreateSession(t *testing.T) {
},
want: &session.CreateSessionResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},

View File

@@ -0,0 +1,34 @@
//go:build integration
package settings_test
import (
"context"
"os"
"testing"
"time"
"github.com/zitadel/zitadel/internal/integration"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
)
var (
CTX, AdminCTX context.Context
Tester *integration.Tester
Client settings.SettingsServiceClient
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, _, cancel := integration.Contexts(3 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
CTX = ctx
AdminCTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
Client = Tester.Client.SettingsV2
return m.Run()
}())
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/query"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
)
func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSettingsRequest) (*settings.GetLoginSettingsResponse, error) {
@@ -124,3 +124,23 @@ func (s *Server) GetGeneralSettings(ctx context.Context, _ *settings.GetGeneralS
DefaultLanguage: instance.DefaultLanguage().String(),
}, nil
}
func (s *Server) GetSecuritySettings(ctx context.Context, req *settings.GetSecuritySettingsRequest) (*settings.GetSecuritySettingsResponse, error) {
policy, err := s.query.SecurityPolicy(ctx)
if err != nil {
return nil, err
}
return &settings.GetSecuritySettingsResponse{
Settings: securityPolicyToSettingsPb(policy),
}, nil
}
func (s *Server) SetSecuritySettings(ctx context.Context, req *settings.SetSecuritySettingsRequest) (*settings.SetSecuritySettingsResponse, error) {
details, err := s.command.SetSecurityPolicy(ctx, securitySettingsToCommand(req))
if err != nil {
return nil, err
}
return &settings.SetSecuritySettingsResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}

View File

@@ -3,6 +3,7 @@ package settings
import (
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
@@ -205,3 +206,21 @@ func idpTypeToPb(idpType domain.IDPType) settings.IdentityProviderType {
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED
}
}
func securityPolicyToSettingsPb(policy *query.SecurityPolicy) *settings.SecuritySettings {
return &settings.SecuritySettings{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
Enabled: policy.EnableIframeEmbedding,
AllowedOrigins: policy.AllowedOrigins,
},
EnableImpersonation: policy.EnableImpersonation,
}
}
func securitySettingsToCommand(req *settings.SetSecuritySettingsRequest) *command.SecurityPolicy {
return &command.SecurityPolicy{
EnableIframeEmbedding: req.GetEmbeddedIframe().GetEnabled(),
AllowedOrigins: req.GetEmbeddedIframe().GetAllowedOrigins(),
EnableImpersonation: req.GetEnableImpersonation(),
}
}

View File

@@ -12,6 +12,7 @@ import (
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
@@ -450,3 +451,35 @@ func Test_idpTypeToPb(t *testing.T) {
})
}
}
func Test_securityPolicyToSettingsPb(t *testing.T) {
want := &settings.SecuritySettings{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
Enabled: true,
AllowedOrigins: []string{"foo", "bar"},
},
EnableImpersonation: true,
}
got := securityPolicyToSettingsPb(&query.SecurityPolicy{
EnableIframeEmbedding: true,
AllowedOrigins: []string{"foo", "bar"},
EnableImpersonation: true,
})
assert.Equal(t, want, got)
}
func Test_securitySettingsToCommand(t *testing.T) {
want := &command.SecurityPolicy{
EnableIframeEmbedding: true,
AllowedOrigins: []string{"foo", "bar"},
EnableImpersonation: true,
}
got := securitySettingsToCommand(&settings.SetSecuritySettingsRequest{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
Enabled: true,
AllowedOrigins: []string{"foo", "bar"},
},
EnableImpersonation: true,
})
assert.Equal(t, want, got)
}

View File

@@ -0,0 +1,174 @@
//go:build integration
package settings_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
)
func TestServer_GetSecuritySettings(t *testing.T) {
_, err := Client.SetSecuritySettings(AdminCTX, &settings.SetSecuritySettingsRequest{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
Enabled: true,
AllowedOrigins: []string{"foo", "bar"},
},
EnableImpersonation: true,
})
require.NoError(t, err)
tests := []struct {
name string
ctx context.Context
want *settings.GetSecuritySettingsResponse
wantErr bool
}{
{
name: "permission error",
ctx: Tester.WithAuthorization(CTX, integration.OrgOwner),
wantErr: true,
},
{
name: "success",
ctx: AdminCTX,
want: &settings.GetSecuritySettingsResponse{
Settings: &settings.SecuritySettings{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
Enabled: true,
AllowedOrigins: []string{"foo", "bar"},
},
EnableImpersonation: true,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp, err := Client.GetSecuritySettings(tt.ctx, &settings.GetSecuritySettingsRequest{})
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
got, want := resp.GetSettings(), tt.want.GetSettings()
assert.Equal(t, want.GetEmbeddedIframe().GetEnabled(), got.GetEmbeddedIframe().GetEnabled(), "enable iframe embedding")
assert.Equal(t, want.GetEmbeddedIframe().GetAllowedOrigins(), got.GetEmbeddedIframe().GetAllowedOrigins(), "allowed origins")
assert.Equal(t, want.GetEnableImpersonation(), got.GetEnableImpersonation(), "enable impersonation")
})
}
}
func TestServer_SetSecuritySettings(t *testing.T) {
type args struct {
ctx context.Context
req *settings.SetSecuritySettingsRequest
}
tests := []struct {
name string
args args
want *settings.SetSecuritySettingsResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: Tester.WithAuthorization(CTX, integration.OrgOwner),
req: &settings.SetSecuritySettingsRequest{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
Enabled: true,
AllowedOrigins: []string{"foo.com", "bar.com"},
},
EnableImpersonation: true,
},
},
wantErr: true,
},
{
name: "success allowed origins",
args: args{
ctx: AdminCTX,
req: &settings.SetSecuritySettingsRequest{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
AllowedOrigins: []string{"foo.com", "bar.com"},
},
},
},
want: &settings.SetSecuritySettingsResponse{
Details: &object_pb.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "success enable iframe embedding",
args: args{
ctx: AdminCTX,
req: &settings.SetSecuritySettingsRequest{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
Enabled: true,
},
},
},
want: &settings.SetSecuritySettingsResponse{
Details: &object_pb.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "success impersonation",
args: args{
ctx: AdminCTX,
req: &settings.SetSecuritySettingsRequest{
EnableImpersonation: true,
},
},
want: &settings.SetSecuritySettingsResponse{
Details: &object_pb.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "success all",
args: args{
ctx: AdminCTX,
req: &settings.SetSecuritySettingsRequest{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
Enabled: true,
AllowedOrigins: []string{"foo.com", "bar.com"},
},
EnableImpersonation: true,
},
},
want: &settings.SetSecuritySettingsResponse{
Details: &object_pb.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.SetSecuritySettings(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@@ -7,6 +7,7 @@ import (
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
@@ -216,6 +217,7 @@ func TestServer_AddOTPEmail(t *testing.T) {
},
want: &user.AddOTPEmailResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
@@ -282,6 +284,7 @@ func TestServer_RemoveOTPEmail(t *testing.T) {
},
want: &user.RemoveOTPEmailResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ResourceOwner,
},
},

View File

@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
@@ -58,6 +59,7 @@ func TestServer_RegisterPasskey(t *testing.T) {
},
want: &user.RegisterPasskeyResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
@@ -109,6 +111,7 @@ func TestServer_RegisterPasskey(t *testing.T) {
},
want: &user.RegisterPasskeyResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
@@ -187,6 +190,7 @@ func TestServer_VerifyPasskeyRegistration(t *testing.T) {
},
want: &user.VerifyPasskeyRegistrationResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
@@ -253,6 +257,7 @@ func TestServer_CreatePasskeyRegistrationLink(t *testing.T) {
},
want: &user.CreatePasskeyRegistrationLinkResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
@@ -272,6 +277,7 @@ func TestServer_CreatePasskeyRegistrationLink(t *testing.T) {
},
want: &user.CreatePasskeyRegistrationLinkResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
@@ -287,6 +293,7 @@ func TestServer_CreatePasskeyRegistrationLink(t *testing.T) {
},
want: &user.CreatePasskeyRegistrationLinkResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},

View File

@@ -143,6 +143,7 @@ func TestServer_SetPassword(t *testing.T) {
},
want: &user.SetPasswordResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
@@ -173,6 +174,7 @@ func TestServer_SetPassword(t *testing.T) {
},
want: &user.SetPasswordResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
@@ -206,6 +208,7 @@ func TestServer_SetPassword(t *testing.T) {
},
want: &user.SetPasswordResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},

View File

@@ -10,6 +10,7 @@ import (
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
@@ -60,6 +61,7 @@ func TestServer_RegisterTOTP(t *testing.T) {
},
want: &user.RegisterTOTPResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
@@ -136,6 +138,7 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) {
},
want: &user.VerifyTOTPRegistrationResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ResourceOwner,
},
},

View File

@@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
@@ -60,6 +61,7 @@ func TestServer_RegisterU2F(t *testing.T) {
},
want: &user.RegisterU2FResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
@@ -134,6 +136,7 @@ func TestServer_VerifyU2FRegistration(t *testing.T) {
},
want: &user.VerifyU2FRegistrationResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},

View File

@@ -8,7 +8,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
@@ -22,8 +21,6 @@ import (
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
var ignoreTypes = []protoreflect.FullName{"google.protobuf.Duration", "google.protobuf.Struct"}
func Test_idpIntentToIDPIntentPb(t *testing.T) {
decryption := func(err error) crypto.EncryptionAlgorithm {
mCrypto := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))

View File

@@ -345,6 +345,10 @@ func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
return nil
}
func (m *mockInstance) EnableImpersonation() bool {
return false
}
func (m *mockInstance) Features() feature.Features {
return feature.Features{}
}

View File

@@ -10,9 +10,15 @@ import (
"github.com/zitadel/zitadel/internal/repository/instance"
)
func (c *Commands) SetSecurityPolicy(ctx context.Context, enabled bool, allowedOrigins []string) (*domain.ObjectDetails, error) {
type SecurityPolicy struct {
EnableIframeEmbedding bool
AllowedOrigins []string
EnableImpersonation bool
}
func (c *Commands) SetSecurityPolicy(ctx context.Context, policy *SecurityPolicy) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
validation := c.prepareSetSecurityPolicy(instanceAgg, enabled, allowedOrigins)
validation := c.prepareSetSecurityPolicy(instanceAgg, policy)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation)
if err != nil {
return nil, err
@@ -28,14 +34,14 @@ func (c *Commands) SetSecurityPolicy(ctx context.Context, enabled bool, allowedO
}, nil
}
func (c *Commands) prepareSetSecurityPolicy(a *instance.Aggregate, enabled bool, allowedOrigins []string) preparation.Validation {
func (c *Commands) prepareSetSecurityPolicy(a *instance.Aggregate, policy *SecurityPolicy) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel, err := c.getSecurityPolicyWriteModel(ctx, filter)
if err != nil {
return nil, err
}
cmd, err := writeModel.NewSetEvent(ctx, &a.Aggregate, enabled, allowedOrigins)
cmd, err := writeModel.NewSetEvent(ctx, &a.Aggregate, policy)
if err != nil {
return nil, err
}

View File

@@ -2,7 +2,7 @@ package command
import (
"context"
"reflect"
"slices"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -11,9 +11,7 @@ import (
type InstanceSecurityPolicyWriteModel struct {
eventstore.WriteModel
Enabled bool
AllowedOrigins []string
SecurityPolicy
}
func NewInstanceSecurityPolicyWriteModel(ctx context.Context) *InstanceSecurityPolicyWriteModel {
@@ -28,8 +26,11 @@ func NewInstanceSecurityPolicyWriteModel(ctx context.Context) *InstanceSecurityP
func (wm *InstanceSecurityPolicyWriteModel) Reduce() error {
for _, event := range wm.Events {
if e, ok := event.(*instance.SecurityPolicySetEvent); ok {
if e.Enabled != nil {
wm.Enabled = *e.Enabled
if e.EnableIframeEmbedding != nil {
wm.EnableIframeEmbedding = *e.EnableIframeEmbedding
} else if e.Enabled != nil {
wm.EnableIframeEmbedding = *e.Enabled
}
if e.AllowedOrigins != nil {
wm.AllowedOrigins = *e.AllowedOrigins
@@ -53,17 +54,19 @@ func (wm *InstanceSecurityPolicyWriteModel) Query() *eventstore.SearchQueryBuild
func (wm *InstanceSecurityPolicyWriteModel) NewSetEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
enabled bool,
allowedOrigins []string,
policy *SecurityPolicy,
) (*instance.SecurityPolicySetEvent, error) {
changes := make([]instance.SecurityPolicyChanges, 0, 2)
var err error
if wm.Enabled != enabled {
changes = append(changes, instance.ChangeSecurityPolicyEnabled(enabled))
if wm.EnableIframeEmbedding != policy.EnableIframeEmbedding {
changes = append(changes, instance.ChangeSecurityPolicyEnableIframeEmbedding(policy.EnableIframeEmbedding))
}
if enabled && !reflect.DeepEqual(wm.AllowedOrigins, allowedOrigins) {
changes = append(changes, instance.ChangeSecurityPolicyAllowedOrigins(allowedOrigins))
if !slices.Equal(wm.AllowedOrigins, policy.AllowedOrigins) {
changes = append(changes, instance.ChangeSecurityPolicyAllowedOrigins(policy.AllowedOrigins))
}
if wm.EnableImpersonation != policy.EnableImpersonation {
changes = append(changes, instance.ChangeSecurityPolicyEnableImpersonation(policy.EnableImpersonation))
}
changeEvent, err := instance.NewSecurityPolicySetEvent(ctx, aggregate, changes)
if err != nil {

View File

@@ -216,6 +216,10 @@ func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
return nil
}
func (m *mockInstance) EnableImpersonation() bool {
return false
}
func (m *mockInstance) Features() feature.Features {
return feature.Features{}
}

View File

@@ -5,12 +5,22 @@ import (
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/timestamppb"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
)
type DetailsMsg interface {
GetDetails() *object.Details
// Details is the interface that covers both v1 and v2 proto generated object details.
type Details interface {
comparable
GetSequence() uint64
GetChangeDate() *timestamppb.Timestamp
GetResourceOwner() string
}
// DetailsMsg is the interface that covers all proto messages which contain v1 or v2 object details.
type DetailsMsg[D Details] interface {
GetDetails() D
}
type ListDetailsMsg interface {
@@ -24,22 +34,24 @@ type ListDetailsMsg interface {
// Dynamically generated values are not compared with expected.
// Instead a sanity check is performed.
// For the sequence a non-zero value is expected.
// The change date has to be now, with a tollerance of 1 second.
// If the change date is populated, it is checked with a tolerance of 1 minute around Now.
//
// The resource owner is compared with expected and is
// therefore the only value that has to be set.
func AssertDetails[D DetailsMsg](t testing.TB, expected, actual D) {
// The resource owner is compared with expected.
func AssertDetails[D Details, M DetailsMsg[D]](t testing.TB, expected, actual M) {
wantDetails, gotDetails := expected.GetDetails(), actual.GetDetails()
if wantDetails == nil {
var nilDetails D
if wantDetails == nilDetails {
assert.Nil(t, gotDetails)
return
}
assert.NotZero(t, gotDetails.GetSequence())
gotCD := gotDetails.GetChangeDate().AsTime()
now := time.Now()
assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute))
if wantDetails.GetChangeDate() != nil {
wantChangeDate := time.Now()
gotChangeDate := gotDetails.GetChangeDate().AsTime()
assert.WithinRange(t, gotChangeDate, wantChangeDate.Add(-time.Minute), wantChangeDate.Add(time.Minute))
}
assert.Equal(t, wantDetails.GetResourceOwner(), gotDetails.GetResourceOwner())
}

View File

@@ -32,6 +32,7 @@ func TestAssertDetails(t *testing.T) {
exptected: myMsg{
details: &object.Details{
ResourceOwner: "me",
ChangeDate: timestamppb.Now(),
},
},
actual: myMsg{

View File

@@ -34,6 +34,7 @@ import (
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
organisation "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
settings "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 "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
@@ -46,6 +47,7 @@ type Client struct {
Auth auth.AuthServiceClient
UserV2 user.UserServiceClient
SessionV2 session.SessionServiceClient
SettingsV2 settings.SettingsServiceClient
OIDCv2 oidc_pb.OIDCServiceClient
OrgV2 organisation.OrganizationServiceClient
System system.SystemServiceClient
@@ -61,6 +63,7 @@ func newClient(cc *grpc.ClientConn) Client {
Auth: auth.NewAuthServiceClient(cc),
UserV2: user.NewUserServiceClient(cc),
SessionV2: session.NewSessionServiceClient(cc),
SettingsV2: settings.NewSettingsServiceClient(cc),
OIDCv2: oidc_pb.NewOIDCServiceClient(cc),
OrgV2: organisation.NewOrganizationServiceClient(cc),
System: system.NewSystemServiceClient(cc),

View File

@@ -411,23 +411,24 @@ func prepareInstanceDomainQuery(ctx context.Context, db prepareDatabase) (sq.Sel
}
type authzInstance struct {
id string
iamProjectID string
consoleID string
consoleAppID string
host string
domain string
defaultLang language.Tag
defaultOrgID string
csp csp
block *bool
auditLogRetention *time.Duration
features feature.Features
id string
iamProjectID string
consoleID string
consoleAppID string
host string
domain string
defaultLang language.Tag
defaultOrgID string
csp csp
enableImpersonation bool
block *bool
auditLogRetention *time.Duration
features feature.Features
}
type csp struct {
enabled bool
allowedOrigins database.TextArray[string]
enableIframeEmbedding bool
allowedOrigins database.TextArray[string]
}
func (i *authzInstance) InstanceID() string {
@@ -463,12 +464,16 @@ func (i *authzInstance) DefaultOrganisationID() string {
}
func (i *authzInstance) SecurityPolicyAllowedOrigins() []string {
if !i.csp.enabled {
if !i.csp.enableIframeEmbedding {
return nil
}
return i.csp.allowedOrigins
}
func (i *authzInstance) EnableImpersonation() bool {
return i.enableImpersonation
}
func (i *authzInstance) Block() *bool {
return i.block
}
@@ -489,7 +494,8 @@ func scanAuthzInstance(host, domain string) (*authzInstance, func(row *sql.Row)
return instance, func(row *sql.Row) error {
var (
lang string
securityPolicyEnabled sql.NullBool
enableIframeEmbedding sql.NullBool
enableImpersonation sql.NullBool
auditLogRetention database.NullDuration
block sql.NullBool
features []byte
@@ -501,8 +507,9 @@ func scanAuthzInstance(host, domain string) (*authzInstance, func(row *sql.Row)
&instance.consoleID,
&instance.consoleAppID,
&lang,
&securityPolicyEnabled,
&enableIframeEmbedding,
&instance.csp.allowedOrigins,
&enableImpersonation,
&auditLogRetention,
&block,
&features,
@@ -520,7 +527,8 @@ func scanAuthzInstance(host, domain string) (*authzInstance, func(row *sql.Row)
if block.Valid {
instance.block = &block.Bool
}
instance.csp.enabled = securityPolicyEnabled.Bool
instance.csp.enableIframeEmbedding = enableIframeEmbedding.Bool
instance.enableImpersonation = enableImpersonation.Bool
if len(features) == 0 {
return nil
}

View File

@@ -18,13 +18,14 @@ select
i.console_client_id,
i.console_app_id,
i.default_language,
s.enabled,
s.enable_iframe_embedding,
s.origins,
s.enable_impersonation,
l.audit_log_retention,
l.block,
f.features
from domain d
join projections.instances i on i.id = d.instance_id
left join projections.security_policies s on i.id = s.instance_id
left join projections.security_policies2 s on i.id = s.instance_id
left join projections.limits l on i.id = l.instance_id
left join features f on i.id = f.instance_id;

View File

@@ -15,13 +15,14 @@ select
i.console_client_id,
i.console_app_id,
i.default_language,
s.enabled,
s.enable_iframe_embedding,
s.origins,
s.enable_impersonation,
l.audit_log_retention,
l.block,
f.features
from projections.instances i
left join projections.security_policies s on i.id = s.instance_id
left join projections.security_policies2 s on i.id = s.instance_id
left join projections.limits l on i.id = l.instance_id
left join features f on i.id = f.instance_id
where i.id = $1;

View File

@@ -11,13 +11,14 @@ import (
)
const (
SecurityPolicyProjectionTable = "projections.security_policies"
SecurityPolicyColumnInstanceID = "instance_id"
SecurityPolicyColumnCreationDate = "creation_date"
SecurityPolicyColumnChangeDate = "change_date"
SecurityPolicyColumnSequence = "sequence"
SecurityPolicyColumnEnabled = "enabled"
SecurityPolicyColumnAllowedOrigins = "origins"
SecurityPolicyProjectionTable = "projections.security_policies2"
SecurityPolicyColumnInstanceID = "instance_id"
SecurityPolicyColumnCreationDate = "creation_date"
SecurityPolicyColumnChangeDate = "change_date"
SecurityPolicyColumnSequence = "sequence"
SecurityPolicyColumnEnableIframeEmbedding = "enable_iframe_embedding"
SecurityPolicyColumnAllowedOrigins = "origins"
SecurityPolicyColumnEnableImpersonation = "enable_impersonation"
)
type securityPolicyProjection struct{}
@@ -37,8 +38,9 @@ func (*securityPolicyProjection) Init() *old_handler.Check {
handler.NewColumn(SecurityPolicyColumnChangeDate, handler.ColumnTypeTimestamp),
handler.NewColumn(SecurityPolicyColumnInstanceID, handler.ColumnTypeText),
handler.NewColumn(SecurityPolicyColumnSequence, handler.ColumnTypeInt64),
handler.NewColumn(SecurityPolicyColumnEnabled, handler.ColumnTypeBool, handler.Default(false)),
handler.NewColumn(SecurityPolicyColumnEnableIframeEmbedding, handler.ColumnTypeBool, handler.Default(false)),
handler.NewColumn(SecurityPolicyColumnAllowedOrigins, handler.ColumnTypeTextArray, handler.Nullable()),
handler.NewColumn(SecurityPolicyColumnEnableImpersonation, handler.ColumnTypeBool, handler.Default(false)),
},
handler.NewPrimaryKey(SecurityPolicyColumnInstanceID),
),
@@ -74,12 +76,17 @@ func (p *securityPolicyProjection) reduceSecurityPolicySet(event eventstore.Even
handler.NewCol(SecurityPolicyColumnInstanceID, e.Aggregate().InstanceID),
handler.NewCol(SecurityPolicyColumnSequence, e.Sequence()),
}
if e.Enabled != nil {
changes = append(changes, handler.NewCol(SecurityPolicyColumnEnabled, *e.Enabled))
if e.EnableIframeEmbedding != nil {
changes = append(changes, handler.NewCol(SecurityPolicyColumnEnableIframeEmbedding, *e.EnableIframeEmbedding))
} else if e.Enabled != nil {
changes = append(changes, handler.NewCol(SecurityPolicyColumnEnableIframeEmbedding, *e.Enabled))
}
if e.AllowedOrigins != nil {
changes = append(changes, handler.NewCol(SecurityPolicyColumnAllowedOrigins, e.AllowedOrigins))
}
if e.EnableImpersonation != nil {
changes = append(changes, handler.NewCol(SecurityPolicyColumnEnableImpersonation, e.EnableImpersonation))
}
return handler.NewUpsertStatement(
e,
[]handler.Column{

View File

@@ -36,14 +36,18 @@ var (
name: projection.SecurityPolicyColumnSequence,
table: securityPolicyTable,
}
SecurityPolicyColumnEnabled = Column{
name: projection.SecurityPolicyColumnEnabled,
SecurityPolicyColumnEnableIframeEmbedding = Column{
name: projection.SecurityPolicyColumnEnableIframeEmbedding,
table: securityPolicyTable,
}
SecurityPolicyColumnAllowedOrigins = Column{
name: projection.SecurityPolicyColumnAllowedOrigins,
table: securityPolicyTable,
}
SecurityPolicyColumnEnableImpersonation = Column{
name: projection.SecurityPolicyColumnEnableImpersonation,
table: securityPolicyTable,
}
)
type SecurityPolicy struct {
@@ -53,8 +57,9 @@ type SecurityPolicy struct {
ResourceOwner string
Sequence uint64
Enabled bool
AllowedOrigins database.TextArray[string]
EnableIframeEmbedding bool
AllowedOrigins database.TextArray[string]
EnableImpersonation bool
}
func (q *Queries) SecurityPolicy(ctx context.Context) (policy *SecurityPolicy, err error) {
@@ -80,8 +85,9 @@ func prepareSecurityPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Sel
SecurityPolicyColumnChangeDate.identifier(),
SecurityPolicyColumnInstanceID.identifier(),
SecurityPolicyColumnSequence.identifier(),
SecurityPolicyColumnEnabled.identifier(),
SecurityPolicyColumnAllowedOrigins.identifier()).
SecurityPolicyColumnEnableIframeEmbedding.identifier(),
SecurityPolicyColumnAllowedOrigins.identifier(),
SecurityPolicyColumnEnableImpersonation.identifier()).
From(securityPolicyTable.identifier() + db.Timetravel(call.Took(ctx))).
PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*SecurityPolicy, error) {
@@ -92,8 +98,9 @@ func prepareSecurityPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Sel
&securityPolicy.ChangeDate,
&securityPolicy.ResourceOwner,
&securityPolicy.Sequence,
&securityPolicy.Enabled,
&securityPolicy.EnableIframeEmbedding,
&securityPolicy.AllowedOrigins,
&securityPolicy.EnableImpersonation,
)
if err != nil && !errors.Is(err, sql.ErrNoRows) { // ignore not found errors
return nil, zerrors.ThrowInternal(err, "QUERY-Dfrt2", "Errors.Internal")

View File

@@ -15,8 +15,12 @@ const (
type SecurityPolicySetEvent struct {
eventstore.BaseEvent `json:"-"`
Enabled *bool `json:"enabled,omitempty"`
AllowedOrigins *[]string `json:"allowedOrigins,omitempty"`
// Enabled is a legacy field which was used before for Iframe Embedding.
// It is kept so older events can still be reduced.
Enabled *bool `json:"enabled,omitempty"`
EnableIframeEmbedding *bool `json:"enable_iframe_embedding,omitempty"`
AllowedOrigins *[]string `json:"allowedOrigins,omitempty"`
EnableImpersonation *bool `json:"enable_impersonation,omitempty"`
}
func NewSecurityPolicySetEvent(
@@ -42,9 +46,9 @@ func NewSecurityPolicySetEvent(
type SecurityPolicyChanges func(event *SecurityPolicySetEvent)
func ChangeSecurityPolicyEnabled(enabled bool) func(event *SecurityPolicySetEvent) {
func ChangeSecurityPolicyEnableIframeEmbedding(enabled bool) func(event *SecurityPolicySetEvent) {
return func(e *SecurityPolicySetEvent) {
e.Enabled = &enabled
e.EnableIframeEmbedding = &enabled
}
}
@@ -57,6 +61,12 @@ func ChangeSecurityPolicyAllowedOrigins(allowedOrigins []string) func(event *Sec
}
}
func ChangeSecurityPolicyEnableImpersonation(enabled bool) func(event *SecurityPolicySetEvent) {
return func(e *SecurityPolicySetEvent) {
e.EnableImpersonation = &enabled
}
}
func (e *SecurityPolicySetEvent) Payload() interface{} {
return e
}