fix: provide more information in the retrieve idp information (#5927)

* fix: provide more information in the retrieve idp information

* change raw_information to proto struct

* change unmarshal

* improve description
This commit is contained in:
Livio Spring 2023-06-20 14:39:50 +02:00 committed by GitHub
parent 83da9cada7
commit 1017568cf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 169 additions and 52 deletions

View File

@ -3,9 +3,19 @@ package grpc
import ( import (
"testing" "testing"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/structpb"
) )
var CustomMappers = map[protoreflect.FullName]func(testing.TB, protoreflect.ProtoMessage) any{
"google.protobuf.Struct": func(t testing.TB, msg protoreflect.ProtoMessage) any {
e, ok := msg.(*structpb.Struct)
require.True(t, ok)
return e.AsMap()
},
}
// AllFieldsSet recusively checks if all values in a message // AllFieldsSet recusively checks if all values in a message
// have a non-zero value. // have a non-zero value.
func AllFieldsSet(t testing.TB, msg protoreflect.Message, ignoreTypes ...protoreflect.FullName) { func AllFieldsSet(t testing.TB, msg protoreflect.Message, ignoreTypes ...protoreflect.FullName) {
@ -36,3 +46,24 @@ func AllFieldsSet(t testing.TB, msg protoreflect.Message, ignoreTypes ...protore
} }
} }
} }
func AllFieldsEqual(t testing.TB, expected, actual protoreflect.Message, customMappers map[protoreflect.FullName]func(testing.TB, protoreflect.ProtoMessage) any) {
md := expected.Descriptor()
name := md.FullName()
if mapper := customMappers[name]; mapper != nil {
require.Equal(t, mapper(t, expected.Interface()), mapper(t, actual.Interface()))
return
}
fields := md.Fields()
for i := 0; i < fields.Len(); i++ {
fd := fields.Get(i)
if fd.Kind() == protoreflect.MessageKind {
AllFieldsEqual(t, expected.Get(fd).Message(), actual.Get(fd).Message(), customMappers)
} else {
require.Equal(t, expected.Get(fd).Interface(), actual.Get(fd).Interface())
}
}
}

View File

@ -6,6 +6,7 @@ import (
"io" "io"
"golang.org/x/text/language" "golang.org/x/text/language"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
@ -64,8 +65,8 @@ func addUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman,
for i, link := range req.GetIdpLinks() { for i, link := range req.GetIdpLinks() {
links[i] = &command.AddLink{ links[i] = &command.AddLink{
IDPID: link.GetIdpId(), IDPID: link.GetIdpId(),
IDPExternalID: link.GetIdpExternalId(), IDPExternalID: link.GetUserId(),
DisplayName: link.GetDisplayName(), DisplayName: link.GetUserName(),
} }
} }
return &command.AddHuman{ return &command.AddHuman{
@ -124,8 +125,8 @@ func (s *Server) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_
orgID := authz.GetCtxData(ctx).OrgID orgID := authz.GetCtxData(ctx).OrgID
details, err := s.command.AddUserIDPLink(ctx, req.UserId, orgID, &domain.UserIDPLink{ details, err := s.command.AddUserIDPLink(ctx, req.UserId, orgID, &domain.UserIDPLink{
IDPConfigID: req.GetIdpLink().GetIdpId(), IDPConfigID: req.GetIdpLink().GetIdpId(),
ExternalUserID: req.GetIdpLink().GetIdpExternalId(), ExternalUserID: req.GetIdpLink().GetUserId(),
DisplayName: req.GetIdpLink().GetDisplayName(), DisplayName: req.GetIdpLink().GetUserName(),
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -176,6 +177,12 @@ func intentToIDPInformationPb(intent *command.IDPIntentWriteModel, alg crypto.En
return nil, err return nil, err
} }
} }
rawInformation := new(structpb.Struct)
err = rawInformation.UnmarshalJSON(intent.IDPUser)
if err != nil {
return nil, err
}
return &user.RetrieveIdentityProviderInformationResponse{ return &user.RetrieveIdentityProviderInformationResponse{
Details: &object_pb.Details{ Details: &object_pb.Details{
Sequence: intent.ProcessedSequence, Sequence: intent.ProcessedSequence,
@ -189,7 +196,10 @@ func intentToIDPInformationPb(intent *command.IDPIntentWriteModel, alg crypto.En
IdToken: idToken, IdToken: idToken,
}, },
}, },
IdpInformation: intent.IDPUser, IdpId: intent.IDPID,
UserId: intent.IDPUserID,
UserName: intent.IDPUserName,
RawInformation: rawInformation,
}, },
}, nil }, nil
} }

View File

@ -15,11 +15,13 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v2/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/idp/providers/oauth" openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
"github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/internal/repository/idp" "github.com/zitadel/zitadel/internal/repository/idp"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha" object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
@ -81,12 +83,15 @@ func createSuccessfulIntent(t *testing.T, idpID string) (string, string, time.Ti
intentID := createIntent(t, idpID) intentID := createIntent(t, idpID)
writeModel, err := Tester.Commands.GetIntentWriteModel(ctx, intentID, Tester.Organisation.ID) writeModel, err := Tester.Commands.GetIntentWriteModel(ctx, intentID, Tester.Organisation.ID)
require.NoError(t, err) require.NoError(t, err)
idpUser := &oauth.UserMapper{ idpUser := openid.NewUser(
RawInfo: map[string]interface{}{ &oidc.UserInfo{
"id": "id", Subject: "id",
UserInfoProfile: oidc.UserInfoProfile{
PreferredUsername: "username",
},
}, },
} )
idpSession := &oauth.Session{ idpSession := &openid.Session{
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{ Token: &oauth2.Token{
AccessToken: "accessToken", AccessToken: "accessToken",
@ -386,9 +391,9 @@ func TestServer_AddHumanUser(t *testing.T) {
}, },
IdpLinks: []*user.IDPLink{ IdpLinks: []*user.IDPLink{
{ {
IdpId: "idpID", IdpId: "idpID",
IdpExternalId: "externalID", UserId: "userID",
DisplayName: "displayName", UserName: "username",
}, },
}, },
}, },
@ -433,9 +438,9 @@ func TestServer_AddHumanUser(t *testing.T) {
}, },
IdpLinks: []*user.IDPLink{ IdpLinks: []*user.IDPLink{
{ {
IdpId: idpID, IdpId: idpID,
IdpExternalId: "externalID", UserId: "userID",
DisplayName: "displayName", UserName: "username",
}, },
}, },
}, },
@ -495,9 +500,9 @@ func TestServer_AddIDPLink(t *testing.T) {
&user.AddIDPLinkRequest{ &user.AddIDPLinkRequest{
UserId: "userID", UserId: "userID",
IdpLink: &user.IDPLink{ IdpLink: &user.IDPLink{
IdpId: idpID, IdpId: idpID,
IdpExternalId: "externalID", UserId: "userID",
DisplayName: "displayName", UserName: "username",
}, },
}, },
}, },
@ -511,9 +516,9 @@ func TestServer_AddIDPLink(t *testing.T) {
&user.AddIDPLinkRequest{ &user.AddIDPLinkRequest{
UserId: Tester.Users[integration.OrgOwner].ID, UserId: Tester.Users[integration.OrgOwner].ID,
IdpLink: &user.IDPLink{ IdpLink: &user.IDPLink{
IdpId: "idpID", IdpId: "idpID",
IdpExternalId: "externalID", UserId: "userID",
DisplayName: "displayName", UserName: "username",
}, },
}, },
}, },
@ -527,9 +532,9 @@ func TestServer_AddIDPLink(t *testing.T) {
&user.AddIDPLinkRequest{ &user.AddIDPLinkRequest{
UserId: Tester.Users[integration.OrgOwner].ID, UserId: Tester.Users[integration.OrgOwner].ID,
IdpLink: &user.IDPLink{ IdpLink: &user.IDPLink{
IdpId: idpID, IdpId: idpID,
IdpExternalId: "externalID", UserId: "userID",
DisplayName: "displayName", UserName: "username",
}, },
}, },
}, },
@ -678,7 +683,17 @@ func TestServer_RetrieveIdentityProviderInformation(t *testing.T) {
IdToken: gu.Ptr("idToken"), IdToken: gu.Ptr("idToken"),
}, },
}, },
IdpInformation: []byte(`{"RawInfo":{"id":"id"}}`), IdpId: idpID,
UserId: "id",
UserName: "username",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"sub": "id",
"preferred_username": "username",
})
require.NoError(t, err)
return s
}(),
}, },
}, },
wantErr: false, wantErr: false,
@ -693,8 +708,7 @@ func TestServer_RetrieveIdentityProviderInformation(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
} }
require.Equal(t, tt.want.GetDetails(), got.GetDetails()) grpc.AllFieldsEqual(t, got.ProtoReflect(), tt.want.ProtoReflect(), grpc.CustomMappers)
require.Equal(t, tt.want.GetIdpInformation(), got.GetIdpInformation())
}) })
} }
} }

View File

@ -9,6 +9,8 @@ import (
"github.com/muhlemmer/gu" "github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc" "github.com/zitadel/zitadel/internal/api/grpc"
@ -21,6 +23,8 @@ import (
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
) )
var ignoreTypes = []protoreflect.FullName{"google.protobuf.Duration", "google.protobuf.Struct"}
func Test_hashedPasswordToCommand(t *testing.T) { func Test_hashedPasswordToCommand(t *testing.T) {
type args struct { type args struct {
hashed *user.HashedPassword hashed *user.HashedPassword
@ -128,8 +132,10 @@ func Test_intentToIDPInformationPb(t *testing.T) {
InstanceID: "instanceID", InstanceID: "instanceID",
ChangeDate: time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local), ChangeDate: time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local),
}, },
IDPID: "idpID", IDPID: "idpID",
IDPUser: []byte(`{"id": "id"}`), IDPUser: []byte(`{"userID": "idpUserID", "username": "username"}`),
IDPUserID: "idpUserID",
IDPUserName: "username",
IDPAccessToken: &crypto.CryptoValue{ IDPAccessToken: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption, CryptoType: crypto.TypeEncryption,
Algorithm: "enc", Algorithm: "enc",
@ -158,8 +164,10 @@ func Test_intentToIDPInformationPb(t *testing.T) {
InstanceID: "instanceID", InstanceID: "instanceID",
ChangeDate: time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local), ChangeDate: time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local),
}, },
IDPID: "idpID", IDPID: "idpID",
IDPUser: []byte(`{"id": "id"}`), IDPUser: []byte(`{"userID": "idpUserID", "username": "username"}`),
IDPUserID: "idpUserID",
IDPUserName: "username",
IDPAccessToken: &crypto.CryptoValue{ IDPAccessToken: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption, CryptoType: crypto.TypeEncryption,
Algorithm: "enc", Algorithm: "enc",
@ -184,8 +192,19 @@ func Test_intentToIDPInformationPb(t *testing.T) {
Oauth: &user.IDPOAuthAccessInformation{ Oauth: &user.IDPOAuthAccessInformation{
AccessToken: "accessToken", AccessToken: "accessToken",
IdToken: gu.Ptr("idToken"), IdToken: gu.Ptr("idToken"),
}}, },
IdpInformation: []byte(`{"id": "id"}`), },
IdpId: "idpID",
UserId: "idpUserID",
UserName: "username",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"userID": "idpUserID",
"username": "username",
})
require.NoError(t, err)
return s
}(),
}, },
}, },
err: nil, err: nil,
@ -196,9 +215,9 @@ func Test_intentToIDPInformationPb(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := intentToIDPInformationPb(tt.args.intent, tt.args.alg) got, err := intentToIDPInformationPb(tt.args.intent, tt.args.alg)
require.ErrorIs(t, err, tt.res.err) require.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.resp, got) grpc.AllFieldsEqual(t, got.ProtoReflect(), tt.res.resp.ProtoReflect(), grpc.CustomMappers)
if tt.res.resp != nil { if tt.res.resp != nil {
grpc.AllFieldsSet(t, got.ProtoReflect()) grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
} }
}) })
} }

View File

@ -123,6 +123,8 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr
ctx, ctx,
&idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate, &idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate,
idpInfo, idpInfo,
idpUser.GetID(),
idpUser.GetPreferredUsername(),
userID, userID,
accessToken, accessToken,
idToken, idToken,

View File

@ -16,6 +16,8 @@ type IDPIntentWriteModel struct {
FailureURL *url.URL FailureURL *url.URL
IDPID string IDPID string
IDPUser []byte IDPUser []byte
IDPUserID string
IDPUserName string
IDPAccessToken *crypto.CryptoValue IDPAccessToken *crypto.CryptoValue
IDPIDToken string IDPIDToken string
UserID string UserID string
@ -72,6 +74,8 @@ func (wm *IDPIntentWriteModel) reduceStartedEvent(e *idpintent.StartedEvent) {
func (wm *IDPIntentWriteModel) reduceSucceededEvent(e *idpintent.SucceededEvent) { func (wm *IDPIntentWriteModel) reduceSucceededEvent(e *idpintent.SucceededEvent) {
wm.UserID = e.UserID wm.UserID = e.UserID
wm.IDPUser = e.IDPUser wm.IDPUser = e.IDPUser
wm.IDPUserID = e.IDPUserID
wm.IDPUserName = e.IDPUserName
wm.IDPAccessToken = e.IDPAccessToken wm.IDPAccessToken = e.IDPAccessToken
wm.IDPIDToken = e.IDPIDToken wm.IDPIDToken = e.IDPIDToken
wm.State = domain.IDPIntentStateSucceeded wm.State = domain.IDPIntentStateSucceeded

View File

@ -439,7 +439,9 @@ func TestCommands_SucceedIDPIntent(t *testing.T) {
event, _ := idpintent.NewSucceededEvent( event, _ := idpintent.NewSucceededEvent(
context.Background(), context.Background(),
&idpintent.NewAggregate("id", "ro").Aggregate, &idpintent.NewAggregate("id", "ro").Aggregate,
[]byte(`{"RawInfo":{"id":"id"}}`), []byte(`{"sub":"id","preferred_username":"username"}`),
"id",
"username",
"", "",
&crypto.CryptoValue{ &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption, CryptoType: crypto.TypeEncryption,
@ -447,7 +449,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) {
KeyID: "id", KeyID: "id",
Crypted: []byte("accessToken"), Crypted: []byte("accessToken"),
}, },
"", "idToken",
) )
return event return event
}(), }(),
@ -458,18 +460,20 @@ func TestCommands_SucceedIDPIntent(t *testing.T) {
args{ args{
ctx: context.Background(), ctx: context.Background(),
writeModel: NewIDPIntentWriteModel("id", "ro"), writeModel: NewIDPIntentWriteModel("id", "ro"),
idpSession: &oauth.Session{ idpSession: &openid.Session{
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{ Token: &oauth2.Token{
AccessToken: "accessToken", AccessToken: "accessToken",
}, },
IDToken: "idToken",
}, },
}, },
idpUser: &oauth.UserMapper{ idpUser: openid.NewUser(&oidc.UserInfo{
RawInfo: map[string]interface{}{ Subject: "id",
"id": "id", UserInfoProfile: oidc.UserInfoProfile{
PreferredUsername: "username",
}, },
}, }),
}, },
res{ res{
token: "aWQ", token: "aWQ",

View File

@ -69,6 +69,8 @@ type SucceededEvent struct {
eventstore.BaseEvent `json:"-"` eventstore.BaseEvent `json:"-"`
IDPUser []byte `json:"idpUser"` IDPUser []byte `json:"idpUser"`
IDPUserID string `json:"idpUserId,omitempty"`
IDPUserName string `json:"idpUserName,omitempty"`
UserID string `json:"userId,omitempty"` UserID string `json:"userId,omitempty"`
IDPAccessToken *crypto.CryptoValue `json:"idpAccessToken,omitempty"` IDPAccessToken *crypto.CryptoValue `json:"idpAccessToken,omitempty"`
IDPIDToken string `json:"idpIdToken,omitempty"` IDPIDToken string `json:"idpIdToken,omitempty"`
@ -78,6 +80,8 @@ func NewSucceededEvent(
ctx context.Context, ctx context.Context,
aggregate *eventstore.Aggregate, aggregate *eventstore.Aggregate,
idpUser []byte, idpUser []byte,
idpUserID,
idpUserName,
userID string, userID string,
idpAccessToken *crypto.CryptoValue, idpAccessToken *crypto.CryptoValue,
idpIDToken string, idpIDToken string,
@ -89,6 +93,8 @@ func NewSucceededEvent(
SucceededEventType, SucceededEventType,
), ),
IDPUser: idpUser, IDPUser: idpUser,
IDPUserID: idpUserID,
IDPUserName: idpUserName,
UserID: userID, UserID: userID,
IDPAccessToken: idpAccessToken, IDPAccessToken: idpAccessToken,
IDPIDToken: idpIDToken, IDPIDToken: idpIDToken,

View File

@ -5,14 +5,41 @@ package zitadel.user.v2alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha;user"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha;user";
import "google/api/field_behavior.proto"; import "google/api/field_behavior.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";
message IDPInformation{ message IDPInformation{
oneof access{ oneof access{
IDPOAuthAccessInformation oauth = 1; IDPOAuthAccessInformation oauth = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "OAuth/OIDC access (and id_token) returned by the identity provider"
}
];
} }
bytes idp_information = 2; string idp_id = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "ID of the identity provider"
example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
}
];
string user_id = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "ID of the user of the identity provider"
example: "\"6516849804890468048461403518\"";
}
];
string user_name = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "username of the user of the identity provider"
example: "\"user@external.com\"";
}
];
google.protobuf.Struct raw_information = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "complete information returned by the identity provider"
}
];
} }
message IDPOAuthAccessInformation{ message IDPOAuthAccessInformation{
@ -30,22 +57,22 @@ message IDPLink {
example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
} }
]; ];
string idp_external_id = 2 [ string user_id = 2 [
(validate.rules).string = {min_len: 1, max_len: 200}, (validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "ID of user of the identity provider" description: "ID of the user of the identity provider"
min_length: 1; min_length: 1;
max_length: 200; max_length: 200;
example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; example: "\"6516849804890468048461403518\"";
} }
]; ];
string display_name = 3 [ string user_name = 3 [
(validate.rules).string = {min_len: 1, max_len: 200}, (validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Display name of user of the identity provider" description: "username of the user of the identity provider"
min_length: 1; min_length: 1;
max_length: 200; max_length: 200;
example: "\"Firstname Lastname\""; example: "\"user@external.com\"";
} }
]; ];
} }