fixup! fixup! fixup! fixup! fixup! fixup! fixup! refactor(api): moving organization API resourced based

added Deactivate/ActivateOrg
This commit is contained in:
Iraq Jaber
2025-05-01 12:37:14 +02:00
parent 40de3d462a
commit 96831fb5f5
5 changed files with 308 additions and 98 deletions

View File

@@ -15,7 +15,9 @@ import (
gofakeit "github.com/brianvoe/gofakeit/v6"
"github.com/zitadel/zitadel/internal/integration"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
@@ -23,7 +25,7 @@ import (
var (
CTX context.Context
Instance *integration.Instance
Client org.OrganizationServiceClient
Client v2beta_org.OrganizationServiceClient
User *user.AddHumanUserResponse
)
@@ -42,66 +44,20 @@ func TestMain(m *testing.M) {
}())
}
func TestServer_GetOrganizationByID(t *testing.T) {
orgs, orgsName, err := createOrgs(1)
if err != nil {
assert.Fail(t, "unable to create org")
}
orgId := orgs[0].OrganizationId
orgName := orgsName[0]
tests := []struct {
name string
ctx context.Context
req *org.GetOrganizationByIDRequest
want *org.GetOrganizationByIDResponse
wantErr bool
}{
{
name: "get organization happy path",
ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeIAMOwner),
req: &org.GetOrganizationByIDRequest{
Id: orgId,
},
},
{
name: "get organization that doesn't exist",
ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeIAMOwner),
req: &org.GetOrganizationByIDRequest{
Id: "non existing organization",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.GetOrganizationByID(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, orgId, got.Organization.Id)
require.Equal(t, orgName, got.Organization.Name)
})
}
}
func TestServer_CreateOrganization(t *testing.T) {
idpResp := Instance.AddGenericOAuthProvider(CTX, Instance.DefaultOrg.Id)
tests := []struct {
name string
ctx context.Context
req *org.CreateOrganizationRequest
want *org.CreateOrganizationResponse
req *v2beta_org.CreateOrganizationRequest
want *v2beta_org.CreateOrganizationResponse
wantErr bool
}{
{
name: "missing permission",
ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &org.CreateOrganizationRequest{
req: &v2beta_org.CreateOrganizationRequest{
Name: "name",
Admins: nil,
},
@@ -110,7 +66,7 @@ func TestServer_CreateOrganization(t *testing.T) {
{
name: "empty name",
ctx: CTX,
req: &org.CreateOrganizationRequest{
req: &v2beta_org.CreateOrganizationRequest{
Name: "",
Admins: nil,
},
@@ -119,9 +75,9 @@ func TestServer_CreateOrganization(t *testing.T) {
{
name: "invalid admin type",
ctx: CTX,
req: &org.CreateOrganizationRequest{
req: &v2beta_org.CreateOrganizationRequest{
Name: gofakeit.AppName(),
Admins: []*org.CreateOrganizationRequest_Admin{
Admins: []*v2beta_org.CreateOrganizationRequest_Admin{
{},
},
},
@@ -130,11 +86,11 @@ func TestServer_CreateOrganization(t *testing.T) {
{
name: "admin with init",
ctx: CTX,
req: &org.CreateOrganizationRequest{
req: &v2beta_org.CreateOrganizationRequest{
Name: gofakeit.AppName(),
Admins: []*org.CreateOrganizationRequest_Admin{
Admins: []*v2beta_org.CreateOrganizationRequest_Admin{
{
UserType: &org.CreateOrganizationRequest_Admin_Human{
UserType: &v2beta_org.CreateOrganizationRequest_Admin_Human{
Human: &user_v2beta.AddHumanUserRequest{
Profile: &user_v2beta.SetHumanProfile{
GivenName: "firstname",
@@ -151,9 +107,9 @@ func TestServer_CreateOrganization(t *testing.T) {
},
},
},
want: &org.CreateOrganizationResponse{
want: &v2beta_org.CreateOrganizationResponse{
OrganizationId: integration.NotEmpty,
CreatedAdmins: []*org.CreateOrganizationResponse_CreatedAdmin{
CreatedAdmins: []*v2beta_org.CreateOrganizationResponse_CreatedAdmin{
{
UserId: integration.NotEmpty,
EmailCode: gu.Ptr(integration.NotEmpty),
@@ -165,14 +121,14 @@ func TestServer_CreateOrganization(t *testing.T) {
{
name: "existing user and new human with idp",
ctx: CTX,
req: &org.CreateOrganizationRequest{
req: &v2beta_org.CreateOrganizationRequest{
Name: gofakeit.AppName(),
Admins: []*org.CreateOrganizationRequest_Admin{
Admins: []*v2beta_org.CreateOrganizationRequest_Admin{
{
UserType: &org.CreateOrganizationRequest_Admin_UserId{UserId: User.GetUserId()},
UserType: &v2beta_org.CreateOrganizationRequest_Admin_UserId{UserId: User.GetUserId()},
},
{
UserType: &org.CreateOrganizationRequest_Admin_Human{
UserType: &v2beta_org.CreateOrganizationRequest_Admin_Human{
Human: &user_v2beta.AddHumanUserRequest{
Profile: &user_v2beta.SetHumanProfile{
GivenName: "firstname",
@@ -196,8 +152,8 @@ func TestServer_CreateOrganization(t *testing.T) {
},
},
},
want: &org.CreateOrganizationResponse{
CreatedAdmins: []*org.CreateOrganizationResponse_CreatedAdmin{
want: &v2beta_org.CreateOrganizationResponse{
CreatedAdmins: []*v2beta_org.CreateOrganizationResponse_CreatedAdmin{
// a single admin is expected, because the first provided already exists
{
UserId: integration.NotEmpty,
@@ -246,14 +202,14 @@ func TestServer_UpdateOrganization(t *testing.T) {
tests := []struct {
name string
ctx context.Context
req *org.UpdateOrganizationRequest
want *org.UpdateOrganizationResponse
req *v2beta_org.UpdateOrganizationRequest
want *v2beta_org.UpdateOrganizationResponse
wantErr bool
}{
{
name: "update org with new name",
ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeIAMOwner),
req: &org.UpdateOrganizationRequest{
req: &v2beta_org.UpdateOrganizationRequest{
Id: orgId,
Name: "new org name",
},
@@ -261,7 +217,7 @@ func TestServer_UpdateOrganization(t *testing.T) {
{
name: "update org with same name",
ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeIAMOwner),
req: &org.UpdateOrganizationRequest{
req: &v2beta_org.UpdateOrganizationRequest{
Id: orgId,
Name: orgName,
},
@@ -269,7 +225,7 @@ func TestServer_UpdateOrganization(t *testing.T) {
{
name: "update org with no id",
ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeIAMOwner),
req: &org.UpdateOrganizationRequest{
req: &v2beta_org.UpdateOrganizationRequest{
Id: orgId,
// Name: "",
},
@@ -295,6 +251,52 @@ func TestServer_UpdateOrganization(t *testing.T) {
}
}
func TestServer_GetOrganizationByID(t *testing.T) {
orgs, orgsName, err := createOrgs(1)
if err != nil {
assert.Fail(t, "unable to create org")
}
orgId := orgs[0].OrganizationId
orgName := orgsName[0]
tests := []struct {
name string
ctx context.Context
req *v2beta_org.GetOrganizationByIDRequest
want *v2beta_org.GetOrganizationByIDResponse
wantErr bool
}{
{
name: "get organization happy path",
ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeIAMOwner),
req: &v2beta_org.GetOrganizationByIDRequest{
Id: orgId,
},
},
{
name: "get organization that doesn't exist",
ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeIAMOwner),
req: &v2beta_org.GetOrganizationByIDRequest{
Id: "non existing organization",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.GetOrganizationByID(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, orgId, got.Organization.Id)
require.Equal(t, orgName, got.Organization.Name)
})
}
}
// TODO: finish off qyery testing in ListOrganizations
func TestServer_ListOrganization(t *testing.T) {
noOfOrgs := 3
@@ -306,15 +308,15 @@ func TestServer_ListOrganization(t *testing.T) {
tests := []struct {
name string
ctx context.Context
req *org.ListOrganizationsRequest
want []*org.Organization
req *v2beta_org.ListOrganizationsRequest
want []*v2beta_org.Organization
wantErr bool
}{
{
name: "list organizations happy path",
ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeIAMOwner),
req: &org.ListOrganizationsRequest{},
want: []*org.Organization{
req: &v2beta_org.ListOrganizationsRequest{},
want: []*v2beta_org.Organization{
{
Id: orgs[0].OrganizationId,
Name: orgsName[0],
@@ -370,8 +372,8 @@ func TestServer_DeleteOrganization(t *testing.T) {
name string
ctx context.Context
createOrgFunc func() string
req *org.DeleteOrganizationRequest
want *org.DeleteOrganizationResponse
req *v2beta_org.DeleteOrganizationRequest
want *v2beta_org.DeleteOrganizationResponse
err error
}{
{
@@ -384,12 +386,12 @@ func TestServer_DeleteOrganization(t *testing.T) {
}
return orgs[0].OrganizationId
},
req: &org.DeleteOrganizationRequest{},
req: &v2beta_org.DeleteOrganizationRequest{},
},
{
name: "delete non existent org",
ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeIAMOwner),
req: &org.DeleteOrganizationRequest{
req: &v2beta_org.DeleteOrganizationRequest{
Id: "non existent org id",
},
err: fmt.Errorf("Organisation not found"),
@@ -415,7 +417,7 @@ func TestServer_DeleteOrganization(t *testing.T) {
assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute))
assert.NotEmpty(t, got.GetDetails().GetResourceOwner())
_, err = Client.GetOrganizationByID(tt.ctx, &org.GetOrganizationByIDRequest{
_, err = Client.GetOrganizationByID(tt.ctx, &v2beta_org.GetOrganizationByIDRequest{
Id: tt.req.Id,
})
require.Contains(t, err.Error(), "Organisation not found")
@@ -423,16 +425,121 @@ func TestServer_DeleteOrganization(t *testing.T) {
}
}
func createOrgs(noOfOrgs int) ([]*org.CreateOrganizationResponse, []string, error) {
func TestServer_DeactivateReactivateNonExistentOrganization(t *testing.T) {
ctx := Instance.WithAuthorization(context.Background(), integration.UserTypeIAMOwner)
// deactivate non existent organization
_, err := Client.DeactivateOrganization(ctx, &v2beta_org.DeactivateOrganizationRequest{
Id: "non existent organization",
})
require.Contains(t, err.Error(), "Organisation not found")
// reactivate non existent organization
_, err = Client.ReactivateOrganization(ctx, &v2beta_org.ReactivateOrganizationRequest{
Id: "non existent organization",
})
require.Contains(t, err.Error(), "Organisation not found")
}
func TestServer_DeactivateReactivateOrganization(t *testing.T) {
// 1. create organization
orgs, _, err := createOrgs(1)
if err != nil {
assert.Fail(t, "unable to create orgs")
}
orgId := orgs[0].OrganizationId
ctx := Instance.WithAuthorization(context.Background(), integration.UserTypeIAMOwner)
// 2. check inital state of organization
res, err := Client.GetOrganizationByID(ctx, &org.GetOrganizationByIDRequest{
Id: orgId,
})
require.NoError(t, err)
require.Equal(t, v2beta_org.OrganizationState_ORGANIZATION_STATE_ACTIVE, res.Organization.State)
// 3. deactivate organization once
deactivate_res, err := Client.DeactivateOrganization(ctx, &v2beta_org.DeactivateOrganizationRequest{
Id: orgId,
})
require.NoError(t, err)
assert.NotZero(t, deactivate_res.GetDetails().GetSequence())
gotCD := deactivate_res.GetDetails().GetChangeDate().AsTime()
now := time.Now()
assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute))
assert.NotEmpty(t, deactivate_res.GetDetails().GetResourceOwner())
// 4. check organization state is deactivated
res, err = Client.GetOrganizationByID(ctx, &v2beta_org.GetOrganizationByIDRequest{
Id: orgId,
})
require.NoError(t, err)
require.Equal(t, v2beta_org.OrganizationState_ORGANIZATION_STATE_INACTIVE, res.Organization.State)
// 5. repeat deactivate organization once
deactivate_res, err = Client.DeactivateOrganization(ctx, &v2beta_org.DeactivateOrganizationRequest{
Id: orgId,
})
require.NoError(t, err)
assert.NotZero(t, deactivate_res.GetDetails().GetSequence())
gotCD = deactivate_res.GetDetails().GetChangeDate().AsTime()
now = time.Now()
assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute))
assert.NotEmpty(t, deactivate_res.GetDetails().GetResourceOwner())
// 6. repeat check organization state is still deactivated
res, err = Client.GetOrganizationByID(ctx, &v2beta_org.GetOrganizationByIDRequest{
Id: orgId,
})
require.NoError(t, err)
require.Equal(t, v2beta_org.OrganizationState_ORGANIZATION_STATE_INACTIVE, res.Organization.State)
// 7. reactivate organization
reactivate_res, err := Client.ReactivateOrganization(ctx, &v2beta_org.ReactivateOrganizationRequest{
Id: orgId,
})
require.NoError(t, err)
assert.NotZero(t, reactivate_res.GetDetails().GetSequence())
gotCD = reactivate_res.GetDetails().GetChangeDate().AsTime()
now = time.Now()
assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute))
assert.NotEmpty(t, reactivate_res.GetDetails().GetResourceOwner())
// 8. check organization state is active
res, err = Client.GetOrganizationByID(ctx, &v2beta_org.GetOrganizationByIDRequest{
Id: orgId,
})
require.NoError(t, err)
require.Equal(t, v2beta_org.OrganizationState_ORGANIZATION_STATE_ACTIVE, res.Organization.State)
// 9. repeat reactivate organization
reactivate_res, err = Client.ReactivateOrganization(ctx, &v2beta_org.ReactivateOrganizationRequest{
Id: orgId,
})
require.NoError(t, err)
assert.NotZero(t, reactivate_res.GetDetails().GetSequence())
gotCD = reactivate_res.GetDetails().GetChangeDate().AsTime()
now = time.Now()
assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute))
assert.NotEmpty(t, reactivate_res.GetDetails().GetResourceOwner())
// 10. repeat check organization state is still active
res, err = Client.GetOrganizationByID(ctx, &v2beta_org.GetOrganizationByIDRequest{
Id: orgId,
})
require.NoError(t, err)
require.Equal(t, v2beta_org.OrganizationState_ORGANIZATION_STATE_ACTIVE, res.Organization.State)
}
func createOrgs(noOfOrgs int) ([]*v2beta_org.CreateOrganizationResponse, []string, error) {
var err error
orgs := make([]*org.CreateOrganizationResponse, noOfOrgs)
orgs := make([]*v2beta_org.CreateOrganizationResponse, noOfOrgs)
orgsName := make([]string, noOfOrgs)
for i := range noOfOrgs {
orgName := gofakeit.Name()
orgsName[i] = orgName
orgs[i], err = Client.CreateOrganization(CTX,
&org.CreateOrganizationRequest{
&v2beta_org.CreateOrganizationRequest{
Name: orgName,
},
)
@@ -444,7 +551,7 @@ func createOrgs(noOfOrgs int) ([]*org.CreateOrganizationResponse, []string, erro
return orgs, orgsName, nil
}
func assertCreatedAdmin(t *testing.T, expected, got *org.CreateOrganizationResponse_CreatedAdmin) {
func assertCreatedAdmin(t *testing.T, expected, got *v2beta_org.CreateOrganizationResponse_CreatedAdmin) {
if expected.GetUserId() != "" {
assert.NotEmpty(t, got.GetUserId())
} else {

View File

@@ -7,6 +7,7 @@ import (
user "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/zerrors"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
)
@@ -68,6 +69,26 @@ func (s *Server) DeleteOrganization(ctx context.Context, request *v2beta_org.Del
}, nil
}
func (s *Server) DeactivateOrganization(ctx context.Context, request *org.DeactivateOrganizationRequest) (*org.DeactivateOrganizationResponse, error) {
objectDetails, err := s.command.DeactivateOrg(ctx, request.Id)
if err != nil {
return nil, err
}
return &org.DeactivateOrganizationResponse{
Details: object.DomainToDetailsPb(objectDetails),
}, nil
}
func (s *Server) ReactivateOrganization(ctx context.Context, request *org.ReactivateOrganizationRequest) (*org.ReactivateOrganizationResponse, error) {
objectDetails, err := s.command.ReactivateOrg(ctx, request.Id)
if err != nil {
return nil, err
}
return &org.ReactivateOrganizationResponse{
Details: object.DomainToDetailsPb(objectDetails),
}, err
}
func createOrganizationRequestToCommand(request *v2beta_org.CreateOrganizationRequest) (*command.OrgSetup, error) {
admins, err := createOrganizationRequestAdminsToCommand(request.GetAdmins())
if err != nil {

View File

@@ -353,7 +353,7 @@ func (c *Commands) UpdateOrg(ctx context.Context, orgID, name string) (*domain.O
return nil, err
}
if !isOrgStateExists(orgWriteModel.State) {
return nil, zerrors.ThrowNotFound(nil, "ORG-1MRds", "Errors.Org.NotFound")
return writeModelToObjectDetails(&orgWriteModel.WriteModel), nil
}
if orgWriteModel.Name == name {
return writeModelToObjectDetails(&orgWriteModel.WriteModel), nil
@@ -388,7 +388,7 @@ func (c *Commands) DeactivateOrg(ctx context.Context, orgID string) (*domain.Obj
return nil, zerrors.ThrowNotFound(nil, "ORG-oL9nT", "Errors.Org.NotFound")
}
if orgWriteModel.State == domain.OrgStateInactive {
return nil, zerrors.ThrowPreconditionFailed(nil, "EVENT-Dbs2g", "Errors.Org.AlreadyDeactivated")
return writeModelToObjectDetails(&orgWriteModel.WriteModel), nil
}
orgAgg := OrgAggregateFromWriteModel(&orgWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, org.NewOrgDeactivatedEvent(ctx, orgAgg))
@@ -411,7 +411,7 @@ func (c *Commands) ReactivateOrg(ctx context.Context, orgID string) (*domain.Obj
return nil, zerrors.ThrowNotFound(nil, "ORG-Dgf3g", "Errors.Org.NotFound")
}
if orgWriteModel.State == domain.OrgStateActive {
return nil, zerrors.ThrowPreconditionFailed(nil, "EVENT-bfnrh", "Errors.Org.AlreadyActive")
return writeModelToObjectDetails(&orgWriteModel.WriteModel), nil
}
orgAgg := OrgAggregateFromWriteModel(&orgWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, org.NewOrgReactivatedEvent(ctx, orgAgg))

View File

@@ -32,6 +32,7 @@ message Organization {
];
}
// TODO rename to OrgState
enum OrganizationState {
ORGANIZATION_STATE_UNSPECIFIED = 0;
ORGANIZATION_STATE_ACTIVE = 1;
@@ -80,7 +81,7 @@ message OrgDomainQuery {
}
message OrgStateQuery {
OrgState state = 1 [
OrganizationState state = 1 [
(validate.rules).enum.defined_only = true,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "current state of the organization";
@@ -97,14 +98,3 @@ message OrgIDQuery {
];
}
enum OrgState {
ORG_STATE_UNSPECIFIED = 0;
ORG_STATE_ACTIVE = 1;
ORG_STATE_INACTIVE = 2;
ORG_STATE_REMOVED = 3;
}
enum OrgFieldName {
ORG_FIELD_NAME_UNSPECIFIED = 0;
ORG_FIELD_NAME_NAME = 1;
}

View File

@@ -249,7 +249,7 @@ service OrganizationService {
responses: {
key: "200";
value: {
description: "org removed successfully";
description: "org deleted successfully";
};
};
responses: {
@@ -266,6 +266,66 @@ service OrganizationService {
};
}
rpc DeactivateOrganization(DeactivateOrganizationRequest) returns (DeactivateOrganizationResponse) {
option (google.api.http) = {
post: "/v2beta/_deactivate"
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "org.write"
}
http_response: {
success_code: 200
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "Organizations";
summary: "Deactivate Organization";
description: "Sets the state of my organization to deactivated. Users of this organization will not be able to log in."
parameters: {
headers: {
name: "x-zitadel-orgid";
description: "The default is always the organization of the requesting user. If you like to get users of another organization include the header. Make sure the user has permission to access the requested data.";
type: STRING,
required: false;
};
};
};
}
rpc ReactivateOrganization(ReactivateOrganizationRequest) returns (ReactivateOrganizationResponse) {
option (google.api.http) = {
post: "/orgs/me/_reactivate"
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "org.write"
}
http_response: {
success_code: 200
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "Organizations";
summary: "Reactivate Organization";
description: "Set the state of my organization to active. The state of the organization has to be deactivated to perform the request. Users of this organization will be able to log in again."
parameters: {
headers: {
name: "x-zitadel-orgid";
description: "The default is always the organization of the requesting user. If you like to get users of another organization include the header. Make sure the user has permission to access the requested data.";
type: STRING,
required: false;
};
};
};
}
}
message CreateOrganizationRequest{
@@ -387,3 +447,35 @@ message DeleteOrganizationRequest {
message DeleteOrganizationResponse {
zitadel.object.v2beta.Details details = 1;
}
//This is an empty request
message DeactivateOrganizationRequest {
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629023906488334\"";
min_length: 1;
max_length: 200;
}
];
}
message DeactivateOrganizationResponse {
zitadel.object.v2beta.Details details = 1;
}
//This is an empty request
message ReactivateOrganizationRequest {
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629023906488334\"";
min_length: 1;
max_length: 200;
}
];
}
message ReactivateOrganizationResponse {
zitadel.object.v2beta.Details details = 1;
}