feat: v2alpha user service idp endpoints (#5879)

* feat: v2alpha user service idp endpoints

* feat: v2alpha user service intent endpoints

* begin idp intents (callback)

* some cleanup

* runnable idp authentication

* cleanup

* proto cleanup

* retrieve idp info

* improve success and failure handling

* some unit tests

* grpc unit tests

* add permission check AddUserIDPLink

* feat: v2alpha intent writemodel refactoring

* feat: v2alpha intent writemodel refactoring

* feat: v2alpha intent writemodel refactoring

* provider from write model

* fix idp type model and add integration tests

* proto cleanup

* fix integration test

* add missing import

* add more integration tests

* auth url test

* feat: v2alpha intent writemodel refactoring

* remove unused functions

* check token on RetrieveIdentityProviderInformation

* feat: v2alpha intent writemodel refactoring

* fix TestServer_RetrieveIdentityProviderInformation

* fix test

* i18n and linting

* feat: v2alpha intent review changes

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
This commit is contained in:
Stefan Benz
2023-05-24 20:29:58 +02:00
committed by GitHub
parent 767b3d7e65
commit fa8f191812
35 changed files with 3560 additions and 19 deletions

View File

@@ -15,6 +15,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/repository/action"
"github.com/zitadel/zitadel/internal/repository/idpintent"
instance_repo "github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/keypair"
"github.com/zitadel/zitadel/internal/repository/org"
@@ -122,6 +123,7 @@ func StartCommands(
action.RegisterEventMappers(repo.eventstore)
quota.RegisterEventMappers(repo.eventstore)
session.RegisterEventMappers(repo.eventstore)
idpintent.RegisterEventMappers(repo.eventstore)
repo.userPasswordAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost)
repo.machineKeySize = int(defaults.SecretGenerators.MachineKeySize)

View File

@@ -6,6 +6,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/repository/idp"
)
@@ -139,3 +140,37 @@ func ExistsIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id,
}
return instanceWriteModel.State.Exists(), nil
}
func IDPProviderWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer, id string) (_ *AllIDPWriteModel, err error) {
writeModel := NewIDPTypeWriteModel(id)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
if len(events) == 0 {
return nil, errors.ThrowPreconditionFailed(nil, "COMMAND-as02jin", "Errors.IDPConfig.NotExisting")
}
writeModel.AppendEvents(events...)
if err := writeModel.Reduce(); err != nil {
return nil, err
}
allWriteModel, err := NewAllIDPWriteModel(
writeModel.ResourceOwner,
writeModel.ResourceOwner == writeModel.InstanceID,
writeModel.ID,
writeModel.Type,
)
if err != nil {
return nil, err
}
events, err = filter(ctx, allWriteModel.Query())
if err != nil {
return nil, err
}
allWriteModel.AppendEvents(events...)
if err := allWriteModel.Reduce(); err != nil {
return nil, err
}
return allWriteModel, err
}

View File

@@ -0,0 +1,177 @@
package command
import (
"context"
"encoding/base64"
"encoding/json"
"net/url"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/jwt"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
"github.com/zitadel/zitadel/internal/repository/idpintent"
)
func (c *Commands) prepareCreateIntent(writeModel *IDPIntentWriteModel, idpID string, successURL, failureURL string) preparation.Validation {
return func() (_ preparation.CreateCommands, err error) {
if idpID == "" {
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-x8j2bk", "Errors.Intent.IDPMissing")
}
successURL, err := url.Parse(successURL)
if err != nil {
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-x8j3bk", "Errors.Intent.SuccessURLMissing")
}
failureURL, err := url.Parse(failureURL)
if err != nil {
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-x8j4bk", "Errors.Intent.FailureURLMissing")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
err = getIDPIntentWriteModel(ctx, writeModel, filter)
if err != nil {
return nil, err
}
exists, err := ExistsIDP(ctx, filter, idpID, writeModel.ResourceOwner)
if !exists || err != nil {
return nil, errors.ThrowPreconditionFailed(err, "COMMAND-39n221fs", "Errors.IDPConfig.NotExisting")
}
return []eventstore.Command{
idpintent.NewStartedEvent(ctx, writeModel.aggregate, successURL, failureURL, idpID),
}, nil
}, nil
}
}
func (c *Commands) CreateIntent(ctx context.Context, idpID, successURL, failureURL, resourceOwner string) (string, *domain.ObjectDetails, error) {
id, err := c.idGenerator.Next()
if err != nil {
return "", nil, err
}
writeModel := NewIDPIntentWriteModel(id, resourceOwner)
if err != nil {
return "", nil, err
}
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareCreateIntent(writeModel, idpID, successURL, failureURL))
if err != nil {
return "", nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return "", nil, err
}
err = AppendAndReduce(writeModel, pushedEvents...)
if err != nil {
return "", nil, err
}
return id, writeModelToObjectDetails(&writeModel.WriteModel), nil
}
func (c *Commands) GetProvider(ctx context.Context, idpID, callbackURL string) (idp.Provider, error) {
writeModel, err := IDPProviderWriteModel(ctx, c.eventstore.Filter, idpID)
if err != nil {
return nil, err
}
return writeModel.ToProvider(callbackURL, c.idpConfigEncryption)
}
func (c *Commands) AuthURLFromProvider(ctx context.Context, idpID, state, callbackURL string) (string, error) {
provider, err := c.GetProvider(ctx, idpID, callbackURL)
if err != nil {
return "", err
}
session, err := provider.BeginAuth(ctx, state)
if err != nil {
return "", err
}
return session.GetAuthURL(), nil
}
func getIDPIntentWriteModel(ctx context.Context, writeModel *IDPIntentWriteModel, filter preparation.FilterToQueryReducer) error {
events, err := filter(ctx, writeModel.Query())
if err != nil {
return err
}
if len(events) == 0 {
return nil
}
writeModel.AppendEvents(events...)
return writeModel.Reduce()
}
func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, idpSession idp.Session, userID string) (string, error) {
token, err := c.idpConfigEncryption.Encrypt([]byte(writeModel.AggregateID))
if err != nil {
return "", err
}
accessToken, idToken, err := tokensForSucceededIDPIntent(idpSession, c.idpConfigEncryption)
if err != nil {
return "", err
}
idpInfo, err := json.Marshal(idpUser)
if err != nil {
return "", err
}
cmd, err := idpintent.NewSucceededEvent(
ctx,
&idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate,
idpInfo,
userID,
accessToken,
idToken,
)
if err != nil {
return "", err
}
err = c.pushAppendAndReduce(ctx, writeModel, cmd)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(token), nil
}
func (c *Commands) FailIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, reason string) error {
cmd := idpintent.NewFailedEvent(
ctx,
&idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate,
reason,
)
_, err := c.eventstore.Push(ctx, cmd)
return err
}
func (c *Commands) GetIntentWriteModel(ctx context.Context, id, resourceOwner string) (*IDPIntentWriteModel, error) {
writeModel := NewIDPIntentWriteModel(id, resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
return writeModel, err
}
// tokensForSucceededIDPIntent extracts the oidc.Tokens if available (and encrypts the access_token) for the succeeded event payload
func tokensForSucceededIDPIntent(session idp.Session, encryptionAlg crypto.EncryptionAlgorithm) (*crypto.CryptoValue, string, error) {
var tokens *oidc.Tokens[*oidc.IDTokenClaims]
switch s := session.(type) {
case *oauth.Session:
tokens = s.Tokens
case *openid.Session:
tokens = s.Tokens
case *jwt.Session:
tokens = s.Tokens
default:
return nil, "", nil
}
if tokens.Token == nil || tokens.AccessToken == "" {
return nil, tokens.IDToken, nil
}
accessToken, err := crypto.Encrypt([]byte(tokens.AccessToken), encryptionAlg)
return accessToken, tokens.IDToken, err
}

View File

@@ -0,0 +1,82 @@
package command
import (
"net/url"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/idpintent"
)
type IDPIntentWriteModel struct {
eventstore.WriteModel
SuccessURL *url.URL
FailureURL *url.URL
IDPID string
IDPUser []byte
IDPAccessToken *crypto.CryptoValue
IDPIDToken string
UserID string
State domain.IDPIntentState
aggregate *eventstore.Aggregate
}
func NewIDPIntentWriteModel(id, resourceOwner string) *IDPIntentWriteModel {
return &IDPIntentWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: id,
ResourceOwner: resourceOwner,
},
aggregate: &idpintent.NewAggregate(id, resourceOwner).Aggregate,
}
}
func (wm *IDPIntentWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *idpintent.StartedEvent:
wm.reduceStartedEvent(e)
case *idpintent.SucceededEvent:
wm.reduceSucceededEvent(e)
case *idpintent.FailedEvent:
wm.reduceFailedEvent(e)
}
}
return wm.WriteModel.Reduce()
}
func (wm *IDPIntentWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(idpintent.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
idpintent.StartedEventType,
idpintent.SucceededEventType,
idpintent.FailedEventType,
).
Builder()
}
func (wm *IDPIntentWriteModel) reduceStartedEvent(e *idpintent.StartedEvent) {
wm.SuccessURL = e.SuccessURL
wm.FailureURL = e.FailureURL
wm.IDPID = e.IDPID
wm.State = domain.IDPIntentStateStarted
}
func (wm *IDPIntentWriteModel) reduceSucceededEvent(e *idpintent.SucceededEvent) {
wm.UserID = e.UserID
wm.IDPUser = e.IDPUser
wm.IDPAccessToken = e.IDPAccessToken
wm.IDPIDToken = e.IDPIDToken
wm.State = domain.IDPIntentStateSucceeded
}
func (wm *IDPIntentWriteModel) reduceFailedEvent(e *idpintent.FailedEvent) {
wm.State = domain.IDPIntentStateFailed
}

View File

@@ -0,0 +1,667 @@
package command
import (
"context"
"net/url"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v2/pkg/oidc"
"golang.org/x/oauth2"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
z_errors "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/id/mock"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/jwt"
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
rep_idp "github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/repository/idpintent"
"github.com/zitadel/zitadel/internal/repository/instance"
)
func TestCommands_CreateIntent(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
idGenerator id.Generator
}
type args struct {
ctx context.Context
idpID string
successURL string
failureURL string
resourceOwner string
}
type res struct {
intentID string
details *domain.ObjectDetails
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"error no id generator",
fields{
eventstore: eventstoreExpect(t),
idGenerator: mock.NewIDGeneratorExpectError(t, z_errors.ThrowInternal(nil, "", "error id")),
},
args{
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
idpID: "",
successURL: "https://success.url",
failureURL: "https://failure.url",
},
res{
err: z_errors.ThrowInternal(nil, "", "error id"),
},
},
{
"error no idpID",
fields{
eventstore: eventstoreExpect(t),
idGenerator: mock.ExpectID(t, "id"),
},
args{
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
idpID: "",
successURL: "https://success.url",
failureURL: "https://failure.url",
},
res{
err: z_errors.ThrowInvalidArgument(nil, "COMMAND-x8j2bk", "Errors.Intent.IDPMissing"),
},
},
{
"error no successURL",
fields{
eventstore: eventstoreExpect(t),
idGenerator: mock.ExpectID(t, "id"),
},
args{
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
idpID: "idp",
successURL: ":",
failureURL: "https://failure.url",
},
res{
err: z_errors.ThrowInvalidArgument(nil, "COMMAND-x8j3bk", "Errors.Intent.SuccessURLMissing"),
},
},
{
"error no failureURL",
fields{
eventstore: eventstoreExpect(t),
idGenerator: mock.ExpectID(t, "id"),
},
args{
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
idpID: "idp",
successURL: "https://success.url",
failureURL: ":",
},
res{
err: z_errors.ThrowInvalidArgument(nil, "COMMAND-x8j4bk", "Errors.Intent.FailureURLMissing"),
},
},
{
"error idp not existing",
fields{
eventstore: eventstoreExpect(t,
expectFilter(),
expectFilter(),
expectFilter(),
),
idGenerator: mock.ExpectID(t, "id"),
},
args{
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
idpID: "idp",
successURL: "https://success.url",
failureURL: "https://failure.url",
},
res{
err: z_errors.ThrowPreconditionFailed(nil, "COMMAND-39n221fs", "Errors.IDPConfig.NotExisting"),
},
},
{
"push",
fields{
eventstore: eventstoreExpect(t,
expectFilter(),
expectFilter(),
expectFilter(
eventFromEventPusher(
instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("ro").Aggregate,
"idp",
"name",
"clientID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("clientSecret"),
},
"auth",
"token",
"user",
"idAttribute",
nil,
rep_idp.Options{},
)),
),
expectPush(
eventPusherToEvents(
func() eventstore.Command {
success, _ := url.Parse("https://success.url")
failure, _ := url.Parse("https://failure.url")
return idpintent.NewStartedEvent(
context.Background(),
&idpintent.NewAggregate("id", "ro").Aggregate,
success,
failure,
"idp",
)
}(),
),
),
),
idGenerator: mock.ExpectID(t, "id"),
},
args{
ctx: context.Background(),
resourceOwner: "ro",
idpID: "idp",
successURL: "https://success.url",
failureURL: "https://failure.url",
},
res{
intentID: "id",
details: &domain.ObjectDetails{ResourceOwner: "ro"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
idGenerator: tt.fields.idGenerator,
}
intentID, details, err := c.CreateIntent(tt.args.ctx, tt.args.idpID, tt.args.successURL, tt.args.failureURL, tt.args.resourceOwner)
require.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.intentID, intentID)
assert.Equal(t, tt.res.details, details)
})
}
}
func TestCommands_AuthURLFromProvider(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
secretCrypto crypto.EncryptionAlgorithm
}
type args struct {
ctx context.Context
idpID string
state string
callbackURL string
}
type res struct {
authURL string
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"idp not existing",
fields{
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
eventstore: eventstoreExpect(t,
expectFilter(),
),
},
args{
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
idpID: "idp",
state: "state",
callbackURL: "url",
},
res{
err: z_errors.ThrowPreconditionFailed(nil, "", ""),
},
},
{
"idp removed",
fields{
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusherWithInstanceID(
"instance",
instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
"idp",
"name",
"clientID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("clientSecret"),
},
"auth",
"token",
"user",
"idAttribute",
nil,
rep_idp.Options{},
)),
eventFromEventPusherWithInstanceID(
"instance",
instance.NewIDPRemovedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
"idp",
),
),
),
),
},
args{
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
idpID: "idp",
state: "state",
callbackURL: "url",
},
res{
err: z_errors.ThrowInternal(nil, "COMMAND-xw921211", "Errors.IDPConfig.NotExisting"),
},
},
{
"push",
fields{
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusherWithInstanceID(
"instance",
instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
"idp",
"name",
"clientID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("clientSecret"),
},
"auth",
"token",
"user",
"idAttribute",
nil,
rep_idp.Options{},
)),
),
expectFilter(
eventFromEventPusherWithInstanceID(
"instance",
instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
"idp",
"name",
"clientID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("clientSecret"),
},
"auth",
"token",
"user",
"idAttribute",
nil,
rep_idp.Options{},
)),
),
),
},
args{
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
idpID: "idp",
state: "state",
callbackURL: "url",
},
res{
authURL: "auth?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&state=state",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
idpConfigEncryption: tt.fields.secretCrypto,
}
authURL, err := c.AuthURLFromProvider(tt.args.ctx, tt.args.idpID, tt.args.state, tt.args.callbackURL)
require.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.authURL, authURL)
})
}
}
func TestCommands_SucceedIDPIntent(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
idpConfigEncryption crypto.EncryptionAlgorithm
}
type args struct {
ctx context.Context
writeModel *IDPIntentWriteModel
idpUser idp.User
idpSession idp.Session
userID string
}
type res struct {
token string
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"encryption fails",
fields{
idpConfigEncryption: func() crypto.EncryptionAlgorithm {
m := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))
m.EXPECT().Encrypt(gomock.Any()).Return(nil, z_errors.ThrowInternal(nil, "id", "encryption failed"))
return m
}(),
},
args{
ctx: context.Background(),
writeModel: NewIDPIntentWriteModel("id", "ro"),
},
res{
err: z_errors.ThrowInternal(nil, "id", "encryption failed"),
},
},
{
"token encryption fails",
fields{
idpConfigEncryption: func() crypto.EncryptionAlgorithm {
m := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))
m.EXPECT().Encrypt(gomock.Any()).DoAndReturn(func(value []byte) ([]byte, error) {
return value, nil
})
m.EXPECT().Encrypt(gomock.Any()).Return(nil, z_errors.ThrowInternal(nil, "id", "encryption failed"))
return m
}(),
},
args{
ctx: context.Background(),
writeModel: NewIDPIntentWriteModel("id", "ro"),
idpSession: &oauth.Session{
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{
AccessToken: "accessToken",
},
},
},
},
res{
err: z_errors.ThrowInternal(nil, "id", "encryption failed"),
},
},
{
"push",
fields{
idpConfigEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
eventstore: eventstoreExpect(t,
expectPush(
eventPusherToEvents(
func() eventstore.Command {
event, _ := idpintent.NewSucceededEvent(
context.Background(),
&idpintent.NewAggregate("id", "ro").Aggregate,
[]byte(`{"RawInfo":{"id":"id"}}`),
"",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("accessToken"),
},
"",
)
return event
}(),
),
),
),
},
args{
ctx: context.Background(),
writeModel: NewIDPIntentWriteModel("id", "ro"),
idpSession: &oauth.Session{
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{
AccessToken: "accessToken",
},
},
},
idpUser: &oauth.UserMapper{
RawInfo: map[string]interface{}{
"id": "id",
},
},
},
res{
token: "aWQ",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
idpConfigEncryption: tt.fields.idpConfigEncryption,
}
got, err := c.SucceedIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.idpSession, tt.args.userID)
require.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.token, got)
})
}
}
func TestCommands_FailIDPIntent(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
writeModel *IDPIntentWriteModel
reason string
}
type res struct {
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"push",
fields{
eventstore: eventstoreExpect(t,
expectPush(
eventPusherToEvents(
idpintent.NewFailedEvent(
context.Background(),
&idpintent.NewAggregate("id", "ro").Aggregate,
"reason",
),
),
),
),
},
args{
ctx: context.Background(),
writeModel: NewIDPIntentWriteModel("id", "ro"),
reason: "reason",
},
res{
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
}
err := c.FailIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.reason)
require.ErrorIs(t, err, tt.res.err)
})
}
}
func Test_tokensForSucceededIDPIntent(t *testing.T) {
type args struct {
session idp.Session
encryptionAlg crypto.EncryptionAlgorithm
}
type res struct {
accessToken *crypto.CryptoValue
idToken string
err error
}
tests := []struct {
name string
args args
res res
}{
{
"no tokens",
args{
&ldap.Session{},
crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
res{
accessToken: nil,
idToken: "",
err: nil,
},
},
{
"token encryption fails",
args{
&oauth.Session{
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{
AccessToken: "accessToken",
},
},
},
func() crypto.EncryptionAlgorithm {
m := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))
m.EXPECT().Encrypt(gomock.Any()).Return(nil, z_errors.ThrowInternal(nil, "id", "encryption failed"))
return m
}(),
},
res{
accessToken: nil,
idToken: "",
err: z_errors.ThrowInternal(nil, "id", "encryption failed"),
},
},
{
"oauth tokens",
args{
&oauth.Session{
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{
AccessToken: "accessToken",
},
},
},
crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
res{
accessToken: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("accessToken"),
},
idToken: "",
err: nil,
},
},
{
"oidc tokens",
args{
&openid.Session{
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{
AccessToken: "accessToken",
},
IDToken: "idToken",
},
},
crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
res{
accessToken: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("accessToken"),
},
idToken: "idToken",
err: nil,
},
},
{
"jwt tokens",
args{
&jwt.Session{
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
IDToken: "idToken",
},
},
crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
res{
accessToken: nil,
idToken: "idToken",
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotAccessToken, gotIDToken, err := tokensForSucceededIDPIntent(tt.args.session, tt.args.encryptionAlg)
require.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.accessToken, gotAccessToken)
assert.Equal(t, tt.res.idToken, gotIDToken)
})
}
}

View File

@@ -1,14 +1,31 @@
package command
import (
"net/http"
"reflect"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v2/pkg/client/rp"
"golang.org/x/oauth2"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
providers "github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/azuread"
"github.com/zitadel/zitadel/internal/idp/providers/github"
"github.com/zitadel/zitadel/internal/idp/providers/gitlab"
"github.com/zitadel/zitadel/internal/idp/providers/google"
"github.com/zitadel/zitadel/internal/idp/providers/jwt"
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
"github.com/zitadel/zitadel/internal/idp/providers/oidc"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/repository/idpconfig"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
)
type OAuthIDPWriteModel struct {
@@ -133,6 +150,45 @@ func (wm *OAuthIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *OAuthIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
}
config := &oauth2.Config{
ClientID: wm.ClientID,
ClientSecret: secret,
Endpoint: oauth2.Endpoint{
AuthURL: wm.AuthorizationEndpoint,
TokenURL: wm.TokenEndpoint,
},
RedirectURL: callbackURL,
Scopes: wm.Scopes,
}
opts := make([]oauth.ProviderOpts, 0, 4)
if wm.IsCreationAllowed {
opts = append(opts, oauth.WithCreationAllowed())
}
if wm.IsLinkingAllowed {
opts = append(opts, oauth.WithLinkingAllowed())
}
if wm.IsAutoCreation {
opts = append(opts, oauth.WithAutoCreation())
}
if wm.IsAutoUpdate {
opts = append(opts, oauth.WithAutoUpdate())
}
return oauth.New(
config,
wm.Name,
wm.UserEndpoint,
func() providers.User {
return oauth.NewUserMapper(wm.IDAttribute)
},
opts...,
)
}
type OIDCIDPWriteModel struct {
eventstore.WriteModel
@@ -286,6 +342,40 @@ func (wm *OIDCIDPWriteModel) reduceOIDCConfigChangedEvent(e *idpconfig.OIDCConfi
}
}
func (wm *OIDCIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
}
opts := make([]oidc.ProviderOpts, 1, 6)
opts[0] = oidc.WithSelectAccount()
if wm.IsIDTokenMapping {
opts = append(opts, oidc.WithIDTokenMapping())
}
if wm.IsCreationAllowed {
opts = append(opts, oidc.WithCreationAllowed())
}
if wm.IsLinkingAllowed {
opts = append(opts, oidc.WithLinkingAllowed())
}
if wm.IsAutoCreation {
opts = append(opts, oidc.WithAutoCreation())
}
if wm.IsAutoUpdate {
opts = append(opts, oidc.WithAutoUpdate())
}
return oidc.New(
wm.Name,
wm.Issuer,
wm.ClientID,
secret,
callbackURL,
wm.Scopes,
oidc.DefaultMapper,
opts...,
)
}
type JWTIDPWriteModel struct {
eventstore.WriteModel
@@ -423,6 +513,31 @@ func (wm *JWTIDPWriteModel) reduceJWTConfigChangedEvent(e *idpconfig.JWTConfigCh
}
}
func (wm *JWTIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
opts := make([]jwt.ProviderOpts, 0)
if wm.IsCreationAllowed {
opts = append(opts, jwt.WithCreationAllowed())
}
if wm.IsLinkingAllowed {
opts = append(opts, jwt.WithLinkingAllowed())
}
if wm.IsAutoCreation {
opts = append(opts, jwt.WithAutoCreation())
}
if wm.IsAutoUpdate {
opts = append(opts, jwt.WithAutoUpdate())
}
return jwt.New(
wm.Name,
wm.Issuer,
wm.JWTEndpoint,
wm.KeysEndpoint,
wm.HeaderName,
idpAlg,
opts...,
)
}
type AzureADIDPWriteModel struct {
eventstore.WriteModel
@@ -527,6 +642,43 @@ func (wm *AzureADIDPWriteModel) NewChanges(
}
return changes, nil
}
func (wm *AzureADIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
}
opts := make([]azuread.ProviderOptions, 0, 3)
if wm.IsEmailVerified {
opts = append(opts, azuread.WithEmailVerified())
}
if wm.Tenant != "" {
opts = append(opts, azuread.WithTenant(azuread.TenantType(wm.Tenant)))
}
oauthOpts := make([]oauth.ProviderOpts, 0, 4)
if wm.IsCreationAllowed {
oauthOpts = append(oauthOpts, oauth.WithCreationAllowed())
}
if wm.IsLinkingAllowed {
oauthOpts = append(oauthOpts, oauth.WithLinkingAllowed())
}
if wm.IsAutoCreation {
oauthOpts = append(oauthOpts, oauth.WithAutoCreation())
}
if wm.IsAutoUpdate {
oauthOpts = append(oauthOpts, oauth.WithAutoUpdate())
}
if len(oauthOpts) > 0 {
opts = append(opts, azuread.WithOAuthOptions(oauthOpts...))
}
return azuread.New(
wm.Name,
wm.ClientID,
secret,
callbackURL,
wm.Scopes,
opts...,
)
}
type GitHubIDPWriteModel struct {
eventstore.WriteModel
@@ -614,6 +766,32 @@ func (wm *GitHubIDPWriteModel) NewChanges(
}
return changes, nil
}
func (wm *GitHubIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
}
oauthOpts := make([]oauth.ProviderOpts, 0, 4)
if wm.IsCreationAllowed {
oauthOpts = append(oauthOpts, oauth.WithCreationAllowed())
}
if wm.IsLinkingAllowed {
oauthOpts = append(oauthOpts, oauth.WithLinkingAllowed())
}
if wm.IsAutoCreation {
oauthOpts = append(oauthOpts, oauth.WithAutoCreation())
}
if wm.IsAutoUpdate {
oauthOpts = append(oauthOpts, oauth.WithAutoUpdate())
}
return github.New(
wm.ClientID,
secret,
callbackURL,
wm.Scopes,
oauthOpts...,
)
}
type GitHubEnterpriseIDPWriteModel struct {
eventstore.WriteModel
@@ -728,6 +906,37 @@ func (wm *GitHubEnterpriseIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *GitHubEnterpriseIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
}
oauthOpts := make([]oauth.ProviderOpts, 0, 4)
if wm.IsCreationAllowed {
oauthOpts = append(oauthOpts, oauth.WithCreationAllowed())
}
if wm.IsLinkingAllowed {
oauthOpts = append(oauthOpts, oauth.WithLinkingAllowed())
}
if wm.IsAutoCreation {
oauthOpts = append(oauthOpts, oauth.WithAutoCreation())
}
if wm.IsAutoUpdate {
oauthOpts = append(oauthOpts, oauth.WithAutoUpdate())
}
return github.NewCustomURL(
wm.Name,
wm.ClientID,
secret,
callbackURL,
wm.AuthorizationEndpoint,
wm.TokenEndpoint,
wm.UserEndpoint,
wm.Scopes,
oauthOpts...,
)
}
type GitLabIDPWriteModel struct {
eventstore.WriteModel
@@ -815,6 +1024,33 @@ func (wm *GitLabIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *GitLabIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
}
opts := make([]oidc.ProviderOpts, 0, 4)
if wm.IsCreationAllowed {
opts = append(opts, oidc.WithCreationAllowed())
}
if wm.IsLinkingAllowed {
opts = append(opts, oidc.WithLinkingAllowed())
}
if wm.IsAutoCreation {
opts = append(opts, oidc.WithAutoCreation())
}
if wm.IsAutoUpdate {
opts = append(opts, oidc.WithAutoUpdate())
}
return gitlab.New(
wm.ClientID,
secret,
callbackURL,
wm.Scopes,
opts...,
)
}
type GitLabSelfHostedIDPWriteModel struct {
eventstore.WriteModel
@@ -910,6 +1146,35 @@ func (wm *GitLabSelfHostedIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *GitLabSelfHostedIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
}
opts := make([]oidc.ProviderOpts, 0, 4)
if wm.IsCreationAllowed {
opts = append(opts, oidc.WithCreationAllowed())
}
if wm.IsLinkingAllowed {
opts = append(opts, oidc.WithLinkingAllowed())
}
if wm.IsAutoCreation {
opts = append(opts, oidc.WithAutoCreation())
}
if wm.IsAutoUpdate {
opts = append(opts, oidc.WithAutoUpdate())
}
return gitlab.NewCustomIssuer(
wm.Name,
wm.Issuer,
wm.ClientID,
secret,
callbackURL,
wm.Scopes,
opts...,
)
}
type GoogleIDPWriteModel struct {
eventstore.WriteModel
@@ -997,6 +1262,38 @@ func (wm *GoogleIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *GoogleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
errorHandler := func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
logging.Errorf("token exchanged failed: %s - %s (state: %s)", errorType, errorType, state)
rp.DefaultErrorHandler(w, r, errorType, errorDesc, state)
}
oidc.WithRelyingPartyOption(rp.WithErrorHandler(errorHandler))
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
}
opts := make([]oidc.ProviderOpts, 0, 4)
if wm.IsCreationAllowed {
opts = append(opts, oidc.WithCreationAllowed())
}
if wm.IsLinkingAllowed {
opts = append(opts, oidc.WithLinkingAllowed())
}
if wm.IsAutoCreation {
opts = append(opts, oidc.WithAutoCreation())
}
if wm.IsAutoUpdate {
opts = append(opts, oidc.WithAutoUpdate())
}
return google.New(
wm.ClientID,
secret,
callbackURL,
wm.Scopes,
opts...,
)
}
type LDAPIDPWriteModel struct {
eventstore.WriteModel
@@ -1157,6 +1454,81 @@ func (wm *LDAPIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *LDAPIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
password, err := crypto.DecryptString(wm.BindPassword, idpAlg)
if err != nil {
return nil, err
}
var opts []ldap.ProviderOpts
if !wm.StartTLS {
opts = append(opts, ldap.WithoutStartTLS())
}
if wm.LDAPAttributes.IDAttribute != "" {
opts = append(opts, ldap.WithCustomIDAttribute(wm.LDAPAttributes.IDAttribute))
}
if wm.LDAPAttributes.FirstNameAttribute != "" {
opts = append(opts, ldap.WithFirstNameAttribute(wm.LDAPAttributes.FirstNameAttribute))
}
if wm.LDAPAttributes.LastNameAttribute != "" {
opts = append(opts, ldap.WithLastNameAttribute(wm.LDAPAttributes.LastNameAttribute))
}
if wm.LDAPAttributes.DisplayNameAttribute != "" {
opts = append(opts, ldap.WithDisplayNameAttribute(wm.LDAPAttributes.DisplayNameAttribute))
}
if wm.LDAPAttributes.NickNameAttribute != "" {
opts = append(opts, ldap.WithNickNameAttribute(wm.LDAPAttributes.NickNameAttribute))
}
if wm.LDAPAttributes.PreferredUsernameAttribute != "" {
opts = append(opts, ldap.WithPreferredUsernameAttribute(wm.LDAPAttributes.PreferredUsernameAttribute))
}
if wm.LDAPAttributes.EmailAttribute != "" {
opts = append(opts, ldap.WithEmailAttribute(wm.LDAPAttributes.EmailAttribute))
}
if wm.LDAPAttributes.EmailVerifiedAttribute != "" {
opts = append(opts, ldap.WithEmailVerifiedAttribute(wm.LDAPAttributes.EmailVerifiedAttribute))
}
if wm.LDAPAttributes.PhoneAttribute != "" {
opts = append(opts, ldap.WithPhoneAttribute(wm.LDAPAttributes.PhoneAttribute))
}
if wm.LDAPAttributes.PhoneVerifiedAttribute != "" {
opts = append(opts, ldap.WithPhoneVerifiedAttribute(wm.LDAPAttributes.PhoneVerifiedAttribute))
}
if wm.LDAPAttributes.PreferredLanguageAttribute != "" {
opts = append(opts, ldap.WithPreferredLanguageAttribute(wm.LDAPAttributes.PreferredLanguageAttribute))
}
if wm.LDAPAttributes.AvatarURLAttribute != "" {
opts = append(opts, ldap.WithAvatarURLAttribute(wm.LDAPAttributes.AvatarURLAttribute))
}
if wm.LDAPAttributes.ProfileAttribute != "" {
opts = append(opts, ldap.WithProfileAttribute(wm.LDAPAttributes.ProfileAttribute))
}
if wm.IsCreationAllowed {
opts = append(opts, ldap.WithCreationAllowed())
}
if wm.IsLinkingAllowed {
opts = append(opts, ldap.WithLinkingAllowed())
}
if wm.IsAutoCreation {
opts = append(opts, ldap.WithAutoCreation())
}
if wm.IsAutoUpdate {
opts = append(opts, ldap.WithAutoUpdate())
}
return ldap.New(
wm.Name,
wm.Servers,
wm.BaseDN,
wm.BindDN,
password,
wm.UserBase,
wm.UserObjectClasses,
wm.UserFilters,
wm.Timeout,
callbackURL,
opts...,
), nil
}
type IDPRemoveWriteModel struct {
eventstore.WriteModel
@@ -1211,3 +1583,252 @@ func (wm *IDPRemoveWriteModel) reduceRemoved(id string) {
}
wm.State = domain.IDPStateRemoved
}
type IDPTypeWriteModel struct {
eventstore.WriteModel
ID string
Type domain.IDPType
State domain.IDPState
}
func NewIDPTypeWriteModel(id string) *IDPTypeWriteModel {
return &IDPTypeWriteModel{
ID: id,
}
}
func (wm *IDPTypeWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *instance.OAuthIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeOAuth, e.Aggregate())
case *org.OAuthIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeOAuth, e.Aggregate())
case *instance.OIDCIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeOIDC, e.Aggregate())
case *org.OIDCIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeOIDC, e.Aggregate())
case *instance.JWTIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeJWT, e.Aggregate())
case *org.JWTIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeJWT, e.Aggregate())
case *instance.AzureADIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeAzureAD, e.Aggregate())
case *org.AzureADIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeAzureAD, e.Aggregate())
case *instance.GitHubIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeGitHub, e.Aggregate())
case *org.GitHubIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeGitHub, e.Aggregate())
case *instance.GitHubEnterpriseIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeGitHubEnterprise, e.Aggregate())
case *org.GitHubEnterpriseIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeGitHubEnterprise, e.Aggregate())
case *instance.GitLabIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeGitLab, e.Aggregate())
case *org.GitLabIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeGitLab, e.Aggregate())
case *instance.GitLabSelfHostedIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeGitLabSelfHosted, e.Aggregate())
case *org.GitLabSelfHostedIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeGitLabSelfHosted, e.Aggregate())
case *instance.GoogleIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeGoogle, e.Aggregate())
case *org.GoogleIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeGoogle, e.Aggregate())
case *instance.LDAPIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate())
case *org.LDAPIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate())
case *instance.IDPRemovedEvent:
wm.reduceRemoved(e.ID)
case *org.IDPRemovedEvent:
wm.reduceRemoved(e.ID)
case *instance.IDPConfigAddedEvent:
if e.Typ == domain.IDPConfigTypeOIDC {
wm.reduceAdded(e.ConfigID, domain.IDPTypeOIDC, e.Aggregate())
} else if e.Typ == domain.IDPConfigTypeJWT {
wm.reduceAdded(e.ConfigID, domain.IDPTypeJWT, e.Aggregate())
}
case *org.IDPConfigAddedEvent:
if e.Typ == domain.IDPConfigTypeOIDC {
wm.reduceAdded(e.ConfigID, domain.IDPTypeOIDC, e.Aggregate())
} else if e.Typ == domain.IDPConfigTypeJWT {
wm.reduceAdded(e.ConfigID, domain.IDPTypeJWT, e.Aggregate())
}
case *instance.IDPConfigRemovedEvent:
wm.reduceRemoved(e.ConfigID)
case *org.IDPConfigRemovedEvent:
wm.reduceRemoved(e.ConfigID)
}
}
return wm.WriteModel.Reduce()
}
func (wm *IDPTypeWriteModel) reduceAdded(id string, t domain.IDPType, agg eventstore.Aggregate) {
if wm.ID != id {
return
}
wm.Type = t
wm.State = domain.IDPStateActive
wm.ResourceOwner = agg.ResourceOwner
wm.InstanceID = agg.InstanceID
}
func (wm *IDPTypeWriteModel) reduceRemoved(id string) {
if wm.ID != id {
return
}
wm.Type = domain.IDPTypeUnspecified
wm.State = domain.IDPStateRemoved
wm.ResourceOwner = ""
wm.InstanceID = ""
}
func (wm *IDPTypeWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AddQuery().
AggregateTypes(instance.AggregateType).
EventTypes(
instance.OAuthIDPAddedEventType,
instance.OIDCIDPAddedEventType,
instance.JWTIDPAddedEventType,
instance.AzureADIDPAddedEventType,
instance.GitHubIDPAddedEventType,
instance.GitHubEnterpriseIDPAddedEventType,
instance.GitLabIDPAddedEventType,
instance.GitLabSelfHostedIDPAddedEventType,
instance.GoogleIDPAddedEventType,
instance.LDAPIDPAddedEventType,
instance.IDPRemovedEventType,
).
EventData(map[string]interface{}{"id": wm.ID}).
Or().
AggregateTypes(org.AggregateType).
EventTypes(
org.OAuthIDPAddedEventType,
org.OIDCIDPAddedEventType,
org.JWTIDPAddedEventType,
org.AzureADIDPAddedEventType,
org.GitHubIDPAddedEventType,
org.GitHubEnterpriseIDPAddedEventType,
org.GitLabIDPAddedEventType,
org.GitLabSelfHostedIDPAddedEventType,
org.GoogleIDPAddedEventType,
org.LDAPIDPAddedEventType,
org.IDPRemovedEventType,
).
EventData(map[string]interface{}{"id": wm.ID}).
Or(). // old events
AggregateTypes(instance.AggregateType).
EventTypes(
instance.IDPConfigAddedEventType,
instance.IDPConfigRemovedEventType,
).
EventData(map[string]interface{}{"idpConfigId": wm.ID}).
Or().
AggregateTypes(org.AggregateType).
EventTypes(
org.IDPConfigAddedEventType,
org.IDPConfigRemovedEventType,
).
EventData(map[string]interface{}{"idpConfigId": wm.ID}).
Builder()
}
type IDP interface {
eventstore.QueryReducer
ToProvider(string, crypto.EncryptionAlgorithm) (providers.Provider, error)
}
type AllIDPWriteModel struct {
model IDP
ID string
IDPType domain.IDPType
ResourceOwner string
Instance bool
}
func NewAllIDPWriteModel(resourceOwner string, instanceBool bool, id string, idpType domain.IDPType) (*AllIDPWriteModel, error) {
writeModel := &AllIDPWriteModel{
ID: id,
IDPType: idpType,
ResourceOwner: resourceOwner,
Instance: instanceBool,
}
if instanceBool {
switch idpType {
case domain.IDPTypeOIDC:
writeModel.model = NewOIDCInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeJWT:
writeModel.model = NewJWTInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeOAuth:
writeModel.model = NewOAuthInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeLDAP:
writeModel.model = NewLDAPInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeAzureAD:
writeModel.model = NewAzureADInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeGitHub:
writeModel.model = NewGitHubInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeGitHubEnterprise:
writeModel.model = NewGitHubEnterpriseInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeGitLab:
writeModel.model = NewGitLabInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeGitLabSelfHosted:
writeModel.model = NewGitLabSelfHostedInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeGoogle:
writeModel.model = NewGoogleInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeUnspecified:
fallthrough
default:
return nil, errors.ThrowInternal(nil, "COMMAND-xw921211", "Errors.IDPConfig.NotExisting")
}
} else {
switch idpType {
case domain.IDPTypeOIDC:
writeModel.model = NewOIDCOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeJWT:
writeModel.model = NewJWTOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeOAuth:
writeModel.model = NewOAuthOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeLDAP:
writeModel.model = NewLDAPOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeAzureAD:
writeModel.model = NewAzureADOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeGitHub:
writeModel.model = NewGitHubOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeGitHubEnterprise:
writeModel.model = NewGitHubEnterpriseOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeGitLab:
writeModel.model = NewGitLabOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeGitLabSelfHosted:
writeModel.model = NewGitLabSelfHostedOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeGoogle:
writeModel.model = NewGoogleOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeUnspecified:
fallthrough
default:
return nil, errors.ThrowInternal(nil, "COMMAND-xw921111", "Errors.IDPConfig.NotExisting")
}
}
return writeModel, nil
}
func (wm *AllIDPWriteModel) Reduce() error {
return wm.model.Reduce()
}
func (wm *AllIDPWriteModel) Query() *eventstore.SearchQueryBuilder {
return wm.model.Query()
}
func (wm *AllIDPWriteModel) AppendEvents(events ...eventstore.Event) {
wm.model.AppendEvents(events...)
}
func (wm *AllIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
return wm.model.ToProvider(callbackURL, idpAlg)
}

View File

@@ -0,0 +1,323 @@
package command
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
)
func TestCommands_AllIDPWriteModel(t *testing.T) {
type args struct {
resourceOwner string
instanceBool bool
id string
idpType domain.IDPType
}
type res struct {
writeModelType interface{}
err error
}
tests := []struct {
name string
args args
res res
}{
{
name: "writemodel instance oidc",
args: args{
resourceOwner: "owner",
instanceBool: true,
id: "id",
idpType: domain.IDPTypeOIDC,
},
res: res{
writeModelType: &InstanceOIDCIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel instance jwt",
args: args{
resourceOwner: "owner",
instanceBool: true,
id: "id",
idpType: domain.IDPTypeJWT,
},
res: res{
writeModelType: &InstanceJWTIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel instance oauth",
args: args{
resourceOwner: "owner",
instanceBool: true,
id: "id",
idpType: domain.IDPTypeOAuth,
},
res: res{
writeModelType: &InstanceOAuthIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel instance ldap",
args: args{
resourceOwner: "owner",
instanceBool: true,
id: "id",
idpType: domain.IDPTypeLDAP,
},
res: res{
writeModelType: &InstanceLDAPIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel instance azureAD",
args: args{
resourceOwner: "owner",
instanceBool: true,
id: "id",
idpType: domain.IDPTypeAzureAD,
},
res: res{
writeModelType: &InstanceAzureADIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel instance github",
args: args{
resourceOwner: "owner",
instanceBool: true,
id: "id",
idpType: domain.IDPTypeGitHub,
},
res: res{
writeModelType: &InstanceGitHubIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel instance github enterprise",
args: args{
resourceOwner: "owner",
instanceBool: true,
id: "id",
idpType: domain.IDPTypeGitHubEnterprise,
},
res: res{
writeModelType: &InstanceGitHubEnterpriseIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel instance gitlab",
args: args{
resourceOwner: "owner",
instanceBool: true,
id: "id",
idpType: domain.IDPTypeGitLab,
},
res: res{
writeModelType: &InstanceGitLabIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel instance gitlab self hosted",
args: args{
resourceOwner: "owner",
instanceBool: true,
id: "id",
idpType: domain.IDPTypeGitLabSelfHosted,
},
res: res{
writeModelType: &InstanceGitLabSelfHostedIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel instance google",
args: args{
resourceOwner: "owner",
instanceBool: true,
id: "id",
idpType: domain.IDPTypeGoogle,
},
res: res{
writeModelType: &InstanceGoogleIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel instance unspecified",
args: args{
resourceOwner: "owner",
instanceBool: true,
id: "id",
idpType: domain.IDPTypeUnspecified,
},
res: res{
err: errors.ThrowInternal(nil, "COMMAND-xw921211", "Errors.IDPConfig.NotExisting"),
},
},
{
name: "writemodel org oidc",
args: args{
resourceOwner: "owner",
instanceBool: false,
id: "id",
idpType: domain.IDPTypeOIDC,
},
res: res{
writeModelType: &OrgOIDCIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel org jwt",
args: args{
resourceOwner: "owner",
instanceBool: false,
id: "id",
idpType: domain.IDPTypeJWT,
},
res: res{
writeModelType: &OrgJWTIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel org oauth",
args: args{
resourceOwner: "owner",
instanceBool: false,
id: "id",
idpType: domain.IDPTypeOAuth,
},
res: res{
writeModelType: &OrgOAuthIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel org ldap",
args: args{
resourceOwner: "owner",
instanceBool: false,
id: "id",
idpType: domain.IDPTypeLDAP,
},
res: res{
writeModelType: &OrgLDAPIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel org azureAD",
args: args{
resourceOwner: "owner",
instanceBool: false,
id: "id",
idpType: domain.IDPTypeAzureAD,
},
res: res{
writeModelType: &OrgAzureADIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel org github",
args: args{
resourceOwner: "owner",
instanceBool: false,
id: "id",
idpType: domain.IDPTypeGitHub,
},
res: res{
writeModelType: &OrgGitHubIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel org github enterprise",
args: args{
resourceOwner: "owner",
instanceBool: false,
id: "id",
idpType: domain.IDPTypeGitHubEnterprise,
},
res: res{
writeModelType: &OrgGitHubEnterpriseIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel org gitlab",
args: args{
resourceOwner: "owner",
instanceBool: false,
id: "id",
idpType: domain.IDPTypeGitLab,
},
res: res{
writeModelType: &OrgGitLabIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel org gitlab self hosted",
args: args{
resourceOwner: "owner",
instanceBool: false,
id: "id",
idpType: domain.IDPTypeGitLabSelfHosted,
},
res: res{
writeModelType: &OrgGitLabSelfHostedIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel org google",
args: args{
resourceOwner: "owner",
instanceBool: false,
id: "id",
idpType: domain.IDPTypeGoogle,
},
res: res{
writeModelType: &OrgGoogleIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel org unspecified",
args: args{
resourceOwner: "owner",
instanceBool: false,
id: "id",
idpType: domain.IDPTypeUnspecified,
},
res: res{
err: errors.ThrowInternal(nil, "COMMAND-xw921111", "Errors.IDPConfig.NotExisting"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wm, err := NewAllIDPWriteModel(tt.args.resourceOwner, tt.args.instanceBool, tt.args.id, tt.args.idpType)
require.ErrorIs(t, err, tt.res.err)
if wm != nil {
assert.IsType(t, tt.res.writeModelType, wm.model)
}
})
}
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/eventstore/repository/mock"
action_repo "github.com/zitadel/zitadel/internal/repository/action"
"github.com/zitadel/zitadel/internal/repository/idpintent"
iam_repo "github.com/zitadel/zitadel/internal/repository/instance"
key_repo "github.com/zitadel/zitadel/internal/repository/keypair"
"github.com/zitadel/zitadel/internal/repository/org"
@@ -41,6 +42,7 @@ func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore {
key_repo.RegisterEventMappers(es)
action_repo.RegisterEventMappers(es)
session.RegisterEventMappers(es)
idpintent.RegisterEventMappers(es)
return es
}

View File

@@ -58,6 +58,9 @@ type AddHuman struct {
Register bool
Metadata []*AddMetadataEntry
// Links are optional
Links []*AddLink
// Details are set after a successful execution of the command
Details *domain.ObjectDetails
@@ -65,6 +68,12 @@ type AddHuman struct {
EmailCode *string
}
type AddLink struct {
IDPID string
DisplayName string
IDPExternalID string
}
func (h *AddHuman) Validate() (err error) {
if err := h.Email.Validate(); err != nil {
return err
@@ -226,6 +235,13 @@ func (c *Commands) AddHumanCommand(human *AddHuman, orgID string, passwordAlg cr
metadataEntry.Value,
))
}
for _, link := range human.Links {
cmd, err := addLink(ctx, filter, a, link)
if err != nil {
return nil, err
}
cmds = append(cmds, cmd)
}
return cmds, nil
}, nil
@@ -260,6 +276,15 @@ func (c *Commands) addHumanCommandEmail(ctx context.Context, filter preparation.
}
return cmds, nil
}
func addLink(ctx context.Context, filter preparation.FilterToQueryReducer, a *user.Aggregate, link *AddLink) (eventstore.Command, error) {
exists, err := ExistsIDP(ctx, filter, link.IDPID, a.ResourceOwner)
if !exists || err != nil {
return nil, errors.ThrowPreconditionFailed(err, "COMMAND-39nf2", "Errors.IDPConfig.NotExisting")
}
return user.NewUserIDPLinkAddedEvent(ctx, &a.Aggregate, link.IDPID, link.DisplayName, link.IDPExternalID), nil
}
func (c *Commands) addHumanCommandPhone(ctx context.Context, filter preparation.FilterToQueryReducer, cmds []eventstore.Command, a *user.Aggregate, human *AddHuman, codeAlg crypto.EncryptionAlgorithm) ([]eventstore.Command, error) {
if human.Phone.Number == "" {
return cmds, nil

View File

@@ -3,20 +3,25 @@ package command
import (
"context"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
func (c *Commands) AddUserIDPLink(ctx context.Context, userID, resourceOwner string, link *domain.UserIDPLink) (err error) {
func (c *Commands) AddUserIDPLink(ctx context.Context, userID, resourceOwner string, link *domain.UserIDPLink) (_ *domain.ObjectDetails, err error) {
if userID == "" {
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-03j8f", "Errors.IDMissing")
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-03j8f", "Errors.IDMissing")
}
if err := c.checkUserExists(ctx, userID, resourceOwner); err != nil {
return err
return nil, err
}
if userID != authz.GetCtxData(ctx).UserID {
if err := c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, userID); err != nil {
return nil, err
}
}
linkWriteModel := NewUserIDPLinkWriteModel(userID, link.IDPConfigID, link.ExternalUserID, resourceOwner)
@@ -24,11 +29,18 @@ func (c *Commands) AddUserIDPLink(ctx context.Context, userID, resourceOwner str
event, err := c.addUserIDPLink(ctx, userAgg, link)
if err != nil {
return err
return nil, err
}
_, err = c.eventstore.Push(ctx, event)
return err
events, err := c.eventstore.Push(ctx, event)
if err != nil {
return nil, err
}
return &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreationDate(),
ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
}, nil
}
func (c *Commands) BulkAddedUserIDPLinks(ctx context.Context, userID, resourceOwner string, links []*domain.UserIDPLink) (err error) {