mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 01:37:31 +00:00
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:
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
177
internal/command/idp_intent.go
Normal file
177
internal/command/idp_intent.go
Normal 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
|
||||
}
|
82
internal/command/idp_intent_model.go
Normal file
82
internal/command/idp_intent_model.go
Normal 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
|
||||
}
|
667
internal/command/idp_intent_test.go
Normal file
667
internal/command/idp_intent_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
|
323
internal/command/idp_model_test.go
Normal file
323
internal/command/idp_model_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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) {
|
||||
|
Reference in New Issue
Block a user