Files
zitadel/internal/api/grpc/project/v2/query.go
Livio Spring c3a3766445 feat(api): move project service v2beta to GA (and deprecate v2beta) (#10844)
# Which Problems Are Solved

As part of our efforts to simplify the structure and versions of our
APIs, were moving all existing v2beta endpoints to v2 and deprecate
them. They will be removed in Zitadel V5.

# How the Problems Are Solved

- This PR moves project v2beta service and its endpoints to a
corresponding v2 version. The v2beta service and endpoints are
deprecated.
- The comments and have been improved and, where not already done, moved
from swagger annotations to proto.
- All required fields have been marked with `(google.api.field_behavior)
= REQUIRED` and validation rules have been added where missing.
- Name ID of the project always `project_id`
- `UpdateProjectRequest` has been updated to align with the creation and
retrieval of a project:
  - `project_role_check` has been renamed to `authorization_required`
  - `has_project_check` has been renamed to `project_access_required`
- `ListProjectRequest` has been changed:
- `project_grant_resource_owner_filter`,
`project_grant_resource_owner_filter` and
`project_organization_id_filter` have been removed and merged into a
single `organization_id_filter` where a `type` can optionally be
specified to select `owned`, `granted` or both project types within a
specified organization.
- `ListProjectGrantReques` has been changed:
- `project_resource_owner_filter` has been renamed to
`project_organization_id_filter`
- `grant_resource_owner_filter` has been renamed to
`granted_organization_id_filter`

# Additional Changes

Replaced deprecated `intergration.WithAuthorization` with
`integration.WithAuthorizationToken` in integration tests.

# Additional Context

- part of #10772
- requires backport to v4.x

(cherry picked from commit 32500e3b0c)
2025-10-28 15:10:03 +01:00

429 lines
16 KiB
Go

package project
import (
"context"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2"
)
func (s *Server) GetProject(ctx context.Context, req *connect.Request[project_pb.GetProjectRequest]) (*connect.Response[project_pb.GetProjectResponse], error) {
project, err := s.query.GetProjectByIDWithPermission(ctx, true, req.Msg.GetProjectId(), s.checkPermission)
if err != nil {
return nil, err
}
return connect.NewResponse(&project_pb.GetProjectResponse{
Project: projectToPb(project),
}), nil
}
func (s *Server) ListProjects(ctx context.Context, req *connect.Request[project_pb.ListProjectsRequest]) (*connect.Response[project_pb.ListProjectsResponse], error) {
queries, err := s.listProjectRequestToModel(req.Msg)
if err != nil {
return nil, err
}
resp, err := s.query.SearchGrantedProjects(ctx, queries, s.checkPermission)
if err != nil {
return nil, err
}
return connect.NewResponse(&project_pb.ListProjectsResponse{
Projects: grantedProjectsToPb(resp.GrantedProjects),
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse),
}), nil
}
func (s *Server) listProjectRequestToModel(req *project_pb.ListProjectsRequest) (*query.ProjectAndGrantedProjectSearchQueries, error) {
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination)
if err != nil {
return nil, err
}
queries, err := projectFiltersToQuery(req.Filters)
if err != nil {
return nil, err
}
return &query.ProjectAndGrantedProjectSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: grantedProjectFieldNameToSortingColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func grantedProjectFieldNameToSortingColumn(field *project_pb.ProjectFieldName) query.Column {
if field == nil {
return query.GrantedProjectColumnCreationDate
}
switch *field {
case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_CREATION_DATE:
return query.GrantedProjectColumnCreationDate
case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_ID:
return query.GrantedProjectColumnID
case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_NAME:
return query.GrantedProjectColumnName
case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_CHANGE_DATE:
return query.GrantedProjectColumnChangeDate
case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_UNSPECIFIED:
return query.GrantedProjectColumnCreationDate
default:
return query.GrantedProjectColumnCreationDate
}
}
func projectFiltersToQuery(queries []*project_pb.ProjectSearchFilter) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, qry := range queries {
q[i], err = projectFilterToModel(qry)
if err != nil {
return nil, err
}
}
return q, nil
}
func projectFilterToModel(filter *project_pb.ProjectSearchFilter) (query.SearchQuery, error) {
switch q := filter.Filter.(type) {
case *project_pb.ProjectSearchFilter_ProjectNameFilter:
return projectNameFilterToQuery(q.ProjectNameFilter)
case *project_pb.ProjectSearchFilter_InProjectIdsFilter:
return projectInIDsFilterToQuery(q.InProjectIdsFilter)
case *project_pb.ProjectSearchFilter_OrganizationIdFilter:
return projectOrganizationIDFilterToQuery(q.OrganizationIdFilter)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid")
}
}
func projectNameFilterToQuery(q *project_pb.ProjectNameFilter) (query.SearchQuery, error) {
return query.NewGrantedProjectNameSearchQuery(filter.TextMethodPbToQuery(q.Method), q.GetProjectName())
}
func projectInIDsFilterToQuery(q *filter_pb.InIDsFilter) (query.SearchQuery, error) {
return query.NewGrantedProjectIDSearchQuery(q.Ids)
}
func projectOrganizationIDFilterToQuery(q *project_pb.ProjectOrganizationIDFilter) (query.SearchQuery, error) {
switch q.GetType() {
case project_pb.ProjectOrganizationIDFilter_OWNED:
return query.NewGrantedProjectResourceOwnerSearchQuery(q.GetOrganizationId())
case project_pb.ProjectOrganizationIDFilter_GRANTED:
return query.NewGrantedProjectGrantedOrganizationIDSearchQuery(q.GetOrganizationId())
case project_pb.ProjectOrganizationIDFilter_OWNED_OR_GRANTED:
return query.NewGrantedProjectOrganizationIDSearchQuery(q.GetOrganizationId())
case project_pb.ProjectOrganizationIDFilter_TYPE_UNSPECIFIED:
return query.NewGrantedProjectOrganizationIDSearchQuery(q.GetOrganizationId())
default:
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-Sk3sd", "List.Query.Invalid")
}
}
func grantedProjectsToPb(projects []*query.GrantedProject) []*project_pb.Project {
o := make([]*project_pb.Project, len(projects))
for i, org := range projects {
o[i] = grantedProjectToPb(org)
}
return o
}
func projectToPb(project *query.Project) *project_pb.Project {
return &project_pb.Project{
ProjectId: project.ID,
OrganizationId: project.ResourceOwner,
CreationDate: timestamppb.New(project.CreationDate),
ChangeDate: timestamppb.New(project.ChangeDate),
State: projectStateToPb(project.State),
Name: project.Name,
PrivateLabelingSetting: privateLabelingSettingToPb(project.PrivateLabelingSetting),
ProjectAccessRequired: project.HasProjectCheck,
ProjectRoleAssertion: project.ProjectRoleAssertion,
AuthorizationRequired: project.ProjectRoleCheck,
}
}
func grantedProjectToPb(project *query.GrantedProject) *project_pb.Project {
var grantedOrganizationID, grantedOrganizationName *string
if project.GrantedOrgID != "" {
grantedOrganizationID = &project.GrantedOrgID
}
if project.OrgName != "" {
grantedOrganizationName = &project.OrgName
}
return &project_pb.Project{
ProjectId: project.ProjectID,
OrganizationId: project.ResourceOwner,
CreationDate: timestamppb.New(project.CreationDate),
ChangeDate: timestamppb.New(project.ChangeDate),
State: projectStateToPb(project.ProjectState),
Name: project.ProjectName,
PrivateLabelingSetting: privateLabelingSettingToPb(project.PrivateLabelingSetting),
ProjectAccessRequired: project.HasProjectCheck,
ProjectRoleAssertion: project.ProjectRoleAssertion,
AuthorizationRequired: project.ProjectRoleCheck,
GrantedOrganizationId: grantedOrganizationID,
GrantedOrganizationName: grantedOrganizationName,
GrantedState: grantedProjectStateToPb(project.ProjectGrantState),
}
}
func projectStateToPb(state domain.ProjectState) project_pb.ProjectState {
switch state {
case domain.ProjectStateActive:
return project_pb.ProjectState_PROJECT_STATE_ACTIVE
case domain.ProjectStateInactive:
return project_pb.ProjectState_PROJECT_STATE_INACTIVE
case domain.ProjectStateUnspecified, domain.ProjectStateRemoved:
return project_pb.ProjectState_PROJECT_STATE_UNSPECIFIED
default:
return project_pb.ProjectState_PROJECT_STATE_UNSPECIFIED
}
}
func grantedProjectStateToPb(state domain.ProjectGrantState) project_pb.GrantedProjectState {
switch state {
case domain.ProjectGrantStateActive:
return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_ACTIVE
case domain.ProjectGrantStateInactive:
return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_INACTIVE
case domain.ProjectGrantStateUnspecified, domain.ProjectGrantStateRemoved:
return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_UNSPECIFIED
default:
return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_UNSPECIFIED
}
}
func privateLabelingSettingToPb(setting domain.PrivateLabelingSetting) project_pb.PrivateLabelingSetting {
switch setting {
case domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy:
return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY
case domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy:
return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ENFORCE_PROJECT_RESOURCE_OWNER_POLICY
case domain.PrivateLabelingSettingUnspecified:
return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED
default:
return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED
}
}
func (s *Server) ListProjectGrants(ctx context.Context, req *connect.Request[project_pb.ListProjectGrantsRequest]) (*connect.Response[project_pb.ListProjectGrantsResponse], error) {
queries, err := s.listProjectGrantsRequestToModel(req.Msg)
if err != nil {
return nil, err
}
resp, err := s.query.SearchProjectGrants(ctx, queries, s.checkPermission)
if err != nil {
return nil, err
}
return connect.NewResponse(&project_pb.ListProjectGrantsResponse{
ProjectGrants: projectGrantsToPb(resp.ProjectGrants),
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse),
}), nil
}
func (s *Server) listProjectGrantsRequestToModel(req *project_pb.ListProjectGrantsRequest) (*query.ProjectGrantSearchQueries, error) {
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination)
if err != nil {
return nil, err
}
queries, err := projectGrantFiltersToModel(req.Filters)
if err != nil {
return nil, err
}
return &query.ProjectGrantSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: projectGrantFieldNameToSortingColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func projectGrantFieldNameToSortingColumn(field *project_pb.ProjectGrantFieldName) query.Column {
if field == nil {
return query.ProjectGrantColumnCreationDate
}
switch *field {
case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_PROJECT_ID:
return query.ProjectGrantColumnProjectID
case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_CREATION_DATE:
return query.ProjectGrantColumnCreationDate
case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_CHANGE_DATE:
return query.ProjectGrantColumnChangeDate
case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_UNSPECIFIED:
return query.ProjectGrantColumnCreationDate
default:
return query.ProjectGrantColumnCreationDate
}
}
func projectGrantFiltersToModel(queries []*project_pb.ProjectGrantSearchFilter) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, qry := range queries {
q[i], err = projectGrantFilterToModel(qry)
if err != nil {
return nil, err
}
}
return q, nil
}
func projectGrantFilterToModel(filter *project_pb.ProjectGrantSearchFilter) (query.SearchQuery, error) {
switch q := filter.Filter.(type) {
case *project_pb.ProjectGrantSearchFilter_ProjectNameFilter:
return projectNameFilterToQuery(q.ProjectNameFilter)
case *project_pb.ProjectGrantSearchFilter_RoleKeyFilter:
return query.NewProjectGrantRoleKeySearchQuery(q.RoleKeyFilter.Key)
case *project_pb.ProjectGrantSearchFilter_InProjectIdsFilter:
return query.NewProjectGrantProjectIDsSearchQuery(q.InProjectIdsFilter.Ids)
case *project_pb.ProjectGrantSearchFilter_OrganizationIdFilter:
return query.NewProjectGrantResourceOwnerSearchQuery(q.OrganizationIdFilter.Id)
case *project_pb.ProjectGrantSearchFilter_GrantedOrganizationIdFilter:
return query.NewProjectGrantGrantedOrgIDSearchQuery(q.GrantedOrganizationIdFilter.Id)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-M099f", "List.Query.Invalid")
}
}
func projectGrantsToPb(projects []*query.ProjectGrant) []*project_pb.ProjectGrant {
p := make([]*project_pb.ProjectGrant, len(projects))
for i, project := range projects {
p[i] = projectGrantToPb(project)
}
return p
}
func projectGrantToPb(project *query.ProjectGrant) *project_pb.ProjectGrant {
return &project_pb.ProjectGrant{
OrganizationId: project.ResourceOwner,
CreationDate: timestamppb.New(project.CreationDate),
ChangeDate: timestamppb.New(project.ChangeDate),
GrantedOrganizationId: project.GrantedOrgID,
GrantedOrganizationName: project.OrgName,
GrantedRoleKeys: project.GrantedRoleKeys,
ProjectId: project.ProjectID,
ProjectName: project.ProjectName,
State: projectGrantStateToPb(project.State),
}
}
func projectGrantStateToPb(state domain.ProjectGrantState) project_pb.ProjectGrantState {
switch state {
case domain.ProjectGrantStateActive:
return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_ACTIVE
case domain.ProjectGrantStateInactive:
return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_INACTIVE
case domain.ProjectGrantStateUnspecified, domain.ProjectGrantStateRemoved:
return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_UNSPECIFIED
default:
return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_UNSPECIFIED
}
}
func (s *Server) ListProjectRoles(ctx context.Context, req *connect.Request[project_pb.ListProjectRolesRequest]) (*connect.Response[project_pb.ListProjectRolesResponse], error) {
queries, err := s.listProjectRolesRequestToModel(req.Msg)
if err != nil {
return nil, err
}
err = queries.AppendProjectIDQuery(req.Msg.GetProjectId())
if err != nil {
return nil, err
}
roles, err := s.query.SearchProjectRoles(ctx, true, queries, s.checkPermission)
if err != nil {
return nil, err
}
return connect.NewResponse(&project_pb.ListProjectRolesResponse{
ProjectRoles: roleViewsToPb(roles.ProjectRoles),
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, roles.SearchResponse),
}), nil
}
func (s *Server) listProjectRolesRequestToModel(req *project_pb.ListProjectRolesRequest) (*query.ProjectRoleSearchQueries, error) {
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination)
if err != nil {
return nil, err
}
queries, err := roleQueriesToModel(req.Filters)
if err != nil {
return nil, err
}
return &query.ProjectRoleSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: projectRoleFieldNameToSortingColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func projectRoleFieldNameToSortingColumn(field *project_pb.ProjectRoleFieldName) query.Column {
if field == nil {
return query.ProjectRoleColumnCreationDate
}
switch *field {
case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_KEY:
return query.ProjectRoleColumnKey
case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_CREATION_DATE:
return query.ProjectRoleColumnCreationDate
case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_CHANGE_DATE:
return query.ProjectRoleColumnChangeDate
case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_UNSPECIFIED:
return query.ProjectRoleColumnCreationDate
default:
return query.ProjectRoleColumnCreationDate
}
}
func roleQueriesToModel(queries []*project_pb.ProjectRoleSearchFilter) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = roleQueryToModel(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func roleQueryToModel(apiQuery *project_pb.ProjectRoleSearchFilter) (query.SearchQuery, error) {
switch q := apiQuery.Filter.(type) {
case *project_pb.ProjectRoleSearchFilter_RoleKeyFilter:
return query.NewProjectRoleKeySearchQuery(filter.TextMethodPbToQuery(q.RoleKeyFilter.Method), q.RoleKeyFilter.Key)
case *project_pb.ProjectRoleSearchFilter_DisplayNameFilter:
return query.NewProjectRoleDisplayNameSearchQuery(filter.TextMethodPbToQuery(q.DisplayNameFilter.Method), q.DisplayNameFilter.DisplayName)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-fms0e", "List.Query.Invalid")
}
}
func roleViewsToPb(roles []*query.ProjectRole) []*project_pb.ProjectRole {
o := make([]*project_pb.ProjectRole, len(roles))
for i, org := range roles {
o[i] = roleViewToPb(org)
}
return o
}
func roleViewToPb(role *query.ProjectRole) *project_pb.ProjectRole {
return &project_pb.ProjectRole{
ProjectId: role.ProjectID,
Key: role.Key,
CreationDate: timestamppb.New(role.CreationDate),
ChangeDate: timestamppb.New(role.ChangeDate),
DisplayName: role.DisplayName,
Group: role.Group,
}
}