diff --git a/internal/api/grpc/auth/idp.go b/internal/api/grpc/auth/idp.go index 0928c3bfd0..5077a2b57e 100644 --- a/internal/api/grpc/auth/idp.go +++ b/internal/api/grpc/auth/idp.go @@ -9,14 +9,18 @@ import ( ) func (s *Server) ListMyLinkedIDPs(ctx context.Context, req *auth_pb.ListMyLinkedIDPsRequest) (*auth_pb.ListMyLinkedIDPsResponse, error) { - idps, err := s.repo.SearchMyExternalIDPs(ctx, ListMyLinkedIDPsRequestToModel(req)) + q, err := ListMyLinkedIDPsRequestToQuery(ctx, req) + if err != nil { + return nil, err + } + idps, err := s.query.UserIDPLinks(ctx, q) if err != nil { return nil, err } return &auth_pb.ListMyLinkedIDPsResponse{ - Result: idp_grpc.IDPsToUserLinkPb(idps.Result), + Result: idp_grpc.IDPUserLinksToPb(idps.Links), Details: object.ToListDetails( - idps.TotalResult, + idps.Count, idps.Sequence, idps.Timestamp, ), diff --git a/internal/api/grpc/auth/idp_converter.go b/internal/api/grpc/auth/idp_converter.go index 90c6ca403b..c2ee6335c6 100644 --- a/internal/api/grpc/auth/idp_converter.go +++ b/internal/api/grpc/auth/idp_converter.go @@ -3,19 +3,27 @@ package auth import ( "context" + "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/api/grpc/object" "github.com/caos/zitadel/internal/domain" - "github.com/caos/zitadel/internal/user/model" + "github.com/caos/zitadel/internal/query" auth_pb "github.com/caos/zitadel/pkg/grpc/auth" ) -func ListMyLinkedIDPsRequestToModel(req *auth_pb.ListMyLinkedIDPsRequest) *model.ExternalIDPSearchRequest { +func ListMyLinkedIDPsRequestToQuery(ctx context.Context, req *auth_pb.ListMyLinkedIDPsRequest) (*query.UserIDPLinksSearchQuery, error) { offset, limit, asc := object.ListQueryToModel(req.Query) - return &model.ExternalIDPSearchRequest{ - Offset: offset, - Limit: limit, - Asc: asc, + q, err := query.NewUserIDPLinksUserIDSearchQuery(authz.GetCtxData(ctx).UserID) + if err != nil { + return nil, err } + return &query.UserIDPLinksSearchQuery{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + Queries: []query.SearchQuery{q}, + }, nil } func RemoveMyLinkedIDPRequestToDomain(ctx context.Context, req *auth_pb.RemoveMyLinkedIDPRequest) *domain.UserIDPLink { diff --git a/internal/api/grpc/idp/converter.go b/internal/api/grpc/idp/converter.go index e8bc35bb14..bf352d3d12 100644 --- a/internal/api/grpc/idp/converter.go +++ b/internal/api/grpc/idp/converter.go @@ -5,7 +5,6 @@ import ( "github.com/caos/zitadel/internal/domain" iam_model "github.com/caos/zitadel/internal/iam/model" "github.com/caos/zitadel/internal/query" - user_model "github.com/caos/zitadel/internal/user/model" idp_pb "github.com/caos/zitadel/pkg/grpc/idp" ) @@ -61,31 +60,30 @@ func ExternalIDPViewToLoginPolicyLinkPb(link *iam_model.IDPProviderView) *idp_pb return &idp_pb.IDPLoginPolicyLink{ IdpId: link.IDPConfigID, IdpName: link.Name, - IdpType: IDPTypeToPb(link.IDPConfigType), + IdpType: IDPTypeViewToPb(link.IDPConfigType), } } -func IDPsToUserLinkPb(res []*user_model.ExternalIDPView) []*idp_pb.IDPUserLink { +func IDPUserLinksToPb(res []*query.UserIDPLink) []*idp_pb.IDPUserLink { links := make([]*idp_pb.IDPUserLink, len(res)) for i, link := range res { - links[i] = ExternalIDPViewToUserLinkPb(link) + links[i] = IDPUserLinkToPb(link) } return links } -func ExternalIDPViewToUserLinkPb(link *user_model.ExternalIDPView) *idp_pb.IDPUserLink { +func IDPUserLinkToPb(link *query.UserIDPLink) *idp_pb.IDPUserLink { return &idp_pb.IDPUserLink{ UserId: link.UserID, - IdpId: link.IDPConfigID, + IdpId: link.IDPID, IdpName: link.IDPName, - ProvidedUserId: link.ExternalUserID, - ProvidedUserName: link.UserDisplayName, - //TODO: as soon as saml is implemented we need to switch here - //IdpType: IDPTypeToPb(link.Type), + ProvidedUserId: link.ProvidedUserID, + ProvidedUserName: link.ProvidedUsername, + IdpType: IDPTypeToPb(link.IDPType), } } -func IDPTypeToPb(idpType iam_model.IdpConfigType) idp_pb.IDPType { +func IDPTypeViewToPb(idpType iam_model.IdpConfigType) idp_pb.IDPType { switch idpType { case iam_model.IDPConfigTypeOIDC: return idp_pb.IDPType_IDP_TYPE_OIDC @@ -98,6 +96,19 @@ func IDPTypeToPb(idpType iam_model.IdpConfigType) idp_pb.IDPType { } } +func IDPTypeToPb(idpType domain.IDPConfigType) idp_pb.IDPType { + switch idpType { + case domain.IDPConfigTypeOIDC: + return idp_pb.IDPType_IDP_TYPE_OIDC + case domain.IDPConfigTypeSAML: + return idp_pb.IDPType_IDP_TYPE_UNSPECIFIED + case domain.IDPConfigTypeJWT: + return idp_pb.IDPType_IDP_TYPE_JWT + default: + return idp_pb.IDPType_IDP_TYPE_UNSPECIFIED + } +} + func IDPStateToPb(state domain.IDPConfigState) idp_pb.IDPState { switch state { case domain.IDPConfigStateActive: diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index d0c06a7315..5d052f3f45 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -606,14 +606,18 @@ func (s *Server) RemoveMachineKey(ctx context.Context, req *mgmt_pb.RemoveMachin } func (s *Server) ListHumanLinkedIDPs(ctx context.Context, req *mgmt_pb.ListHumanLinkedIDPsRequest) (*mgmt_pb.ListHumanLinkedIDPsResponse, error) { - res, err := s.user.SearchExternalIDPs(ctx, ListHumanLinkedIDPsRequestToModel(req)) + queries, err := ListHumanLinkedIDPsRequestToQuery(ctx, req) + if err != nil { + return nil, err + } + res, err := s.query.UserIDPLinks(ctx, queries) if err != nil { return nil, err } return &mgmt_pb.ListHumanLinkedIDPsResponse{ - Result: idp_grpc.IDPsToUserLinkPb(res.Result), + Result: idp_grpc.IDPUserLinksToPb(res.Links), Details: obj_grpc.ToListDetails( - res.TotalResult, + res.Count, res.Sequence, res.Timestamp, ), diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go index 52192b9c1c..3b7b42b683 100644 --- a/internal/api/grpc/management/user_converter.go +++ b/internal/api/grpc/management/user_converter.go @@ -16,6 +16,7 @@ import ( "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/eventstore/v1/models" key_model "github.com/caos/zitadel/internal/key/model" + "github.com/caos/zitadel/internal/query" user_model "github.com/caos/zitadel/internal/user/model" mgmt_pb "github.com/caos/zitadel/pkg/grpc/management" user_pb "github.com/caos/zitadel/pkg/grpc/user" @@ -234,14 +235,24 @@ func RemoveHumanLinkedIDPRequestToDomain(ctx context.Context, req *mgmt_pb.Remov } } -func ListHumanLinkedIDPsRequestToModel(req *mgmt_pb.ListHumanLinkedIDPsRequest) *user_model.ExternalIDPSearchRequest { +func ListHumanLinkedIDPsRequestToQuery(ctx context.Context, req *mgmt_pb.ListHumanLinkedIDPsRequest) (*query.UserIDPLinksSearchQuery, error) { offset, limit, asc := object.ListQueryToModel(req.Query) - return &user_model.ExternalIDPSearchRequest{ - Offset: offset, - Limit: limit, - Asc: asc, - Queries: []*user_model.ExternalIDPSearchQuery{{Key: user_model.ExternalIDPSearchKeyUserID, Method: domain.SearchMethodEquals, Value: req.UserId}}, + userQuery, err := query.NewUserIDPLinksUserIDSearchQuery(req.UserId) + if err != nil { + return nil, err } + resourceOwnerQuery, err := query.NewUserIDPLinksResourceOwnerSearchQuery(authz.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + return &query.UserIDPLinksSearchQuery{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + Queries: []query.SearchQuery{userQuery, resourceOwnerQuery}, + }, nil } func ListUserMembershipsRequestToModel(req *mgmt_pb.ListUserMembershipsRequest) (*user_model.UserMembershipSearchRequest, error) { diff --git a/internal/domain/idp_config.go b/internal/domain/idp_config.go index e29337b332..e263445bfb 100644 --- a/internal/domain/idp_config.go +++ b/internal/domain/idp_config.go @@ -78,6 +78,7 @@ const ( //count is for validation idpConfigTypeCount + IDPConfigTypeUnspecified IDPConfigType = -1 ) func (f IDPConfigType) Valid() bool { diff --git a/internal/query/idp.go b/internal/query/idp.go index e7f99333bc..4af4554858 100644 --- a/internal/query/idp.go +++ b/internal/query/idp.go @@ -98,6 +98,10 @@ var ( name: projection.IDPAutoRegisterCol, table: idpTable, } + IDPTypeCol = Column{ + name: projection.IDPTypeCol, + table: idpTable, + } ) var ( diff --git a/internal/query/projection/idp.go b/internal/query/projection/idp.go index 08a75417e5..7a7bdedb2f 100644 --- a/internal/query/projection/idp.go +++ b/internal/query/projection/idp.go @@ -134,6 +134,7 @@ const ( IDPStylingTypeCol = "styling_type" IDPOwnerTypeCol = "owner_type" IDPAutoRegisterCol = "auto_register" + IDPTypeCol = "type" OIDCConfigIDPIDCol = "idp_id" OIDCConfigClientIDCol = "client_id" @@ -311,6 +312,7 @@ func (p *IDPProjection) reduceOIDCConfigAdded(event eventstore.EventReader) (*ha []handler.Column{ handler.NewCol(IDPChangeDateCol, idpEvent.CreationDate()), handler.NewCol(IDPSequenceCol, idpEvent.Sequence()), + handler.NewCol(IDPTypeCol, domain.IDPConfigTypeOIDC), }, []handler.Condition{ handler.NewCond(IDPIDCol, idpEvent.IDPConfigID), @@ -413,6 +415,7 @@ func (p *IDPProjection) reduceJWTConfigAdded(event eventstore.EventReader) (*han []handler.Column{ handler.NewCol(IDPChangeDateCol, idpEvent.CreationDate()), handler.NewCol(IDPSequenceCol, idpEvent.Sequence()), + handler.NewCol(IDPTypeCol, domain.IDPConfigTypeJWT), }, []handler.Condition{ handler.NewCond(IDPIDCol, idpEvent.IDPConfigID), diff --git a/internal/query/projection/idp_test.go b/internal/query/projection/idp_test.go index 4e457595be..b08e7172f2 100644 --- a/internal/query/projection/idp_test.go +++ b/internal/query/projection/idp_test.go @@ -227,10 +227,11 @@ func TestIDPProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE zitadel.projections.idps SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedStmt: "UPDATE zitadel.projections.idps SET (change_date, sequence, type) = ($1, $2, $3) WHERE (id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), + domain.IDPConfigTypeOIDC, "idp-config-id", }, }, @@ -353,10 +354,11 @@ func TestIDPProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE zitadel.projections.idps SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedStmt: "UPDATE zitadel.projections.idps SET (change_date, sequence, type) = ($1, $2, $3) WHERE (id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), + domain.IDPConfigTypeJWT, "idp-config-id", }, }, @@ -643,10 +645,11 @@ func TestIDPProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE zitadel.projections.idps SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedStmt: "UPDATE zitadel.projections.idps SET (change_date, sequence, type) = ($1, $2, $3) WHERE (id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), + domain.IDPConfigTypeOIDC, "idp-config-id", }, }, @@ -769,10 +772,11 @@ func TestIDPProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE zitadel.projections.idps SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedStmt: "UPDATE zitadel.projections.idps SET (change_date, sequence, type) = ($1, $2, $3) WHERE (id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), + domain.IDPConfigTypeJWT, "idp-config-id", }, }, diff --git a/internal/query/projection/idp_user_link.go b/internal/query/projection/idp_user_link.go index 9d170d8cd7..c255c8bf96 100644 --- a/internal/query/projection/idp_user_link.go +++ b/internal/query/projection/idp_user_link.go @@ -15,10 +15,6 @@ type IDPUserLinkProjection struct { crdb.StatementHandler } -const ( - IDPUserLinkTable = "zitadel.projections.idp_user_links" -) - func NewIDPUserLinkProjection(ctx context.Context, config crdb.StatementHandlerConfig) *IDPUserLinkProjection { p := &IDPUserLinkProjection{} config.ProjectionName = IDPUserLinkTable @@ -50,6 +46,7 @@ func (p *IDPUserLinkProjection) reducers() []handler.AggregateReducer { } const ( + IDPUserLinkTable = "zitadel.projections.idp_user_links" IDPUserLinkIDPIDCol = "idp_id" IDPUserLinkUserIDCol = "user_id" IDPUserLinkExternalUserIDCol = "external_user_id" diff --git a/internal/query/user_idp_link.go b/internal/query/user_idp_link.go new file mode 100644 index 0000000000..a3a6702440 --- /dev/null +++ b/internal/query/user_idp_link.go @@ -0,0 +1,158 @@ +package query + +import ( + "context" + "database/sql" + + sq "github.com/Masterminds/squirrel" + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/query/projection" +) + +type UserIDPLink struct { + IDPID string + UserID string + IDPName string + ProvidedUserID string + ProvidedUsername string + IDPType domain.IDPConfigType +} + +type UserIDPLinks struct { + SearchResponse + Links []*UserIDPLink +} + +type UserIDPLinksSearchQuery struct { + SearchRequest + Queries []SearchQuery +} + +func (q *UserIDPLinksSearchQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + query = q.SearchRequest.toQuery(query) + for _, q := range q.Queries { + query = q.toQuery(query) + } + return query +} + +var ( + idpUserLinkTable = table{ + name: projection.IDPUserLinkTable, + } + IDPUserLinkIDPIDCol = Column{ + name: projection.IDPUserLinkIDPIDCol, + table: idpUserLinkTable, + } + IDPUserLinkUserIDCol = Column{ + name: projection.IDPUserLinkUserIDCol, + table: idpUserLinkTable, + } + IDPUserLinkExternalUserIDCol = Column{ + name: projection.IDPUserLinkExternalUserIDCol, + table: idpUserLinkTable, + } + IDPUserLinkCreationDateCol = Column{ + name: projection.IDPUserLinkCreationDateCol, + table: idpUserLinkTable, + } + IDPUserLinkChangeDateCol = Column{ + name: projection.IDPUserLinkChangeDateCol, + table: idpUserLinkTable, + } + IDPUserLinkSequenceCol = Column{ + name: projection.IDPUserLinkSequenceCol, + table: idpUserLinkTable, + } + IDPUserLinkResourceOwnerCol = Column{ + name: projection.IDPUserLinkResourceOwnerCol, + table: idpUserLinkTable, + } + IDPUserLinkDisplayNameCol = Column{ + name: projection.IDPUserLinkDisplayNameCol, + table: idpUserLinkTable, + } +) + +func (q *Queries) UserIDPLinks(ctx context.Context, queries *UserIDPLinksSearchQuery) (idps *UserIDPLinks, err error) { + query, scan := prepareUserIDPLinksQuery() + stmt, args, err := queries.toQuery(query).ToSql() + if err != nil { + return nil, errors.ThrowInvalidArgument(err, "QUERY-4zzFK", "Errors.Query.InvalidRequest") + } + + rows, err := q.client.QueryContext(ctx, stmt, args...) + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-C1E4D", "Errors.Internal") + } + idps, err = scan(rows) + if err != nil { + return nil, err + } + idps.LatestSequence, err = q.latestSequence(ctx, idpUserLinkTable) + return idps, err +} + +func NewUserIDPLinksUserIDSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(IDPUserLinkUserIDCol, value, TextEquals) +} + +func NewUserIDPLinksResourceOwnerSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(IDPUserLinkResourceOwnerCol, value, TextEquals) +} + +func prepareUserIDPLinksQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserIDPLinks, error)) { + return sq.Select( + IDPUserLinkIDPIDCol.identifier(), + IDPUserLinkUserIDCol.identifier(), + IDPNameCol.identifier(), + IDPUserLinkExternalUserIDCol.identifier(), + IDPUserLinkDisplayNameCol.identifier(), + IDPTypeCol.identifier(), + countColumn.identifier()). + From(idpUserLinkTable.identifier()). + LeftJoin(join(IDPIDCol, IDPUserLinkIDPIDCol)).PlaceholderFormat(sq.Dollar), + func(rows *sql.Rows) (*UserIDPLinks, error) { + idps := make([]*UserIDPLink, 0) + var count uint64 + for rows.Next() { + var ( + idpName = sql.NullString{} + idpType = sql.NullInt16{} + idp = new(UserIDPLink) + ) + err := rows.Scan( + &idp.IDPID, + &idp.UserID, + &idpName, + &idp.ProvidedUserID, + &idp.ProvidedUsername, + &idpType, + &count, + ) + if err != nil { + return nil, err + } + idp.IDPName = idpName.String + //IDPType 0 is oidc so we have to set unspecified manually + if idpType.Valid { + idp.IDPType = domain.IDPConfigType(idpType.Int16) + } else { + idp.IDPType = domain.IDPConfigTypeUnspecified + } + idps = append(idps, idp) + } + + if err := rows.Close(); err != nil { + return nil, errors.ThrowInternal(err, "QUERY-nwx6U", "Errors.Query.CloseRows") + } + + return &UserIDPLinks{ + Links: idps, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } +} diff --git a/internal/query/user_idp_link_test.go b/internal/query/user_idp_link_test.go new file mode 100644 index 0000000000..e63e2f3618 --- /dev/null +++ b/internal/query/user_idp_link_test.go @@ -0,0 +1,139 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "regexp" + "testing" + + "github.com/caos/zitadel/internal/domain" +) + +var ( + userIDPLinksQuery = regexp.QuoteMeta(`SELECT zitadel.projections.idp_user_links.idp_id,` + + ` zitadel.projections.idp_user_links.user_id,` + + ` zitadel.projections.idps.name,` + + ` zitadel.projections.idp_user_links.external_user_id,` + + ` zitadel.projections.idp_user_links.display_name,` + + ` zitadel.projections.idps.type,` + + ` COUNT(*) OVER ()` + + ` FROM zitadel.projections.idp_user_links` + + ` LEFT JOIN zitadel.projections.idps ON zitadel.projections.idp_user_links.idp_id = zitadel.projections.idps.id`) + userIDPLinksCols = []string{ + "idp_id", + "user_id", + "name", + "external_user_id", + "display_name", + "type", + "count", + } +) + +func Test_UserIDPLinkPrepares(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareIDPsQuery found", + prepare: prepareUserIDPLinksQuery, + want: want{ + sqlExpectations: mockQueries( + userIDPLinksQuery, + userIDPLinksCols, + [][]driver.Value{ + { + "idp-id", + "user-id", + "idp-name", + "external-user-id", + "display-name", + domain.IDPConfigTypeJWT, + }, + }, + ), + }, + object: &UserIDPLinks{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Links: []*UserIDPLink{ + { + IDPID: "idp-id", + UserID: "user-id", + IDPName: "idp-name", + ProvidedUserID: "external-user-id", + ProvidedUsername: "display-name", + IDPType: domain.IDPConfigTypeJWT, + }, + }, + }, + }, + { + name: "prepareIDPsQuery no idp", + prepare: prepareUserIDPLinksQuery, + want: want{ + sqlExpectations: mockQueries( + userIDPLinksQuery, + userIDPLinksCols, + [][]driver.Value{ + { + "idp-id", + "user-id", + nil, + "external-user-id", + "display-name", + nil, + }, + }, + ), + }, + object: &UserIDPLinks{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Links: []*UserIDPLink{ + { + IDPID: "idp-id", + UserID: "user-id", + IDPName: "", + ProvidedUserID: "external-user-id", + ProvidedUsername: "display-name", + IDPType: domain.IDPConfigTypeUnspecified, + }, + }, + }, + }, + { + name: "prepareIDPsQuery sql err", + prepare: prepareUserIDPLinksQuery, + want: want{ + sqlExpectations: mockQueryErr( + userIDPLinksQuery, + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) + }) + } +} diff --git a/migrations/cockroach/V1.100__idp_type.sql b/migrations/cockroach/V1.100__idp_type.sql new file mode 100644 index 0000000000..0a6f391e8b --- /dev/null +++ b/migrations/cockroach/V1.100__idp_type.sql @@ -0,0 +1,13 @@ +ALTER TABLE zitadel.projections.idps ADD COLUMN type INT2; + +-- jwt-type is 2 +-- oidc-type is 0 +WITH doa AS ( + SELECT i.id, IF(o.idp_id IS NULL, 0, 2) as type + FROM projections.idps i + LEFT JOIN projections.idps_oidc_config o + ON o.idp_id = i.id + LEFT JOIN projections.idps_jwt_config j + ON j.idp_id = i.id +) +UPDATE zitadel.projections.idps SET type = doa.type FROM doa WHERE doa.id = zitadel.projections.idps.id;