feat(queries): user IDP links (#2751)

This commit is contained in:
Silvan 2021-12-07 08:33:52 +01:00 committed by GitHub
parent 2ad03285f1
commit 303d4945a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 394 additions and 37 deletions

View File

@ -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,
),

View File

@ -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 {

View File

@ -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:

View File

@ -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,
),

View File

@ -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) {

View File

@ -78,6 +78,7 @@ const (
//count is for validation
idpConfigTypeCount
IDPConfigTypeUnspecified IDPConfigType = -1
)
func (f IDPConfigType) Valid() bool {

View File

@ -98,6 +98,10 @@ var (
name: projection.IDPAutoRegisterCol,
table: idpTable,
}
IDPTypeCol = Column{
name: projection.IDPTypeCol,
table: idpTable,
}
)
var (

View File

@ -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),

View File

@ -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",
},
},

View File

@ -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"

View File

@ -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
}
}

View File

@ -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)
})
}
}

View File

@ -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;