From 7c96dcd9a2a5e174f9bd38d361f3e30447d94747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabienne=20B=C3=BChler?= Date: Fri, 14 Feb 2025 11:48:16 +0100 Subject: [PATCH 01/32] docs: update readme with features and new login gif (#9357) # Which Problems Are Solved SCIM 2.0 Server was not listed in the readme of Zitadel New Login was not listed # How the Problems Are Solved Added scim 2.0 as a feature to the list Added new login, including a gif to showcase --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 592952cdc2..5d4aecf441 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ We provide you with a wide range of out-of-the-box features to accelerate your p :white_check_mark: LDAP :white_check_mark: Passkeys / FIDO2 :white_check_mark: OTP +:white_check_mark: SCIM 2.0 Server and an unlimited audit trail is there for you, ready to use. With ZITADEL, you are assured of a robust and customizable turnkey solution for all your authentication and authorization needs. @@ -124,6 +125,7 @@ Authentication - [Custom sessions](https://zitadel.com/docs/guides/integrate/login-ui/username-password) if you need to go beyond OIDC or SAML - [Machine-to-machine](https://zitadel.com/docs/guides/integrate/service-users/authenticate-service-users) with JWT profile, Personal Access Tokens (PAT), and Client Credentials - [Token exchange and impersonation](https://zitadel.com/docs/guides/integrate/token-exchange) +- [Beta: Hosted Login V2](https://zitadel.com/docs/guides/integrate/login/hosted-login#hosted-login-version-2-beta) our new login version 2.0 Multi-Tenancy @@ -137,10 +139,11 @@ Integration - [GRPC and REST APIs](https://zitadel.com/docs/apis/introduction) for every functionality and resource - [Actions](https://zitadel.com/docs/apis/actions/introduction) to call any API, send webhooks, adjust workflows, or customize tokens - [Role Based Access Control (RBAC)](https://zitadel.com/docs/guides/integrate/retrieve-user-roles) +- [SCIM 2.0 Server](https://zitadel.com/docs/apis/scim2) - [Examples and SDKs](https://zitadel.com/docs/sdk-examples/introduction) - [Audit Log and SOC/SIEM](https://zitadel.com/docs/guides/integrate/external-audit-log) - [User registration and onboarding](https://zitadel.com/docs/guides/integrate/onboarding) -- [Hosted and custom login user interface](https://zitadel.com/docs/guides/integrate/login-ui) +- [Hosted and custom login user interface](https://zitadel.com/docs/guides/integrate/login/login-users) Self-Service - [Self-registration](https://zitadel.com/docs/concepts/features/selfservice#registration) including verification @@ -187,6 +190,11 @@ Use [Console](https://zitadel.com/docs/guides/manage/console/overview) or our [A [![Console Showcase](https://user-images.githubusercontent.com/1366906/223663344-67038d5f-4415-4285-ab20-9a4d397e2138.gif)](http://www.youtube.com/watch?v=RPpHktAcCtk "Console Showcase") +### Login V2 + +Check out our new Login V2 version in our [typescript repository](https://github.com/zitadel/typescript) or in our [documentation](https://zitadel.com/docs/guides/integrate/login/hosted-login#hosted-login-version-2-beta) +[![New Login Showcase](https://github.com/user-attachments/assets/cb5c5212-128b-4dc9-b11d-cabfd3f73e26)] + ## Security You can find our security policy [here](./SECURITY.md). From 0cb03808269339bbc65d9fdb2e3d7dfa54cf5305 Mon Sep 17 00:00:00 2001 From: Iraq <66622793+kkrime@users.noreply.github.com> Date: Mon, 17 Feb 2025 09:55:28 +0000 Subject: [PATCH 02/32] feat: updating eventstore.permitted_orgs sql function (#9309) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Which Problems Are Solved Performance issue for GRPC call `zitadel.user.v2.UserService.ListUsers` due to lack of org filtering on `ListUsers` # Additional Context Replace this example with links to related issues, discussions, discord threads, or other sources with more context. Use the Closing #issue syntax for issues that are resolved with this PR. - Closes https://github.com/zitadel/zitadel/issues/9191 --------- Co-authored-by: Iraq Jaber Co-authored-by: Tim Möhlmann --- cmd/setup/49.go | 39 ++++++++++++++ cmd/setup/49/01-permitted_orgs_function.sql | 56 +++++++++++++++++++++ cmd/setup/config.go | 1 + cmd/setup/setup.go | 2 + internal/api/grpc/admin/export.go | 2 +- internal/api/grpc/admin/org.go | 2 +- internal/api/grpc/management/org.go | 2 +- internal/api/grpc/management/user.go | 5 +- internal/api/grpc/user/v2/query.go | 25 +++++---- internal/api/grpc/user/v2beta/query.go | 25 +++++---- internal/api/scim/resources/user.go | 2 +- internal/api/ui/login/login.go | 2 +- internal/query/permission.go | 8 +-- internal/query/user.go | 8 +-- 14 files changed, 143 insertions(+), 36 deletions(-) create mode 100644 cmd/setup/49.go create mode 100644 cmd/setup/49/01-permitted_orgs_function.sql diff --git a/cmd/setup/49.go b/cmd/setup/49.go new file mode 100644 index 0000000000..28bf797110 --- /dev/null +++ b/cmd/setup/49.go @@ -0,0 +1,39 @@ +package setup + +import ( + "context" + "embed" + "fmt" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +type InitPermittedOrgsFunction struct { + eventstoreClient *database.DB +} + +var ( + //go:embed 49/*.sql + permittedOrgsFunction embed.FS +) + +func (mig *InitPermittedOrgsFunction) Execute(ctx context.Context, _ eventstore.Event) error { + statements, err := readStatements(permittedOrgsFunction, "49", "") + if err != nil { + return err + } + for _, stmt := range statements { + logging.WithFields("file", stmt.file, "migration", mig.String()).Info("execute statement") + if _, err := mig.eventstoreClient.ExecContext(ctx, stmt.query); err != nil { + return fmt.Errorf("%s %s: %w", mig.String(), stmt.file, err) + } + } + return nil +} + +func (*InitPermittedOrgsFunction) String() string { + return "49_init_permitted_orgs_function" +} diff --git a/cmd/setup/49/01-permitted_orgs_function.sql b/cmd/setup/49/01-permitted_orgs_function.sql new file mode 100644 index 0000000000..9f291c016b --- /dev/null +++ b/cmd/setup/49/01-permitted_orgs_function.sql @@ -0,0 +1,56 @@ +DROP FUNCTION IF EXISTS eventstore.permitted_orgs; + +CREATE OR REPLACE FUNCTION eventstore.permitted_orgs( + instanceId TEXT + , userId TEXT + , perm TEXT + , filter_orgs TEXT + + , org_ids OUT TEXT[] +) + LANGUAGE 'plpgsql' + STABLE +AS $$ +DECLARE + matched_roles TEXT[]; -- roles containing permission +BEGIN + SELECT array_agg(rp.role) INTO matched_roles + FROM eventstore.role_permissions rp + WHERE rp.instance_id = instanceId + AND rp.permission = perm; + + -- First try if the permission was granted thru an instance-level role + DECLARE + has_instance_permission bool; + BEGIN + SELECT true INTO has_instance_permission + FROM eventstore.instance_members im + WHERE im.role = ANY(matched_roles) + AND im.instance_id = instanceId + AND im.user_id = userId + LIMIT 1; + + IF has_instance_permission THEN + -- Return all organizations or only those in filter_orgs + SELECT array_agg(o.org_id) INTO org_ids + FROM eventstore.instance_orgs o + WHERE o.instance_id = instanceId + AND CASE WHEN filter_orgs != '' + THEN o.org_id IN (filter_orgs) + ELSE TRUE END; + RETURN; + END IF; + END; + + -- Return the organizations where permission were granted thru org-level roles + SELECT array_agg(org_id) INTO org_ids + FROM ( + SELECT DISTINCT om.org_id + FROM eventstore.org_members om + WHERE om.role = ANY(matched_roles) + AND om.instance_id = instanceID + AND om.user_id = userId + ); + RETURN; +END; +$$; diff --git a/cmd/setup/config.go b/cmd/setup/config.go index d782a32dd6..0153f7227f 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -137,6 +137,7 @@ type Steps struct { s46InitPermissionFunctions *InitPermissionFunctions s47FillMembershipFields *FillMembershipFields s48Apps7SAMLConfigsLoginVersion *Apps7SAMLConfigsLoginVersion + s49InitPermittedOrgsFunction *InitPermittedOrgsFunction } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index bfa289ab36..74b16355f3 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -174,6 +174,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s46InitPermissionFunctions = &InitPermissionFunctions{eventstoreClient: dbClient} steps.s47FillMembershipFields = &FillMembershipFields{eventstore: eventstoreClient} steps.s48Apps7SAMLConfigsLoginVersion = &Apps7SAMLConfigsLoginVersion{dbClient: dbClient} + steps.s49InitPermittedOrgsFunction = &InitPermittedOrgsFunction{eventstoreClient: dbClient} err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) logging.OnError(err).Fatal("unable to start projections") @@ -238,6 +239,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s45CorrectProjectOwners, steps.s46InitPermissionFunctions, steps.s47FillMembershipFields, + steps.s49InitPermittedOrgsFunction, } { mustExecuteMigration(ctx, eventstoreClient, step, "migration failed") } diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go index 68b6053c2c..da364909cb 100644 --- a/internal/api/grpc/admin/export.go +++ b/internal/api/grpc/admin/export.go @@ -554,7 +554,7 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w if err != nil { return nil, nil, nil, nil, err } - users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{orgSearch}}, nil) + users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{orgSearch}}, org, nil) if err != nil { return nil, nil, nil, nil, err } diff --git a/internal/api/grpc/admin/org.go b/internal/api/grpc/admin/org.go index 934de1b570..f788bb5f5a 100644 --- a/internal/api/grpc/admin/org.go +++ b/internal/api/grpc/admin/org.go @@ -108,7 +108,7 @@ func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain str if err != nil { return nil, err } - users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}, nil) + users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}, "", nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/org.go b/internal/api/grpc/management/org.go index d25d46d852..abc179a763 100644 --- a/internal/api/grpc/management/org.go +++ b/internal/api/grpc/management/org.go @@ -330,7 +330,7 @@ func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain, or } queries = append(queries, owner) } - users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: queries}, nil) + users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: queries}, orgID, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index dac651af81..17bca58993 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -64,11 +64,12 @@ func (s *Server) ListUsers(ctx context.Context, req *mgmt_pb.ListUsersRequest) ( return nil, err } - err = queries.AppendMyResourceOwnerQuery(authz.GetCtxData(ctx).OrgID) + orgID := authz.GetCtxData(ctx).OrgID + err = queries.AppendMyResourceOwnerQuery(orgID) if err != nil { return nil, err } - res, err := s.query.SearchUsers(ctx, queries, nil) + res, err := s.query.SearchUsers(ctx, queries, orgID, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/user/v2/query.go b/internal/api/grpc/user/v2/query.go index aeb17d5dcf..aec5367ded 100644 --- a/internal/api/grpc/user/v2/query.go +++ b/internal/api/grpc/user/v2/query.go @@ -29,11 +29,11 @@ func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest) } func (s *Server) ListUsers(ctx context.Context, req *user.ListUsersRequest) (*user.ListUsersResponse, error) { - queries, err := listUsersRequestToModel(req) + queries, filterOrgId, err := listUsersRequestToModel(req) if err != nil { return nil, err } - res, err := s.query.SearchUsers(ctx, queries, s.checkPermission) + res, err := s.query.SearchUsers(ctx, queries, filterOrgId, s.checkPermission) if err != nil { return nil, err } @@ -169,11 +169,11 @@ func accessTokenTypeToPb(accessTokenType domain.OIDCTokenType) user.AccessTokenT } } -func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueries, error) { +func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueries, string, error) { offset, limit, asc := object.ListQueryToQuery(req.Query) - queries, err := userQueriesToQuery(req.Queries, 0 /*start from level 0*/) + queries, filterOrgId, err := userQueriesToQuery(req.Queries, 0 /*start from level 0*/) if err != nil { - return nil, err + return nil, "", err } return &query.UserSearchQueries{ SearchRequest: query.SearchRequest{ @@ -183,7 +183,7 @@ func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueri SortingColumn: userFieldNameToSortingColumn(req.SortingColumn), }, Queries: queries, - }, nil + }, filterOrgId, nil } func userFieldNameToSortingColumn(field user.UserFieldName) query.Column { @@ -213,15 +213,18 @@ func userFieldNameToSortingColumn(field user.UserFieldName) query.Column { } } -func userQueriesToQuery(queries []*user.SearchQuery, level uint8) (_ []query.SearchQuery, err error) { +func userQueriesToQuery(queries []*user.SearchQuery, level uint8) (_ []query.SearchQuery, filterOrgId string, err error) { q := make([]query.SearchQuery, len(queries)) for i, query := range queries { + if orgFilter := query.GetOrganizationIdQuery(); orgFilter != nil { + filterOrgId = orgFilter.OrganizationId + } q[i], err = userQueryToQuery(query, level) if err != nil { - return nil, err + return nil, filterOrgId, err } } - return q, nil + return q, filterOrgId, nil } func userQueryToQuery(query *user.SearchQuery, level uint8) (query.SearchQuery, error) { @@ -315,14 +318,14 @@ func inUserIdsQueryToQuery(q *user.InUserIDQuery) (query.SearchQuery, error) { return query.NewUserInUserIdsSearchQuery(q.UserIds) } func orQueryToQuery(q *user.OrQuery, level uint8) (query.SearchQuery, error) { - mappedQueries, err := userQueriesToQuery(q.Queries, level+1) + mappedQueries, _, err := userQueriesToQuery(q.Queries, level+1) if err != nil { return nil, err } return query.NewUserOrSearchQuery(mappedQueries) } func andQueryToQuery(q *user.AndQuery, level uint8) (query.SearchQuery, error) { - mappedQueries, err := userQueriesToQuery(q.Queries, level+1) + mappedQueries, _, err := userQueriesToQuery(q.Queries, level+1) if err != nil { return nil, err } diff --git a/internal/api/grpc/user/v2beta/query.go b/internal/api/grpc/user/v2beta/query.go index e3602abc33..7baa53e73e 100644 --- a/internal/api/grpc/user/v2beta/query.go +++ b/internal/api/grpc/user/v2beta/query.go @@ -29,11 +29,11 @@ func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest) } func (s *Server) ListUsers(ctx context.Context, req *user.ListUsersRequest) (*user.ListUsersResponse, error) { - queries, err := listUsersRequestToModel(req) + queries, filterOrgIds, err := listUsersRequestToModel(req) if err != nil { return nil, err } - res, err := s.query.SearchUsers(ctx, queries, s.checkPermission) + res, err := s.query.SearchUsers(ctx, queries, filterOrgIds, s.checkPermission) if err != nil { return nil, err } @@ -165,11 +165,11 @@ func accessTokenTypeToPb(accessTokenType domain.OIDCTokenType) user.AccessTokenT } } -func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueries, error) { +func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueries, string, error) { offset, limit, asc := object.ListQueryToQuery(req.Query) - queries, err := userQueriesToQuery(req.Queries, 0 /*start from level 0*/) + queries, filterOrgId, err := userQueriesToQuery(req.Queries, 0 /*start from level 0*/) if err != nil { - return nil, err + return nil, "", err } return &query.UserSearchQueries{ SearchRequest: query.SearchRequest{ @@ -179,7 +179,7 @@ func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueri SortingColumn: userFieldNameToSortingColumn(req.SortingColumn), }, Queries: queries, - }, nil + }, filterOrgId, nil } func userFieldNameToSortingColumn(field user.UserFieldName) query.Column { @@ -209,15 +209,18 @@ func userFieldNameToSortingColumn(field user.UserFieldName) query.Column { } } -func userQueriesToQuery(queries []*user.SearchQuery, level uint8) (_ []query.SearchQuery, err error) { +func userQueriesToQuery(queries []*user.SearchQuery, level uint8) (_ []query.SearchQuery, filterOrgId string, err error) { q := make([]query.SearchQuery, len(queries)) for i, query := range queries { + if orgFilter := query.GetOrganizationIdQuery(); orgFilter != nil { + filterOrgId = orgFilter.OrganizationId + } q[i], err = userQueryToQuery(query, level) if err != nil { - return nil, err + return nil, filterOrgId, err } } - return q, nil + return q, filterOrgId, nil } func userQueryToQuery(query *user.SearchQuery, level uint8) (query.SearchQuery, error) { @@ -311,14 +314,14 @@ func inUserIdsQueryToQuery(q *user.InUserIDQuery) (query.SearchQuery, error) { return query.NewUserInUserIdsSearchQuery(q.UserIds) } func orQueryToQuery(q *user.OrQuery, level uint8) (query.SearchQuery, error) { - mappedQueries, err := userQueriesToQuery(q.Queries, level+1) + mappedQueries, _, err := userQueriesToQuery(q.Queries, level+1) if err != nil { return nil, err } return query.NewUserOrSearchQuery(mappedQueries) } func andQueryToQuery(q *user.AndQuery, level uint8) (query.SearchQuery, error) { - mappedQueries, err := userQueriesToQuery(q.Queries, level+1) + mappedQueries, _, err := userQueriesToQuery(q.Queries, level+1) if err != nil { return nil, err } diff --git a/internal/api/scim/resources/user.go b/internal/api/scim/resources/user.go index bc8d864994..ffd39aa23f 100644 --- a/internal/api/scim/resources/user.go +++ b/internal/api/scim/resources/user.go @@ -240,7 +240,7 @@ func (h *UsersHandler) List(ctx context.Context, request *ListRequest) (*ListRes return NewListResponse(count, q.SearchRequest, make([]*ScimUser, 0)), nil } - users, err := h.query.SearchUsers(ctx, q, nil) + users, err := h.query.SearchUsers(ctx, q, authz.GetCtxData(ctx).OrgID, nil) if err != nil { return nil, err } diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go index 444c5aaa85..4b028a347f 100644 --- a/internal/api/ui/login/login.go +++ b/internal/api/ui/login/login.go @@ -182,7 +182,7 @@ func (l *Login) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgName string if err != nil { return nil, err } - users, err := l.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}, nil) + users, err := l.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}, "", nil) if err != nil { return nil, err } diff --git a/internal/query/permission.go b/internal/query/permission.go index 96d7db6c6a..591493375e 100644 --- a/internal/query/permission.go +++ b/internal/query/permission.go @@ -11,8 +11,8 @@ import ( ) const ( - // eventstore.permitted_orgs(instanceid text, userid text, perm text) - wherePermittedOrgsClause = "%s = ANY(eventstore.permitted_orgs(?, ?, ?))" + // eventstore.permitted_orgs(instanceid text, userid text, perm text, filter_orgs text) + wherePermittedOrgsClause = "%s = ANY(eventstore.permitted_orgs(?, ?, ?, ?))" ) // wherePermittedOrgs sets a `WHERE` clause to the query that filters the orgs @@ -23,13 +23,15 @@ const ( // and is typically the `resource_owner` column in ZITADEL. // We use full identifiers in the query builder so this function should be // called with something like `UserResourceOwnerCol.identifier()` for example. -func wherePermittedOrgs(ctx context.Context, query sq.SelectBuilder, orgIDColumn, permission string) sq.SelectBuilder { +func wherePermittedOrgs(ctx context.Context, query sq.SelectBuilder, filterOrgIds, orgIDColumn, permission string) sq.SelectBuilder { userID := authz.GetCtxData(ctx).UserID logging.WithFields("permission_check_v2_flag", authz.GetFeatures(ctx).PermissionCheckV2, "org_id_column", orgIDColumn, "permission", permission, "user_id", userID).Debug("permitted orgs check used") + return query.Where( fmt.Sprintf(wherePermittedOrgsClause, orgIDColumn), authz.GetInstance(ctx).InstanceID(), userID, permission, + filterOrgIds, ) } diff --git a/internal/query/user.go b/internal/query/user.go index bb76e51f66..0b00b45e03 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -635,8 +635,8 @@ func (q *Queries) CountUsers(ctx context.Context, queries *UserSearchQueries) (c return count, err } -func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries, permissionCheck domain.PermissionCheck) (*Users, error) { - users, err := q.searchUsers(ctx, queries, permissionCheck != nil && authz.GetFeatures(ctx).PermissionCheckV2) +func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries, filterOrgIds string, permissionCheck domain.PermissionCheck) (*Users, error) { + users, err := q.searchUsers(ctx, queries, filterOrgIds, permissionCheck != nil && authz.GetFeatures(ctx).PermissionCheckV2) if err != nil { return nil, err } @@ -646,7 +646,7 @@ func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries, p return users, nil } -func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, permissionCheckV2 bool) (users *Users, err error) { +func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, filterOrgIds string, permissionCheckV2 bool) (users *Users, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -655,7 +655,7 @@ func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, p UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), }) if permissionCheckV2 { - query = wherePermittedOrgs(ctx, query, UserResourceOwnerCol.identifier(), domain.PermissionUserRead) + query = wherePermittedOrgs(ctx, query, filterOrgIds, UserResourceOwnerCol.identifier(), domain.PermissionUserRead) } stmt, args, err := query.ToSql() From ad225836d5ace60927ff9429e276a86480af1357 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Mon, 17 Feb 2025 17:06:55 +0100 Subject: [PATCH 03/32] chore: deprecated skip-dirs move to exclude-dirs (#9370) Moved the deprecated skip-dirs option to the exclude-dirs --- .golangci.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index f480eb8c10..1cae359605 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -4,12 +4,7 @@ issues: max-issues-per-linter: 0 # Set to 0 to disable. max-same-issues: 0 - -run: - concurrency: 4 - timeout: 10m - go: '1.22' - skip-dirs: + exclude-dirs: - .artifacts - .backups - .codecov @@ -25,6 +20,11 @@ run: - openapi - proto - tools + +run: + concurrency: 4 + timeout: 10m + go: '1.22' linters: enable: # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false] From 3042bbb9932a9c9ce76a1c5b6039fbf3c334260a Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 17 Feb 2025 19:25:46 +0100 Subject: [PATCH 04/32] feat: Use V2 API's in Console (#9312) # Which Problems Are Solved Solves #8976 # Additional Changes I have done some intensive refactorings and we are using the new @zitadel/client package for GRPC access. # Additional Context - Closes #8976 --------- Co-authored-by: Max Peintner --- console/angular.json | 2 +- console/package.json | 4 + console/src/app/app.component.html | 2 +- console/src/app/app.component.ts | 26 +- .../features/features.component.html | 28 + .../components/features/features.component.ts | 15 +- .../directives/has-role/has-role.directive.ts | 15 +- .../accounts-card/accounts-card.component.ts | 5 +- .../app/modules/footer/footer.component.html | 4 +- .../app/modules/header/header.component.html | 6 +- .../modules/info-row/info-row.component.html | 12 +- .../modules/info-row/info-row.component.ts | 97 ++- .../metadata-dialog.component.ts | 85 +- .../metadata/metadata/metadata.component.html | 9 +- .../metadata/metadata/metadata.component.ts | 41 +- .../modules/paginator/paginator.component.ts | 3 +- .../refresh-table/refresh-table.component.ts | 3 +- .../app/modules/sidenav/sidenav.component.ts | 20 +- .../pages/signedout/signedout.component.ts | 23 +- .../user-create/user-create.component.html | 49 +- .../user-create/user-create.component.scss | 1 + .../user-create/user-create.component.ts | 458 +++++++---- .../auth-user-detail.component.html | 329 ++++---- .../auth-user-detail.component.ts | 722 ++++++++-------- .../edit-dialog/edit-dialog.component.html | 2 +- .../edit-dialog/edit-dialog.component.ts | 22 +- .../resend-email-dialog.component.html | 2 +- .../resend-email-dialog.component.ts | 14 +- .../contact/contact.component.html | 10 +- .../user-detail/contact/contact.component.ts | 22 +- .../detail-form-machine.component.html | 2 +- .../detail-form-machine.component.ts | 94 ++- .../detail-form/detail-form.component.html | 22 +- .../detail-form/detail-form.component.ts | 172 ++-- .../external-idps/external-idps.component.ts | 98 ++- .../password/password.component.html | 30 +- .../password/password.component.ts | 310 ++++--- .../passwordless/passwordless.component.html | 8 +- .../passwordless/passwordless.component.ts | 58 +- .../user-detail/user-detail.component.html | 531 ++++++------ .../user-detail/user-detail.component.ts | 768 ++++++++++-------- .../user-mfa/user-mfa.component.html | 38 +- .../user-mfa/user-mfa.component.ts | 206 ++--- .../user-table/user-table.component.html | 56 +- .../user-table/user-table.component.ts | 152 ++-- .../timestamp-to-date.pipe.ts | 10 +- console/src/app/services/feature.service.ts | 14 +- console/src/app/services/grpc-auth.service.ts | 245 +++--- console/src/app/services/grpc.service.ts | 162 ++-- .../services/interceptors/auth.interceptor.ts | 120 +-- .../exhausted.grpc.interceptor.ts | 18 +- .../services/interceptors/i18n.interceptor.ts | 10 +- .../services/interceptors/org.interceptor.ts | 26 +- console/src/app/services/new-auth.service.ts | 30 + console/src/app/services/new-mgmt.service.ts | 92 +++ console/src/app/services/user.service.ts | 302 +++++++ console/src/app/utils/formatPhone.ts | 2 +- console/src/app/utils/pairwiseStartWith.ts | 11 + console/src/assets/i18n/bg.json | 15 +- console/src/assets/i18n/cs.json | 15 +- console/src/assets/i18n/de.json | 15 +- console/src/assets/i18n/en.json | 15 +- console/src/assets/i18n/es.json | 15 +- console/src/assets/i18n/fr.json | 15 +- console/src/assets/i18n/hu.json | 15 +- console/src/assets/i18n/id.json | 15 +- console/src/assets/i18n/it.json | 15 +- console/src/assets/i18n/ja.json | 15 +- console/src/assets/i18n/ko.json | 15 +- console/src/assets/i18n/mk.json | 15 +- console/src/assets/i18n/nl.json | 15 +- console/src/assets/i18n/pl.json | 15 +- console/src/assets/i18n/pt.json | 15 +- console/src/assets/i18n/ru.json | 15 +- console/src/assets/i18n/sv.json | 15 +- console/src/assets/i18n/zh.json | 15 +- console/yarn.lock | 44 + e2e/cypress/e2e/machines/machines.cy.ts | 4 +- internal/api/grpc/feature/v2/converter.go | 2 + .../api/grpc/feature/v2/converter_test.go | 10 + internal/command/instance_features.go | 3 +- internal/command/instance_features_model.go | 5 + internal/feature/feature.go | 2 + internal/feature/key_enumer.go | 12 +- internal/query/instance_features.go | 1 + internal/query/instance_features_model.go | 3 + .../query/projection/instance_features.go | 4 + .../feature/feature_v2/eventstore.go | 1 + .../repository/feature/feature_v2/feature.go | 1 + proto/zitadel/feature/v2/instance.proto | 14 + 90 files changed, 3679 insertions(+), 2315 deletions(-) create mode 100644 console/src/app/services/new-auth.service.ts create mode 100644 console/src/app/services/new-mgmt.service.ts create mode 100644 console/src/app/services/user.service.ts create mode 100644 console/src/app/utils/pairwiseStartWith.ts diff --git a/console/angular.json b/console/angular.json index 278498ccd7..5564b2c428 100644 --- a/console/angular.json +++ b/console/angular.json @@ -63,7 +63,7 @@ { "type": "initial", "maximumWarning": "8mb", - "maximumError": "9mb" + "maximumError": "10mb" }, { "type": "anyComponentStyle", diff --git a/console/package.json b/console/package.json index fcf3a4bbf8..2c1d38da1b 100644 --- a/console/package.json +++ b/console/package.json @@ -24,6 +24,8 @@ "@angular/platform-browser-dynamic": "^16.2.5", "@angular/router": "^16.2.5", "@angular/service-worker": "^16.2.5", + "@connectrpc/connect": "^2.0.0", + "@connectrpc/connect-web": "^2.0.0", "@ctrl/ngx-codemirror": "^6.1.0", "@fortawesome/angular-fontawesome": "^0.13.0", "@fortawesome/fontawesome-svg-core": "^6.4.2", @@ -31,6 +33,8 @@ "@grpc/grpc-js": "^1.11.2", "@netlify/framework-info": "^9.8.13", "@ngx-translate/core": "^15.0.0", + "@zitadel/client": "^1.0.6", + "@zitadel/proto": "^1.0.3", "angular-oauth2-oidc": "^15.0.1", "angularx-qrcode": "^16.0.0", "buffer": "^6.0.3", diff --git a/console/src/app/app.component.html b/console/src/app/app.component.html index 5b31b33dc4..18c5c72501 100644 --- a/console/src/app/app.component.html +++ b/console/src/app/app.component.html @@ -1,5 +1,5 @@
- + { // We use navigateByUrl as our urls may have queryParams - this.router.navigateByUrl(currentUrl); + this.router.navigateByUrl(currentUrl).then(); }); } @@ -283,18 +283,16 @@ export class AppComponent implements OnDestroy { this.translate.addLangs(supportedLanguages); this.translate.setDefaultLang(fallbackLanguage); - this.authService.userSubject.pipe(takeUntil(this.destroy$)).subscribe((userprofile) => { - if (userprofile) { - const cropped = navigator.language.split('-')[0] ?? fallbackLanguage; - const fallbackLang = cropped.match(supportedLanguagesRegexp) ? cropped : fallbackLanguage; + this.authService.user.pipe(filter(Boolean), takeUntil(this.destroy$)).subscribe((userprofile) => { + const cropped = navigator.language.split('-')[0] ?? fallbackLanguage; + const fallbackLang = cropped.match(supportedLanguagesRegexp) ? cropped : fallbackLanguage; - const lang = userprofile?.human?.profile?.preferredLanguage.match(supportedLanguagesRegexp) - ? userprofile.human.profile?.preferredLanguage - : fallbackLang; - this.translate.use(lang); - this.language = lang; - this.document.documentElement.lang = lang; - } + const lang = userprofile?.human?.profile?.preferredLanguage.match(supportedLanguagesRegexp) + ? userprofile.human.profile?.preferredLanguage + : fallbackLang; + this.translate.use(lang); + this.language = lang; + this.document.documentElement.lang = lang; }); } @@ -308,7 +306,7 @@ export class AppComponent implements OnDestroy { } private setFavicon(theme: string): void { - this.authService.labelpolicy.pipe(takeUntil(this.destroy$)).subscribe((lP) => { + this.authService.labelpolicy$.pipe(startWith(undefined), takeUntil(this.destroy$)).subscribe((lP) => { if (theme === 'dark-theme' && lP?.iconUrlDark) { // Check if asset url is stable, maybe it was deleted but still wasn't applied fetch(lP.iconUrlDark).then((response) => { diff --git a/console/src/app/components/features/features.component.html b/console/src/app/components/features/features.component.html index e663569210..fdd397084a 100644 --- a/console/src/app/components/features/features.component.html +++ b/console/src/app/components/features/features.component.html @@ -403,6 +403,34 @@ 'SETTING.FEATURES.OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION' | translate }}
+ +
+ {{ 'SETTING.FEATURES.CONSOLEUSEV2USERAPI' | translate }} +
+ + +
+ {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} +
+
+ +
+ {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} +
+
+
+
+ {{ + 'SETTING.FEATURES.CONSOLEUSEV2USERAPI_DESCRIPTION' | translate + }} +
diff --git a/console/src/app/components/features/features.component.ts b/console/src/app/components/features/features.component.ts index 327e9d2792..0f8ff761f6 100644 --- a/console/src/app/components/features/features.component.ts +++ b/console/src/app/components/features/features.component.ts @@ -16,13 +16,14 @@ import { InfoSectionModule } from 'src/app/modules/info-section/info-section.mod import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module'; import { Event } from 'src/app/proto/generated/zitadel/event_pb'; import { Source } from 'src/app/proto/generated/zitadel/feature/v2beta/feature_pb'; -import { - GetInstanceFeaturesResponse, - SetInstanceFeaturesRequest, -} from 'src/app/proto/generated/zitadel/feature/v2beta/instance_pb'; import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service'; import { FeatureService } from 'src/app/services/feature.service'; import { ToastService } from 'src/app/services/toast.service'; +import { + GetInstanceFeaturesResponse, + SetInstanceFeaturesRequest, +} from '../../proto/generated/zitadel/feature/v2/instance_pb'; +import { withIdentifier } from 'codelyzer/util/astQuery'; enum ToggleState { ENABLED = 'ENABLED', @@ -39,6 +40,7 @@ type ToggleStates = { oidcTokenExchange?: FeatureState; actions?: FeatureState; oidcSingleV1SessionTermination?: FeatureState; + consoleUseV2UserApi?: FeatureState; }; @Component({ @@ -142,6 +144,7 @@ export class FeaturesComponent implements OnDestroy { ); changed = true; } + req.setConsoleUseV2UserApi(this.toggleStates?.consoleUseV2UserApi?.state === ToggleState.ENABLED); if (changed) { this.featureService @@ -232,6 +235,10 @@ export class FeaturesComponent implements OnDestroy { ? ToggleState.ENABLED : ToggleState.DISABLED, }, + consoleUseV2UserApi: { + source: this.featureData.consoleUseV2UserApi?.source || Source.SOURCE_INSTANCE, + state: this.featureData.consoleUseV2UserApi?.enabled ? ToggleState.ENABLED : ToggleState.DISABLED, + }, }; }); } diff --git a/console/src/app/directives/has-role/has-role.directive.ts b/console/src/app/directives/has-role/has-role.directive.ts index b58e1f3a10..9ba21c1dd2 100644 --- a/console/src/app/directives/has-role/has-role.directive.ts +++ b/console/src/app/directives/has-role/has-role.directive.ts @@ -1,18 +1,17 @@ -import { Directive, Input, OnDestroy, TemplateRef, ViewContainerRef } from '@angular/core'; -import { Subject, takeUntil } from 'rxjs'; +import { DestroyRef, Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Directive({ selector: '[cnslHasRole]', }) -export class HasRoleDirective implements OnDestroy { - private destroy$: Subject = new Subject(); +export class HasRoleDirective { private hasView: boolean = false; @Input() public set hasRole(roles: string[] | RegExp[] | undefined) { if (roles && roles.length > 0) { this.authService .isAllowed(roles) - .pipe(takeUntil(this.destroy$)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((isAllowed) => { if (isAllowed && !this.hasView) { if (this.viewContainerRef.length !== 0) { @@ -38,10 +37,6 @@ export class HasRoleDirective implements OnDestroy { private authService: GrpcAuthService, protected templateRef: TemplateRef, protected viewContainerRef: ViewContainerRef, + private readonly destroyRef: DestroyRef, ) {} - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } } diff --git a/console/src/app/modules/accounts-card/accounts-card.component.ts b/console/src/app/modules/accounts-card/accounts-card.component.ts index 617a41bf6d..2676a5bcf5 100644 --- a/console/src/app/modules/accounts-card/accounts-card.component.ts +++ b/console/src/app/modules/accounts-card/accounts-card.component.ts @@ -4,6 +4,7 @@ import { AuthConfig } from 'angular-oauth2-oidc'; import { Session, User, UserState } from 'src/app/proto/generated/zitadel/user_pb'; import { AuthenticationService } from 'src/app/services/authentication.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; +import { toSignal } from '@angular/core/rxjs-interop'; @Component({ selector: 'cnsl-accounts-card', @@ -18,6 +19,8 @@ export class AccountsCardComponent implements OnInit { public sessions: Session.AsObject[] = []; public loadingUsers: boolean = false; public UserState: any = UserState; + private labelpolicy = toSignal(this.userService.labelpolicy$, { initialValue: undefined }); + constructor( public authService: AuthenticationService, private router: Router, @@ -68,7 +71,7 @@ export class AccountsCardComponent implements OnInit { } public logout(): void { - const lP = JSON.stringify(this.userService.labelpolicy.getValue()); + const lP = JSON.stringify(this.labelpolicy()); localStorage.setItem('labelPolicyOnSignout', lP); this.authService.signout(); diff --git a/console/src/app/modules/footer/footer.component.html b/console/src/app/modules/footer/footer.component.html index 26d863d129..b9eda2d7db 100644 --- a/console/src/app/modules/footer/footer.component.html +++ b/console/src/app/modules/footer/footer.component.html @@ -1,6 +1,6 @@