mirror of
https://github.com/zitadel/zitadel.git
synced 2025-07-29 20:13:42 +00:00
feat: org v2 ListOrganizations (#8411)
# Which Problems Are Solved Org v2 service does not have a ListOrganizations endpoint. # How the Problems Are Solved Implement ListOrganizations endpoint. # Additional Changes - moved descriptions in the protos to comments - corrected the RemoveNoPermissions for the ListUsers, to get the correct TotalResults # Additional Context For new typescript login
This commit is contained in:
parent
3e3d46ac0d
commit
5fab533e37
@ -356,6 +356,14 @@ module.exports = {
|
|||||||
categoryLinkSource: "auto",
|
categoryLinkSource: "auto",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
org_v2: {
|
||||||
|
specPath: ".artifacts/openapi/zitadel/org/v2/org_service.swagger.json",
|
||||||
|
outputDir: "docs/apis/resources/org_service_v2",
|
||||||
|
sidebarOptions: {
|
||||||
|
groupPathsBy: "tag",
|
||||||
|
categoryLinkSource: "auto",
|
||||||
|
},
|
||||||
|
},
|
||||||
idp_v2: {
|
idp_v2: {
|
||||||
specPath: ".artifacts/openapi/zitadel/idp/v2/idp_service.swagger.json",
|
specPath: ".artifacts/openapi/zitadel/idp/v2/idp_service.swagger.json",
|
||||||
outputDir: "docs/apis/resources/idp_service_v2",
|
outputDir: "docs/apis/resources/idp_service_v2",
|
||||||
|
@ -679,6 +679,18 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
items: require("./docs/apis/resources/feature_service_v2/sidebar.ts"),
|
items: require("./docs/apis/resources/feature_service_v2/sidebar.ts"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
label: "Organization Lifecycle",
|
||||||
|
link: {
|
||||||
|
type: "generated-index",
|
||||||
|
title: "Organization Service API",
|
||||||
|
slug: "/apis/resources/org_service/v2",
|
||||||
|
description:
|
||||||
|
'This API is intended to manage organizations for ZITADEL. \n'
|
||||||
|
},
|
||||||
|
items: require("./docs/apis/resources/org_service_v2/sidebar.ts"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "category",
|
type: "category",
|
||||||
label: "Identity Provider Lifecycle",
|
label: "Identity Provider Lifecycle",
|
||||||
|
@ -36,7 +36,7 @@ func (s *Server) ExportData(ctx context.Context, req *admin_pb.ExportDataRequest
|
|||||||
}
|
}
|
||||||
orgSearchQuery.Queries = []query.SearchQuery{orgIDsSearchQuery}
|
orgSearchQuery.Queries = []query.SearchQuery{orgIDsSearchQuery}
|
||||||
}
|
}
|
||||||
queriedOrgs, err := s.query.SearchOrgs(ctx, orgSearchQuery)
|
queriedOrgs, err := s.query.SearchOrgs(ctx, orgSearchQuery, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -554,7 +554,7 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{orgSearch}})
|
users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{orgSearch}}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ func (s *Server) ListOrgs(ctx context.Context, req *admin_pb.ListOrgsRequest) (*
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
orgs, err := s.query.SearchOrgs(ctx, queries)
|
orgs, err := s.query.SearchOrgs(ctx, queries, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -108,7 +108,7 @@ func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain str
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}})
|
users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -220,7 +220,7 @@ func (s *Server) ListMyProjectOrgs(ctx context.Context, req *auth_pb.ListMyProje
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
orgs, err := s.query.SearchOrgs(ctx, queries)
|
orgs, err := s.query.SearchOrgs(ctx, queries, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -330,7 +330,7 @@ func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain, or
|
|||||||
}
|
}
|
||||||
queries = append(queries, owner)
|
queries = append(queries, owner)
|
||||||
}
|
}
|
||||||
users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: queries})
|
users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: queries}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ func (s *Server) ListUsers(ctx context.Context, req *mgmt_pb.ListUsersRequest) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
res, err := s.query.SearchUsers(ctx, queries)
|
res, err := s.query.SearchUsers(ctx, queries, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -19,22 +19,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
CTX context.Context
|
CTX context.Context
|
||||||
Tester *integration.Tester
|
OwnerCTX context.Context
|
||||||
Client org.OrganizationServiceClient
|
UserCTX context.Context
|
||||||
User *user.AddHumanUserResponse
|
Tester *integration.Tester
|
||||||
|
Client org.OrganizationServiceClient
|
||||||
|
User *user.AddHumanUserResponse
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
os.Exit(func() int {
|
os.Exit(func() int {
|
||||||
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
|
ctx, _, cancel := integration.Contexts(5 * time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
Tester = integration.NewTester(ctx)
|
Tester = integration.NewTester(ctx)
|
||||||
defer Tester.Done()
|
defer Tester.Done()
|
||||||
Client = Tester.Client.OrgV2
|
Client = Tester.Client.OrgV2
|
||||||
|
|
||||||
CTX, _ = Tester.WithAuthorization(ctx, integration.IAMOwner), errCtx
|
CTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
|
||||||
|
OwnerCTX = Tester.WithAuthorization(ctx, integration.OrgOwner)
|
||||||
|
UserCTX = Tester.WithAuthorization(ctx, integration.Login)
|
||||||
User = Tester.CreateHumanUser(CTX)
|
User = Tester.CreateHumanUser(CTX)
|
||||||
return m.Run()
|
return m.Run()
|
||||||
}())
|
}())
|
||||||
|
132
internal/api/grpc/org/v2/query.go
Normal file
132
internal/api/grpc/org/v2/query.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package org
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) ListOrganizations(ctx context.Context, req *org.ListOrganizationsRequest) (*org.ListOrganizationsResponse, error) {
|
||||||
|
queries, err := listOrgRequestToModel(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
orgs, err := s.query.SearchOrgs(ctx, queries, s.checkPermission)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &org.ListOrganizationsResponse{
|
||||||
|
Result: organizationsToPb(orgs.Orgs),
|
||||||
|
Details: object.ToListDetails(orgs.SearchResponse),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listOrgRequestToModel(req *org.ListOrganizationsRequest) (*query.OrgSearchQueries, error) {
|
||||||
|
offset, limit, asc := object.ListQueryToQuery(req.Query)
|
||||||
|
queries, err := orgQueriesToQuery(req.Queries)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &query.OrgSearchQueries{
|
||||||
|
SearchRequest: query.SearchRequest{
|
||||||
|
Offset: offset,
|
||||||
|
Limit: limit,
|
||||||
|
SortingColumn: fieldNameToOrganizationColumn(req.SortingColumn),
|
||||||
|
Asc: asc,
|
||||||
|
},
|
||||||
|
Queries: queries,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func orgQueriesToQuery(queries []*org.SearchQuery) (_ []query.SearchQuery, err error) {
|
||||||
|
q := make([]query.SearchQuery, len(queries))
|
||||||
|
for i, query := range queries {
|
||||||
|
q[i], err = orgQueryToQuery(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return q, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func orgQueryToQuery(orgQuery *org.SearchQuery) (query.SearchQuery, error) {
|
||||||
|
switch q := orgQuery.Query.(type) {
|
||||||
|
case *org.SearchQuery_DomainQuery:
|
||||||
|
return query.NewOrgDomainSearchQuery(object.TextMethodToQuery(q.DomainQuery.Method), q.DomainQuery.Domain)
|
||||||
|
case *org.SearchQuery_NameQuery:
|
||||||
|
return query.NewOrgNameSearchQuery(object.TextMethodToQuery(q.NameQuery.Method), q.NameQuery.Name)
|
||||||
|
case *org.SearchQuery_StateQuery:
|
||||||
|
return query.NewOrgStateSearchQuery(orgStateToDomain(q.StateQuery.State))
|
||||||
|
case *org.SearchQuery_IdQuery:
|
||||||
|
return query.NewOrgIDSearchQuery(q.IdQuery.Id)
|
||||||
|
default:
|
||||||
|
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func orgStateToPb(state domain.OrgState) org.OrganizationState {
|
||||||
|
switch state {
|
||||||
|
case domain.OrgStateActive:
|
||||||
|
return org.OrganizationState_ORGANIZATION_STATE_ACTIVE
|
||||||
|
case domain.OrgStateInactive:
|
||||||
|
return org.OrganizationState_ORGANIZATION_STATE_INACTIVE
|
||||||
|
case domain.OrgStateRemoved:
|
||||||
|
return org.OrganizationState_ORGANIZATION_STATE_REMOVED
|
||||||
|
case domain.OrgStateUnspecified:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return org.OrganizationState_ORGANIZATION_STATE_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func orgStateToDomain(state org.OrganizationState) domain.OrgState {
|
||||||
|
switch state {
|
||||||
|
case org.OrganizationState_ORGANIZATION_STATE_ACTIVE:
|
||||||
|
return domain.OrgStateActive
|
||||||
|
case org.OrganizationState_ORGANIZATION_STATE_INACTIVE:
|
||||||
|
return domain.OrgStateInactive
|
||||||
|
case org.OrganizationState_ORGANIZATION_STATE_REMOVED:
|
||||||
|
return domain.OrgStateRemoved
|
||||||
|
case org.OrganizationState_ORGANIZATION_STATE_UNSPECIFIED:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return domain.OrgStateUnspecified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldNameToOrganizationColumn(fieldName org.OrganizationFieldName) query.Column {
|
||||||
|
switch fieldName {
|
||||||
|
case org.OrganizationFieldName_ORGANIZATION_FIELD_NAME_NAME:
|
||||||
|
return query.OrgColumnName
|
||||||
|
case org.OrganizationFieldName_ORGANIZATION_FIELD_NAME_UNSPECIFIED:
|
||||||
|
return query.Column{}
|
||||||
|
default:
|
||||||
|
return query.Column{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func organizationsToPb(orgs []*query.Org) []*org.Organization {
|
||||||
|
o := make([]*org.Organization, len(orgs))
|
||||||
|
for i, org := range orgs {
|
||||||
|
o[i] = organizationToPb(org)
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
func organizationToPb(organization *query.Org) *org.Organization {
|
||||||
|
return &org.Organization{
|
||||||
|
Id: organization.ID,
|
||||||
|
Name: organization.Name,
|
||||||
|
PrimaryDomain: organization.Domain,
|
||||||
|
Details: object.DomainToDetailsPb(&domain.ObjectDetails{
|
||||||
|
Sequence: organization.Sequence,
|
||||||
|
EventDate: organization.ChangeDate,
|
||||||
|
ResourceOwner: organization.ResourceOwner,
|
||||||
|
}),
|
||||||
|
State: orgStateToPb(organization.State),
|
||||||
|
}
|
||||||
|
}
|
443
internal/api/grpc/org/v2/query_integration_test.go
Normal file
443
internal/api/grpc/org/v2/query_integration_test.go
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package org_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
|
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type orgAttr struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Details *object.Details
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_ListOrganizations(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
req *org.ListOrganizationsRequest
|
||||||
|
dep func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error)
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *org.ListOrganizationsResponse
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "list org by id, ok, multiple",
|
||||||
|
args: args{
|
||||||
|
CTX,
|
||||||
|
&org.ListOrganizationsRequest{
|
||||||
|
Queries: []*org.SearchQuery{
|
||||||
|
OrganizationIdQuery(Tester.Organisation.ID),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) {
|
||||||
|
count := 3
|
||||||
|
orgs := make([]orgAttr, count)
|
||||||
|
prefix := fmt.Sprintf("ListOrgs%d", time.Now().UnixNano())
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
name := prefix + strconv.Itoa(i)
|
||||||
|
orgResp := Tester.CreateOrganization(ctx, name, fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()))
|
||||||
|
orgs[i] = orgAttr{
|
||||||
|
ID: orgResp.GetOrganizationId(),
|
||||||
|
Name: name,
|
||||||
|
Details: orgResp.GetDetails(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request.Queries = []*org.SearchQuery{
|
||||||
|
OrganizationNamePrefixQuery(prefix),
|
||||||
|
}
|
||||||
|
return orgs, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &org.ListOrganizationsResponse{
|
||||||
|
Details: &object.ListDetails{
|
||||||
|
TotalResult: 3,
|
||||||
|
Timestamp: timestamppb.Now(),
|
||||||
|
},
|
||||||
|
SortingColumn: 0,
|
||||||
|
Result: []*org.Organization{
|
||||||
|
{
|
||||||
|
State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list org by id, ok",
|
||||||
|
args: args{
|
||||||
|
CTX,
|
||||||
|
&org.ListOrganizationsRequest{
|
||||||
|
Queries: []*org.SearchQuery{
|
||||||
|
OrganizationIdQuery(Tester.Organisation.ID),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
want: &org.ListOrganizationsResponse{
|
||||||
|
Details: &object.ListDetails{
|
||||||
|
TotalResult: 1,
|
||||||
|
Timestamp: timestamppb.Now(),
|
||||||
|
},
|
||||||
|
SortingColumn: 0,
|
||||||
|
Result: []*org.Organization{
|
||||||
|
{
|
||||||
|
State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE,
|
||||||
|
Name: Tester.Organisation.Name,
|
||||||
|
Details: &object.Details{
|
||||||
|
Sequence: Tester.Organisation.Sequence,
|
||||||
|
ChangeDate: timestamppb.New(Tester.Organisation.ChangeDate),
|
||||||
|
ResourceOwner: Tester.Organisation.ResourceOwner,
|
||||||
|
},
|
||||||
|
Id: Tester.Organisation.ID,
|
||||||
|
PrimaryDomain: Tester.Organisation.Domain,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list org by name, ok",
|
||||||
|
args: args{
|
||||||
|
CTX,
|
||||||
|
&org.ListOrganizationsRequest{
|
||||||
|
Queries: []*org.SearchQuery{
|
||||||
|
OrganizationNameQuery(Tester.Organisation.Name),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
want: &org.ListOrganizationsResponse{
|
||||||
|
Details: &object.ListDetails{
|
||||||
|
TotalResult: 1,
|
||||||
|
Timestamp: timestamppb.Now(),
|
||||||
|
},
|
||||||
|
SortingColumn: 0,
|
||||||
|
Result: []*org.Organization{
|
||||||
|
{
|
||||||
|
State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE,
|
||||||
|
Name: Tester.Organisation.Name,
|
||||||
|
Details: &object.Details{
|
||||||
|
Sequence: Tester.Organisation.Sequence,
|
||||||
|
ChangeDate: timestamppb.New(Tester.Organisation.ChangeDate),
|
||||||
|
ResourceOwner: Tester.Organisation.ResourceOwner,
|
||||||
|
},
|
||||||
|
Id: Tester.Organisation.ID,
|
||||||
|
PrimaryDomain: Tester.Organisation.Domain,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list org by domain, ok",
|
||||||
|
args: args{
|
||||||
|
CTX,
|
||||||
|
&org.ListOrganizationsRequest{
|
||||||
|
Queries: []*org.SearchQuery{
|
||||||
|
OrganizationDomainQuery(Tester.Organisation.Domain),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
want: &org.ListOrganizationsResponse{
|
||||||
|
Details: &object.ListDetails{
|
||||||
|
TotalResult: 1,
|
||||||
|
Timestamp: timestamppb.Now(),
|
||||||
|
},
|
||||||
|
SortingColumn: 0,
|
||||||
|
Result: []*org.Organization{
|
||||||
|
{
|
||||||
|
State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE,
|
||||||
|
Name: Tester.Organisation.Name,
|
||||||
|
Details: &object.Details{
|
||||||
|
Sequence: Tester.Organisation.Sequence,
|
||||||
|
ChangeDate: timestamppb.New(Tester.Organisation.ChangeDate),
|
||||||
|
ResourceOwner: Tester.Organisation.ResourceOwner,
|
||||||
|
},
|
||||||
|
Id: Tester.Organisation.ID,
|
||||||
|
PrimaryDomain: Tester.Organisation.Domain,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list org by inactive state, ok",
|
||||||
|
args: args{
|
||||||
|
CTX,
|
||||||
|
&org.ListOrganizationsRequest{
|
||||||
|
Queries: []*org.SearchQuery{},
|
||||||
|
},
|
||||||
|
func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) {
|
||||||
|
name := gofakeit.Name()
|
||||||
|
orgResp := Tester.CreateOrganization(ctx, name, gofakeit.Email())
|
||||||
|
deactivateOrgResp := Tester.DeactivateOrganization(ctx, orgResp.GetOrganizationId())
|
||||||
|
request.Queries = []*org.SearchQuery{
|
||||||
|
OrganizationIdQuery(orgResp.GetOrganizationId()),
|
||||||
|
OrganizationStateQuery(org.OrganizationState_ORGANIZATION_STATE_INACTIVE),
|
||||||
|
}
|
||||||
|
return []orgAttr{{
|
||||||
|
ID: orgResp.GetOrganizationId(),
|
||||||
|
Name: name,
|
||||||
|
Details: &object.Details{
|
||||||
|
ResourceOwner: deactivateOrgResp.GetDetails().GetResourceOwner(),
|
||||||
|
Sequence: deactivateOrgResp.GetDetails().GetSequence(),
|
||||||
|
ChangeDate: deactivateOrgResp.GetDetails().GetChangeDate(),
|
||||||
|
},
|
||||||
|
}}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &org.ListOrganizationsResponse{
|
||||||
|
Details: &object.ListDetails{
|
||||||
|
TotalResult: 1,
|
||||||
|
Timestamp: timestamppb.Now(),
|
||||||
|
},
|
||||||
|
SortingColumn: 0,
|
||||||
|
Result: []*org.Organization{
|
||||||
|
{
|
||||||
|
State: org.OrganizationState_ORGANIZATION_STATE_INACTIVE,
|
||||||
|
Details: &object.Details{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list org by domain, ok, sorted",
|
||||||
|
args: args{
|
||||||
|
CTX,
|
||||||
|
&org.ListOrganizationsRequest{
|
||||||
|
Queries: []*org.SearchQuery{
|
||||||
|
OrganizationDomainQuery(Tester.Organisation.Domain),
|
||||||
|
},
|
||||||
|
SortingColumn: org.OrganizationFieldName_ORGANIZATION_FIELD_NAME_NAME,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
want: &org.ListOrganizationsResponse{
|
||||||
|
Details: &object.ListDetails{
|
||||||
|
TotalResult: 1,
|
||||||
|
Timestamp: timestamppb.Now(),
|
||||||
|
},
|
||||||
|
SortingColumn: 1,
|
||||||
|
Result: []*org.Organization{
|
||||||
|
{
|
||||||
|
State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE,
|
||||||
|
Name: Tester.Organisation.Name,
|
||||||
|
Details: &object.Details{
|
||||||
|
Sequence: Tester.Organisation.Sequence,
|
||||||
|
ChangeDate: timestamppb.New(Tester.Organisation.ChangeDate),
|
||||||
|
ResourceOwner: Tester.Organisation.ResourceOwner,
|
||||||
|
},
|
||||||
|
Id: Tester.Organisation.ID,
|
||||||
|
PrimaryDomain: Tester.Organisation.Domain,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list org, no result",
|
||||||
|
args: args{
|
||||||
|
CTX,
|
||||||
|
&org.ListOrganizationsRequest{
|
||||||
|
Queries: []*org.SearchQuery{
|
||||||
|
OrganizationDomainQuery("notexisting"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
want: &org.ListOrganizationsResponse{
|
||||||
|
Details: &object.ListDetails{
|
||||||
|
TotalResult: 0,
|
||||||
|
Timestamp: timestamppb.Now(),
|
||||||
|
},
|
||||||
|
SortingColumn: 0,
|
||||||
|
Result: []*org.Organization{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list org, no login",
|
||||||
|
args: args{
|
||||||
|
context.Background(),
|
||||||
|
&org.ListOrganizationsRequest{
|
||||||
|
Queries: []*org.SearchQuery{
|
||||||
|
OrganizationDomainQuery("nopermission"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list org, no permission",
|
||||||
|
args: args{
|
||||||
|
UserCTX,
|
||||||
|
&org.ListOrganizationsRequest{},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
want: &org.ListOrganizationsResponse{
|
||||||
|
Details: &object.ListDetails{
|
||||||
|
TotalResult: 0,
|
||||||
|
Timestamp: timestamppb.Now(),
|
||||||
|
},
|
||||||
|
SortingColumn: 1,
|
||||||
|
Result: []*org.Organization{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list org, no permission org owner",
|
||||||
|
args: args{
|
||||||
|
OwnerCTX,
|
||||||
|
&org.ListOrganizationsRequest{
|
||||||
|
Queries: []*org.SearchQuery{
|
||||||
|
OrganizationDomainQuery("nopermission"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
want: &org.ListOrganizationsResponse{
|
||||||
|
Details: &object.ListDetails{
|
||||||
|
TotalResult: 0,
|
||||||
|
Timestamp: timestamppb.Now(),
|
||||||
|
},
|
||||||
|
SortingColumn: 1,
|
||||||
|
Result: []*org.Organization{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list org, org owner",
|
||||||
|
args: args{
|
||||||
|
OwnerCTX,
|
||||||
|
&org.ListOrganizationsRequest{},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
want: &org.ListOrganizationsResponse{
|
||||||
|
Details: &object.ListDetails{
|
||||||
|
TotalResult: 1,
|
||||||
|
Timestamp: timestamppb.Now(),
|
||||||
|
},
|
||||||
|
SortingColumn: 1,
|
||||||
|
Result: []*org.Organization{
|
||||||
|
{
|
||||||
|
State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE,
|
||||||
|
Name: Tester.Organisation.Name,
|
||||||
|
Details: &object.Details{
|
||||||
|
Sequence: Tester.Organisation.Sequence,
|
||||||
|
ChangeDate: timestamppb.New(Tester.Organisation.ChangeDate),
|
||||||
|
ResourceOwner: Tester.Organisation.ResourceOwner,
|
||||||
|
},
|
||||||
|
Id: Tester.Organisation.ID,
|
||||||
|
PrimaryDomain: Tester.Organisation.Domain,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.args.dep != nil {
|
||||||
|
orgs, err := tt.args.dep(tt.args.ctx, tt.args.req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
if len(orgs) > 0 {
|
||||||
|
for i, org := range orgs {
|
||||||
|
tt.want.Result[i].Name = org.Name
|
||||||
|
tt.want.Result[i].Id = org.ID
|
||||||
|
tt.want.Result[i].Details = org.Details
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
retryDuration := time.Minute
|
||||||
|
if ctxDeadline, ok := CTX.Deadline(); ok {
|
||||||
|
retryDuration = time.Until(ctxDeadline)
|
||||||
|
}
|
||||||
|
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||||
|
got, listErr := Client.ListOrganizations(tt.args.ctx, tt.args.req)
|
||||||
|
assertErr := assert.NoError
|
||||||
|
if tt.wantErr {
|
||||||
|
assertErr = assert.Error
|
||||||
|
}
|
||||||
|
assertErr(ttt, listErr)
|
||||||
|
if listErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// totalResult is unrelated to the tests here so gets carried over, can vary from the count of results due to permissions
|
||||||
|
tt.want.Details.TotalResult = got.Details.TotalResult
|
||||||
|
// always first check length, otherwise its failed anyway
|
||||||
|
assert.Len(ttt, got.Result, len(tt.want.Result))
|
||||||
|
|
||||||
|
for i := range tt.want.Result {
|
||||||
|
// domain from result, as it is generated though the create
|
||||||
|
tt.want.Result[i].PrimaryDomain = got.Result[i].PrimaryDomain
|
||||||
|
// sequence from result, as it can be with different sequence from create
|
||||||
|
tt.want.Result[i].Details.Sequence = got.Result[i].Details.Sequence
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range tt.want.Result {
|
||||||
|
assert.Contains(ttt, got.Result, tt.want.Result[i])
|
||||||
|
}
|
||||||
|
integration.AssertListDetails(t, tt.want, got)
|
||||||
|
}, retryDuration, time.Millisecond*100, "timeout waiting for expected user result")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func OrganizationIdQuery(resourceowner string) *org.SearchQuery {
|
||||||
|
return &org.SearchQuery{Query: &org.SearchQuery_IdQuery{
|
||||||
|
IdQuery: &org.OrganizationIDQuery{
|
||||||
|
Id: resourceowner,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func OrganizationNameQuery(name string) *org.SearchQuery {
|
||||||
|
return &org.SearchQuery{Query: &org.SearchQuery_NameQuery{
|
||||||
|
NameQuery: &org.OrganizationNameQuery{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func OrganizationNamePrefixQuery(name string) *org.SearchQuery {
|
||||||
|
return &org.SearchQuery{Query: &org.SearchQuery_NameQuery{
|
||||||
|
NameQuery: &org.OrganizationNameQuery{
|
||||||
|
Name: name,
|
||||||
|
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_STARTS_WITH,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func OrganizationDomainQuery(domain string) *org.SearchQuery {
|
||||||
|
return &org.SearchQuery{Query: &org.SearchQuery_DomainQuery{
|
||||||
|
DomainQuery: &org.OrganizationDomainQuery{
|
||||||
|
Domain: domain,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func OrganizationStateQuery(state org.OrganizationState) *org.SearchQuery {
|
||||||
|
return &org.SearchQuery{Query: &org.SearchQuery_StateQuery{
|
||||||
|
StateQuery: &org.OrganizationStateQuery{
|
||||||
|
State: state,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
}
|
@ -39,11 +39,10 @@ func (s *Server) ListUsers(ctx context.Context, req *user.ListUsersRequest) (*us
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
res, err := s.query.SearchUsers(ctx, queries)
|
res, err := s.query.SearchUsers(ctx, queries, s.checkPermission)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
res.RemoveNoPermission(ctx, s.checkPermission)
|
|
||||||
return &user.ListUsersResponse{
|
return &user.ListUsersResponse{
|
||||||
Result: UsersToPb(res.Users, s.assetAPIPrefix(ctx)),
|
Result: UsersToPb(res.Users, s.assetAPIPrefix(ctx)),
|
||||||
Details: object.ToListDetails(res.SearchResponse),
|
Details: object.ToListDetails(res.SearchResponse),
|
||||||
|
@ -13,8 +13,8 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
)
|
)
|
||||||
@ -914,6 +914,10 @@ func TestServer_ListUsers(t *testing.T) {
|
|||||||
assert.Len(ttt, tt.want.Result, len(infos))
|
assert.Len(ttt, tt.want.Result, len(infos))
|
||||||
// always first check length, otherwise its failed anyway
|
// always first check length, otherwise its failed anyway
|
||||||
assert.Len(ttt, got.Result, len(tt.want.Result))
|
assert.Len(ttt, got.Result, len(tt.want.Result))
|
||||||
|
|
||||||
|
// totalResult is unrelated to the tests here so gets carried over, can vary from the count of results due to permissions
|
||||||
|
tt.want.Details.TotalResult = got.Details.TotalResult
|
||||||
|
|
||||||
// fill in userid and username as it is generated
|
// fill in userid and username as it is generated
|
||||||
for i := range infos {
|
for i := range infos {
|
||||||
tt.want.Result[i].UserId = infos[i].UserID
|
tt.want.Result[i].UserId = infos[i].UserID
|
||||||
|
@ -39,11 +39,10 @@ func (s *Server) ListUsers(ctx context.Context, req *user.ListUsersRequest) (*us
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
res, err := s.query.SearchUsers(ctx, queries)
|
res, err := s.query.SearchUsers(ctx, queries, s.checkPermission)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
res.RemoveNoPermission(ctx, s.checkPermission)
|
|
||||||
return &user.ListUsersResponse{
|
return &user.ListUsersResponse{
|
||||||
Result: UsersToPb(res.Users, s.assetAPIPrefix(ctx)),
|
Result: UsersToPb(res.Users, s.assetAPIPrefix(ctx)),
|
||||||
Details: object.ToListDetails(res.SearchResponse),
|
Details: object.ToListDetails(res.SearchResponse),
|
||||||
|
@ -923,6 +923,10 @@ func TestServer_ListUsers(t *testing.T) {
|
|||||||
// always first check length, otherwise its failed anyway
|
// always first check length, otherwise its failed anyway
|
||||||
assert.Len(ttt, got.Result, len(tt.want.Result))
|
assert.Len(ttt, got.Result, len(tt.want.Result))
|
||||||
// fill in userid and username as it is generated
|
// fill in userid and username as it is generated
|
||||||
|
|
||||||
|
// totalResult is unrelated to the tests here so gets carried over, can vary from the count of results due to permissions
|
||||||
|
tt.want.Details.TotalResult = got.Details.TotalResult
|
||||||
|
|
||||||
for i := range infos {
|
for i := range infos {
|
||||||
tt.want.Result[i].UserId = infos[i].UserID
|
tt.want.Result[i].UserId = infos[i].UserID
|
||||||
tt.want.Result[i].Username = infos[i].Username
|
tt.want.Result[i].Username = infos[i].Username
|
||||||
|
@ -171,7 +171,7 @@ func (l *Login) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgName string
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
users, err := l.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}})
|
users, err := l.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ const (
|
|||||||
PermissionUserCredentialWrite = "user.credential.write"
|
PermissionUserCredentialWrite = "user.credential.write"
|
||||||
PermissionSessionWrite = "session.write"
|
PermissionSessionWrite = "session.write"
|
||||||
PermissionSessionDelete = "session.delete"
|
PermissionSessionDelete = "session.delete"
|
||||||
|
PermissionOrgRead = "org.read"
|
||||||
PermissionIDPRead = "iam.idp.read"
|
PermissionIDPRead = "iam.idp.read"
|
||||||
PermissionOrgIDPRead = "org.idp.read"
|
PermissionOrgIDPRead = "org.idp.read"
|
||||||
)
|
)
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
"google.golang.org/protobuf/types/known/durationpb"
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
|
||||||
@ -258,6 +259,39 @@ func (s *Tester) CreateOrganization(ctx context.Context, name, adminEmail string
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Tester) DeactivateOrganization(ctx context.Context, orgID string) *mgmt.DeactivateOrgResponse {
|
||||||
|
resp, err := s.Client.Mgmt.DeactivateOrg(
|
||||||
|
SetOrgID(ctx, orgID),
|
||||||
|
&mgmt.DeactivateOrgRequest{},
|
||||||
|
)
|
||||||
|
logging.OnError(err).Fatal("deactivate org")
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetOrgID(ctx context.Context, orgID string) context.Context {
|
||||||
|
md, ok := metadata.FromOutgoingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return metadata.AppendToOutgoingContext(ctx, "x-zitadel-orgid", orgID)
|
||||||
|
}
|
||||||
|
md.Set("x-zitadel-orgid", orgID)
|
||||||
|
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{
|
||||||
|
Name: name,
|
||||||
|
Admins: []*org.AddOrganizationRequest_Admin{
|
||||||
|
{
|
||||||
|
UserType: &org.AddOrganizationRequest_Admin_UserId{
|
||||||
|
UserId: userID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
logging.OnError(err).Fatal("create org")
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Tester) CreateHumanUserVerified(ctx context.Context, org, email string) *user.AddHumanUserResponse {
|
func (s *Tester) CreateHumanUserVerified(ctx context.Context, org, email string) *user.AddHumanUserResponse {
|
||||||
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{
|
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{
|
||||||
Organization: &object.Organization{
|
Organization: &object.Organization{
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
sq "github.com/Masterminds/squirrel"
|
sq "github.com/Masterminds/squirrel"
|
||||||
@ -82,6 +83,17 @@ type Org struct {
|
|||||||
Domain string
|
Domain string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func orgsCheckPermission(ctx context.Context, orgs *Orgs, permissionCheck domain_pkg.PermissionCheck) {
|
||||||
|
orgs.Orgs = slices.DeleteFunc(orgs.Orgs,
|
||||||
|
func(org *Org) bool {
|
||||||
|
if err := permissionCheck(ctx, domain_pkg.PermissionOrgRead, org.ID, org.ID); err != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
type OrgSearchQueries struct {
|
type OrgSearchQueries struct {
|
||||||
SearchRequest
|
SearchRequest
|
||||||
Queries []SearchQuery
|
Queries []SearchQuery
|
||||||
@ -254,7 +266,18 @@ func (q *Queries) ExistsOrg(ctx context.Context, id, domain string) (verifiedID
|
|||||||
return org.ID, nil
|
return org.ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) SearchOrgs(ctx context.Context, queries *OrgSearchQueries) (orgs *Orgs, err error) {
|
func (q *Queries) SearchOrgs(ctx context.Context, queries *OrgSearchQueries, permissionCheck domain_pkg.PermissionCheck) (*Orgs, error) {
|
||||||
|
orgs, err := q.searchOrgs(ctx, queries)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if permissionCheck != nil {
|
||||||
|
orgsCheckPermission(ctx, orgs, permissionCheck)
|
||||||
|
}
|
||||||
|
return orgs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) searchOrgs(ctx context.Context, queries *OrgSearchQueries) (orgs *Orgs, err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/DATA-DOG/go-sqlmock"
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/database"
|
"github.com/zitadel/zitadel/internal/database"
|
||||||
db_mock "github.com/zitadel/zitadel/internal/database/mock"
|
db_mock "github.com/zitadel/zitadel/internal/database/mock"
|
||||||
@ -441,3 +442,126 @@ func TestQueries_IsOrgUnique(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOrg_RemoveNoPermission(t *testing.T) {
|
||||||
|
type want struct {
|
||||||
|
orgs []*Org
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want want
|
||||||
|
orgs *Orgs
|
||||||
|
permissions []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"permissions for all",
|
||||||
|
want{
|
||||||
|
orgs: []*Org{
|
||||||
|
{ID: "first"}, {ID: "second"}, {ID: "third"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&Orgs{
|
||||||
|
Orgs: []*Org{
|
||||||
|
{ID: "first"}, {ID: "second"}, {ID: "third"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]string{"first", "second", "third"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"permissions for one, first",
|
||||||
|
want{
|
||||||
|
orgs: []*Org{
|
||||||
|
{ID: "first"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&Orgs{
|
||||||
|
Orgs: []*Org{
|
||||||
|
{ID: "first"}, {ID: "second"}, {ID: "third"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]string{"first"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"permissions for one, second",
|
||||||
|
want{
|
||||||
|
orgs: []*Org{
|
||||||
|
{ID: "second"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&Orgs{
|
||||||
|
Orgs: []*Org{
|
||||||
|
{ID: "first"}, {ID: "second"}, {ID: "third"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]string{"second"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"permissions for one, third",
|
||||||
|
want{
|
||||||
|
orgs: []*Org{
|
||||||
|
{ID: "third"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&Orgs{
|
||||||
|
Orgs: []*Org{
|
||||||
|
{ID: "first"}, {ID: "second"}, {ID: "third"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]string{"third"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"permissions for two, first third",
|
||||||
|
want{
|
||||||
|
orgs: []*Org{
|
||||||
|
{ID: "first"}, {ID: "third"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&Orgs{
|
||||||
|
Orgs: []*Org{
|
||||||
|
{ID: "first"}, {ID: "second"}, {ID: "third"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]string{"first", "third"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"permissions for two, second third",
|
||||||
|
want{
|
||||||
|
orgs: []*Org{
|
||||||
|
{ID: "second"}, {ID: "third"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&Orgs{
|
||||||
|
Orgs: []*Org{
|
||||||
|
{ID: "first"}, {ID: "second"}, {ID: "third"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]string{"second", "third"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no permissions",
|
||||||
|
want{
|
||||||
|
orgs: []*Org{},
|
||||||
|
},
|
||||||
|
&Orgs{
|
||||||
|
Orgs: []*Org{
|
||||||
|
{ID: "first"}, {ID: "second"}, {ID: "third"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]string{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
checkPermission := func(ctx context.Context, permission, orgID, resourceID string) (err error) {
|
||||||
|
for _, perm := range tt.permissions {
|
||||||
|
if resourceID == perm {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New("failed")
|
||||||
|
}
|
||||||
|
orgsCheckPermission(context.Background(), tt.orgs, checkPermission)
|
||||||
|
require.Equal(t, tt.want.orgs, tt.orgs.Orgs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"errors"
|
"errors"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -123,27 +124,18 @@ type NotifyUser struct {
|
|||||||
PasswordSet bool
|
PasswordSet bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Users) RemoveNoPermission(ctx context.Context, permissionCheck domain.PermissionCheck) {
|
func usersCheckPermission(ctx context.Context, users *Users, permissionCheck domain.PermissionCheck) {
|
||||||
removableIndexes := make([]int, 0)
|
ctxData := authz.GetCtxData(ctx)
|
||||||
for i := range u.Users {
|
users.Users = slices.DeleteFunc(users.Users,
|
||||||
ctxData := authz.GetCtxData(ctx)
|
func(user *User) bool {
|
||||||
if ctxData.UserID != u.Users[i].ID {
|
if ctxData.UserID != user.ID {
|
||||||
if err := permissionCheck(ctx, domain.PermissionUserRead, u.Users[i].ResourceOwner, u.Users[i].ID); err != nil {
|
if err := permissionCheck(ctx, domain.PermissionUserRead, user.ResourceOwner, user.ID); err != nil {
|
||||||
removableIndexes = append(removableIndexes, i)
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return false
|
||||||
}
|
},
|
||||||
removed := 0
|
)
|
||||||
for _, removeIndex := range removableIndexes {
|
|
||||||
u.Users = removeUser(u.Users, removeIndex-removed)
|
|
||||||
removed++
|
|
||||||
}
|
|
||||||
// reset count as some users could be removed
|
|
||||||
u.SearchResponse.Count = uint64(len(u.Users))
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeUser(slice []*User, s int) []*User {
|
|
||||||
return append(slice[:s], slice[s+1:]...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserSearchQueries struct {
|
type UserSearchQueries struct {
|
||||||
@ -597,7 +589,18 @@ func (q *Queries) GetNotifyUser(ctx context.Context, shouldTriggered bool, queri
|
|||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries) (users *Users, err error) {
|
func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries, permissionCheck domain.PermissionCheck) (*Users, error) {
|
||||||
|
users, err := q.searchUsers(ctx, queries)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if permissionCheck != nil {
|
||||||
|
usersCheckPermission(ctx, users, permissionCheck)
|
||||||
|
}
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries) (users *Users, err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_RemoveNoPermission(t *testing.T) {
|
func TestUser_RemoveNoPermission(t *testing.T) {
|
||||||
type want struct {
|
type want struct {
|
||||||
users []*User
|
users []*User
|
||||||
}
|
}
|
||||||
@ -134,7 +134,7 @@ func Test_RemoveNoPermission(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return errors.New("failed")
|
return errors.New("failed")
|
||||||
}
|
}
|
||||||
tt.users.RemoveNoPermission(context.Background(), checkPermission)
|
usersCheckPermission(context.Background(), tt.users, checkPermission)
|
||||||
require.Equal(t, tt.want.users, tt.users.Users)
|
require.Equal(t, tt.want.users, tt.users.Users)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
43
proto/zitadel/org/v2/org.proto
Normal file
43
proto/zitadel/org/v2/org.proto
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package zitadel.org.v2;
|
||||||
|
|
||||||
|
option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2;org";
|
||||||
|
|
||||||
|
import "google/api/field_behavior.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
|
import "validate/validate.proto";
|
||||||
|
import "zitadel/object/v2/object.proto";
|
||||||
|
|
||||||
|
|
||||||
|
message Organization {
|
||||||
|
// Unique identifier of the organization.
|
||||||
|
string id = 1 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"69629023906488334\""
|
||||||
|
}
|
||||||
|
];
|
||||||
|
zitadel.object.v2.Details details = 2;
|
||||||
|
// Current state of the organization, for example active, inactive and deleted.
|
||||||
|
OrganizationState state = 3;
|
||||||
|
// Name of the organization.
|
||||||
|
string name = 4 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"ZITADEL\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// Primary domain used in the organization.
|
||||||
|
string primary_domain = 5 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"zitadel.cloud\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrganizationState {
|
||||||
|
ORGANIZATION_STATE_UNSPECIFIED = 0;
|
||||||
|
ORGANIZATION_STATE_ACTIVE = 1;
|
||||||
|
ORGANIZATION_STATE_INACTIVE = 2;
|
||||||
|
ORGANIZATION_STATE_REMOVED = 3;
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
|
|
||||||
package zitadel.org.v2;
|
package zitadel.org.v2;
|
||||||
|
|
||||||
import "zitadel/object/v2/object.proto";
|
import "zitadel/object/v2/object.proto";
|
||||||
@ -18,12 +17,14 @@ import "google/protobuf/duration.proto";
|
|||||||
import "google/protobuf/struct.proto";
|
import "google/protobuf/struct.proto";
|
||||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
import "validate/validate.proto";
|
import "validate/validate.proto";
|
||||||
|
import "zitadel/org/v2/org.proto";
|
||||||
|
import "zitadel/org/v2/query.proto";
|
||||||
|
|
||||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2;org";
|
option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2;org";
|
||||||
|
|
||||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
|
||||||
info: {
|
info: {
|
||||||
title: "User Service";
|
title: "Organization Service";
|
||||||
version: "2.0";
|
version: "2.0";
|
||||||
description: "This API is intended to manage organizations in a ZITADEL instance.";
|
description: "This API is intended to manage organizations in a ZITADEL instance.";
|
||||||
contact:{
|
contact:{
|
||||||
@ -111,7 +112,9 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
|
|||||||
|
|
||||||
service OrganizationService {
|
service OrganizationService {
|
||||||
|
|
||||||
// Create a new organization and grant the user(s) permission to manage it
|
// Create an Organization
|
||||||
|
//
|
||||||
|
// Create a new organization with an administrative user. If no specific roles are sent for the users, they will be granted the role ORG_OWNER.
|
||||||
rpc AddOrganization(AddOrganizationRequest) returns (AddOrganizationResponse) {
|
rpc AddOrganization(AddOrganizationRequest) returns (AddOrganizationResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
post: "/v2/organizations"
|
post: "/v2/organizations"
|
||||||
@ -128,8 +131,6 @@ service OrganizationService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
summary: "Create an Organization";
|
|
||||||
description: "Create a new organization with an administrative user. If no specific roles are sent for the users, they will be granted the role ORG_OWNER."
|
|
||||||
responses: {
|
responses: {
|
||||||
key: "200"
|
key: "200"
|
||||||
value: {
|
value: {
|
||||||
@ -138,6 +139,42 @@ service OrganizationService {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Search Organizations
|
||||||
|
//
|
||||||
|
// Search for Organizations. By default, we will return all organization of the instance. Make sure to include a limit and sorting for pagination..
|
||||||
|
rpc ListOrganizations(ListOrganizationsRequest) returns (ListOrganizationsResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/v2/organizations/_search";
|
||||||
|
body: "*";
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||||
|
auth_option: {
|
||||||
|
permission: "authenticated"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
responses: {
|
||||||
|
key: "200"
|
||||||
|
value: {
|
||||||
|
description: "A list of all organizations matching the query";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
key: "400";
|
||||||
|
value: {
|
||||||
|
description: "invalid list query";
|
||||||
|
schema: {
|
||||||
|
json_schema: {
|
||||||
|
ref: "#/definitions/rpcStatus";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message AddOrganizationRequest{
|
message AddOrganizationRequest{
|
||||||
@ -172,3 +209,18 @@ message AddOrganizationResponse{
|
|||||||
string organization_id = 2;
|
string organization_id = 2;
|
||||||
repeated CreatedAdmin created_admins = 3;
|
repeated CreatedAdmin created_admins = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ListOrganizationsRequest {
|
||||||
|
//list limitations and ordering
|
||||||
|
zitadel.object.v2.ListQuery query = 1;
|
||||||
|
// the field the result is sorted
|
||||||
|
zitadel.org.v2.OrganizationFieldName sorting_column = 2;
|
||||||
|
//criteria the client is looking for
|
||||||
|
repeated zitadel.org.v2.SearchQuery queries = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListOrganizationsResponse {
|
||||||
|
zitadel.object.v2.ListDetails details = 1;
|
||||||
|
zitadel.org.v2.OrganizationFieldName sorting_column = 2;
|
||||||
|
repeated zitadel.org.v2.Organization result = 3;
|
||||||
|
}
|
||||||
|
83
proto/zitadel/org/v2/query.proto
Normal file
83
proto/zitadel/org/v2/query.proto
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package zitadel.org.v2;
|
||||||
|
|
||||||
|
option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2;org";
|
||||||
|
|
||||||
|
import "google/api/annotations.proto";
|
||||||
|
import "google/api/field_behavior.proto";
|
||||||
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
|
import "validate/validate.proto";
|
||||||
|
import "zitadel/org/v2/org.proto";
|
||||||
|
import "zitadel/object/v2/object.proto";
|
||||||
|
|
||||||
|
|
||||||
|
message SearchQuery {
|
||||||
|
oneof query {
|
||||||
|
option (validate.required) = true;
|
||||||
|
|
||||||
|
OrganizationNameQuery name_query = 1;
|
||||||
|
OrganizationDomainQuery domain_query = 2;
|
||||||
|
OrganizationStateQuery state_query = 3;
|
||||||
|
OrganizationIDQuery id_query = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message OrganizationNameQuery {
|
||||||
|
// Name of the organization.
|
||||||
|
string name = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"gigi-giraffe\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// Defines which text equality method is used.
|
||||||
|
zitadel.object.v2.TextQueryMethod method = 2 [
|
||||||
|
(validate.rules).enum.defined_only = true
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
message OrganizationDomainQuery {
|
||||||
|
// Domain used in organization, not necessary primary domain.
|
||||||
|
string domain = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"citadel.cloud\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// Defines which text equality method is used.
|
||||||
|
zitadel.object.v2.TextQueryMethod method = 2 [
|
||||||
|
(validate.rules).enum.defined_only = true
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
message OrganizationStateQuery {
|
||||||
|
// Current state of the organization.
|
||||||
|
OrganizationState state = 1 [
|
||||||
|
(validate.rules).enum.defined_only = true
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
message OrganizationIDQuery {
|
||||||
|
// Unique identifier of the organization.
|
||||||
|
string id = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"69629023906488334\""
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrganizationFieldName {
|
||||||
|
ORGANIZATION_FIELD_NAME_UNSPECIFIED = 0;
|
||||||
|
ORGANIZATION_FIELD_NAME_NAME = 1;
|
||||||
|
}
|
@ -179,9 +179,6 @@ service UserService {
|
|||||||
auth_option: {
|
auth_option: {
|
||||||
permission: "authenticated"
|
permission: "authenticated"
|
||||||
}
|
}
|
||||||
http_response: {
|
|
||||||
success_code: 200
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user