mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 18:17:35 +00:00
test: add sink functionality for idp intents (#9116)
# Which Problems Are Solved New integration tests can't use command side to simulate successful intents. # How the Problems Are Solved Add endpoints to only in integration tests available sink to create already successful intents. # Additional Changes None # Additional Context Closes #8557 --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
@@ -526,101 +526,20 @@ func (i *Instance) AddSAMLPostProvider(ctx context.Context) string {
|
||||
return resp.GetId()
|
||||
}
|
||||
|
||||
/*
|
||||
func (s *Instance) CreateIntent(t *testing.T, ctx context.Context, idpID string) string {
|
||||
resp, err := i.Client.UserV2.StartIdentityProviderIntent(ctx, &user.StartIdentityProviderIntentRequest{
|
||||
func (i *Instance) CreateIntent(ctx context.Context, idpID string) *user_v2.StartIdentityProviderIntentResponse {
|
||||
resp, err := i.Client.UserV2.StartIdentityProviderIntent(ctx, &user_v2.StartIdentityProviderIntentRequest{
|
||||
IdpId: idpID,
|
||||
Content: &user.StartIdentityProviderIntentRequest_Urls{
|
||||
Urls: &user.RedirectURLs{
|
||||
Content: &user_v2.StartIdentityProviderIntentRequest_Urls{
|
||||
Urls: &user_v2.RedirectURLs{
|
||||
SuccessUrl: "https://example.com/success",
|
||||
FailureUrl: "https://example.com/failure",
|
||||
},
|
||||
AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME,
|
||||
},
|
||||
})
|
||||
logging.OnError(err).Fatal("create generic OAuth idp")
|
||||
return resp
|
||||
}
|
||||
|
||||
func (i *Instance) CreateIntent(t *testing.T, ctx context.Context, idpID string) string {
|
||||
ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance)
|
||||
writeModel, _, err := s.Commands.CreateIntent(ctx, idpID, "https://example.com/success", "https://example.com/failure", s.Instance.InstanceID())
|
||||
require.NoError(t, err)
|
||||
return writeModel.AggregateID
|
||||
}
|
||||
|
||||
func (i *Instance) CreateSuccessfulOAuthIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) {
|
||||
ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance)
|
||||
intentID := s.CreateIntent(t, ctx, idpID)
|
||||
writeModel, err := s.Commands.GetIntentWriteModel(ctx, intentID, s.Instance.InstanceID())
|
||||
require.NoError(t, err)
|
||||
idpUser := openid.NewUser(
|
||||
&oidc.UserInfo{
|
||||
Subject: idpUserID,
|
||||
UserInfoProfile: oidc.UserInfoProfile{
|
||||
PreferredUsername: "username",
|
||||
},
|
||||
},
|
||||
)
|
||||
idpSession := &openid.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: &oauth2.Token{
|
||||
AccessToken: "accessToken",
|
||||
},
|
||||
IDToken: "idToken",
|
||||
},
|
||||
}
|
||||
token, err := s.Commands.SucceedIDPIntent(ctx, writeModel, idpUser, idpSession, userID)
|
||||
require.NoError(t, err)
|
||||
return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence
|
||||
}
|
||||
|
||||
func (s *Instance) CreateSuccessfulLDAPIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) {
|
||||
ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance)
|
||||
intentID := s.CreateIntent(t, ctx, idpID)
|
||||
writeModel, err := s.Commands.GetIntentWriteModel(ctx, intentID, s.Instance.InstanceID())
|
||||
require.NoError(t, err)
|
||||
username := "username"
|
||||
lang := language.Make("en")
|
||||
idpUser := ldap.NewUser(
|
||||
idpUserID,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
username,
|
||||
"",
|
||||
false,
|
||||
"",
|
||||
false,
|
||||
lang,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
attributes := map[string][]string{"id": {idpUserID}, "username": {username}, "language": {lang.String()}}
|
||||
token, err := s.Commands.SucceedLDAPIDPIntent(ctx, writeModel, idpUser, userID, attributes)
|
||||
require.NoError(t, err)
|
||||
return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence
|
||||
}
|
||||
|
||||
func (s *Instance) CreateSuccessfulSAMLIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) {
|
||||
ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance)
|
||||
intentID := s.CreateIntent(t, ctx, idpID)
|
||||
writeModel, err := s.Server.Commands.GetIntentWriteModel(ctx, intentID, s.Instance.InstanceID())
|
||||
require.NoError(t, err)
|
||||
|
||||
idpUser := &saml.UserMapper{
|
||||
ID: idpUserID,
|
||||
Attributes: map[string][]string{"attribute1": {"value1"}},
|
||||
}
|
||||
assertion := &crewjam_saml.Assertion{ID: "id"}
|
||||
|
||||
token, err := s.Server.Commands.SucceedSAMLIDPIntent(ctx, writeModel, idpUser, userID, assertion)
|
||||
require.NoError(t, err)
|
||||
return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence
|
||||
}
|
||||
*/
|
||||
|
||||
func (i *Instance) CreateVerifiedWebAuthNSession(t *testing.T, ctx context.Context, userID string) (id, token string, start, change time.Time) {
|
||||
return i.CreateVerifiedWebAuthNSessionWithLifetime(t, ctx, userID, 0)
|
||||
}
|
||||
|
@@ -3,6 +3,9 @@
|
||||
package sink
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -10,11 +13,23 @@ import (
|
||||
"path"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
crewjam_saml "github.com/crewjam/saml"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
|
||||
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/saml"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -33,6 +48,60 @@ func CallURL(ch Channel) string {
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) {
|
||||
u := url.URL{
|
||||
Scheme: "http",
|
||||
Host: host,
|
||||
Path: successfulIntentOAuthPath(),
|
||||
}
|
||||
resp, err := callIntent(u.String(), &SuccessfulIntentRequest{
|
||||
InstanceID: instanceID,
|
||||
IDPID: idpID,
|
||||
IDPUserID: idpUserID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", time.Time{}, uint64(0), err
|
||||
}
|
||||
return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil
|
||||
}
|
||||
|
||||
func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) {
|
||||
u := url.URL{
|
||||
Scheme: "http",
|
||||
Host: host,
|
||||
Path: successfulIntentSAMLPath(),
|
||||
}
|
||||
resp, err := callIntent(u.String(), &SuccessfulIntentRequest{
|
||||
InstanceID: instanceID,
|
||||
IDPID: idpID,
|
||||
IDPUserID: idpUserID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", time.Time{}, uint64(0), err
|
||||
}
|
||||
return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil
|
||||
}
|
||||
|
||||
func SuccessfulLDAPIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) {
|
||||
u := url.URL{
|
||||
Scheme: "http",
|
||||
Host: host,
|
||||
Path: successfulIntentLDAPPath(),
|
||||
}
|
||||
resp, err := callIntent(u.String(), &SuccessfulIntentRequest{
|
||||
InstanceID: instanceID,
|
||||
IDPID: idpID,
|
||||
IDPUserID: idpUserID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", time.Time{}, uint64(0), err
|
||||
}
|
||||
return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil
|
||||
}
|
||||
|
||||
// StartServer starts a simple HTTP server on localhost:8081
|
||||
// ZITADEL can use the server to send HTTP requests which can be
|
||||
// used to validate tests through [Subscribe]rs.
|
||||
@@ -41,7 +110,7 @@ func CallURL(ch Channel) string {
|
||||
// [CallURL] can be used to obtain the full URL for a given Channel.
|
||||
//
|
||||
// This function is only active when the `integration` build tag is enabled
|
||||
func StartServer() (close func()) {
|
||||
func StartServer(commands *command.Commands) (close func()) {
|
||||
router := chi.NewRouter()
|
||||
for _, ch := range ChannelValues() {
|
||||
fwd := &forwarder{
|
||||
@@ -50,6 +119,9 @@ func StartServer() (close func()) {
|
||||
}
|
||||
router.HandleFunc(rootPath(ch), fwd.receiveHandler)
|
||||
router.HandleFunc(subscribePath(ch), fwd.subscriptionHandler)
|
||||
router.HandleFunc(successfulIntentOAuthPath(), successfulIntentHandler(commands, createSuccessfulOAuthIntent))
|
||||
router.HandleFunc(successfulIntentSAMLPath(), successfulIntentHandler(commands, createSuccessfulSAMLIntent))
|
||||
router.HandleFunc(successfulIntentLDAPPath(), successfulIntentHandler(commands, createSuccessfulLDAPIntent))
|
||||
}
|
||||
s := &http.Server{
|
||||
Addr: listenAddr,
|
||||
@@ -76,6 +148,26 @@ func subscribePath(c Channel) string {
|
||||
return path.Join("/", c.String(), "subscribe")
|
||||
}
|
||||
|
||||
func intentPath() string {
|
||||
return path.Join("/", "intent")
|
||||
}
|
||||
|
||||
func successfulIntentPath() string {
|
||||
return path.Join(intentPath(), "/", "successful")
|
||||
}
|
||||
|
||||
func successfulIntentOAuthPath() string {
|
||||
return path.Join(successfulIntentPath(), "/", "oauth")
|
||||
}
|
||||
|
||||
func successfulIntentSAMLPath() string {
|
||||
return path.Join(successfulIntentPath(), "/", "saml")
|
||||
}
|
||||
|
||||
func successfulIntentLDAPPath() string {
|
||||
return path.Join(successfulIntentPath(), "/", "ldap")
|
||||
}
|
||||
|
||||
// forwarder handles incoming HTTP requests from ZITADEL and
|
||||
// forwards them to all subscribed web sockets.
|
||||
type forwarder struct {
|
||||
@@ -165,3 +257,165 @@ func readLoop(ws *websocket.Conn) (done chan error) {
|
||||
|
||||
return done
|
||||
}
|
||||
|
||||
type SuccessfulIntentRequest struct {
|
||||
InstanceID string `json:"instance_id"`
|
||||
IDPID string `json:"idp_id"`
|
||||
IDPUserID string `json:"idp_user_id"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
type SuccessfulIntentResponse struct {
|
||||
IntentID string `json:"intent_id"`
|
||||
Token string `json:"token"`
|
||||
ChangeDate time.Time `json:"change_date"`
|
||||
Sequence uint64 `json:"sequence"`
|
||||
}
|
||||
|
||||
func callIntent(url string, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) {
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := http.Post(url, "application/json", io.NopCloser(bytes.NewReader(data)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New(string(body))
|
||||
}
|
||||
result := new(SuccessfulIntentResponse)
|
||||
if err := json.Unmarshal(body, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func successfulIntentHandler(cmd *command.Commands, createIntent func(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error)) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
req := &SuccessfulIntentRequest{}
|
||||
if err := json.Unmarshal(body, req); err != nil {
|
||||
}
|
||||
|
||||
ctx := authz.WithInstanceID(r.Context(), req.InstanceID)
|
||||
resp, err := createIntent(ctx, cmd, req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Write(data)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func createIntent(ctx context.Context, cmd *command.Commands, instanceID, idpID string) (string, error) {
|
||||
writeModel, _, err := cmd.CreateIntent(ctx, idpID, "https://example.com/success", "https://example.com/failure", instanceID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return writeModel.AggregateID, nil
|
||||
}
|
||||
|
||||
func createSuccessfulOAuthIntent(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) {
|
||||
intentID, err := createIntent(ctx, cmd, req.InstanceID, req.IDPID)
|
||||
writeModel, err := cmd.GetIntentWriteModel(ctx, intentID, req.InstanceID)
|
||||
idpUser := openid.NewUser(
|
||||
&oidc.UserInfo{
|
||||
Subject: req.IDPUserID,
|
||||
UserInfoProfile: oidc.UserInfoProfile{
|
||||
PreferredUsername: "username",
|
||||
},
|
||||
},
|
||||
)
|
||||
idpSession := &openid.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: &oauth2.Token{
|
||||
AccessToken: "accessToken",
|
||||
},
|
||||
IDToken: "idToken",
|
||||
},
|
||||
}
|
||||
token, err := cmd.SucceedIDPIntent(ctx, writeModel, idpUser, idpSession, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SuccessfulIntentResponse{
|
||||
intentID,
|
||||
token,
|
||||
writeModel.ChangeDate,
|
||||
writeModel.ProcessedSequence,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func createSuccessfulSAMLIntent(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) {
|
||||
intentID, err := createIntent(ctx, cmd, req.InstanceID, req.IDPID)
|
||||
writeModel, err := cmd.GetIntentWriteModel(ctx, intentID, req.InstanceID)
|
||||
|
||||
idpUser := &saml.UserMapper{
|
||||
ID: req.IDPUserID,
|
||||
Attributes: map[string][]string{"attribute1": {"value1"}},
|
||||
}
|
||||
assertion := &crewjam_saml.Assertion{ID: "id"}
|
||||
|
||||
token, err := cmd.SucceedSAMLIDPIntent(ctx, writeModel, idpUser, req.UserID, assertion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SuccessfulIntentResponse{
|
||||
intentID,
|
||||
token,
|
||||
writeModel.ChangeDate,
|
||||
writeModel.ProcessedSequence,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func createSuccessfulLDAPIntent(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) {
|
||||
intentID, err := createIntent(ctx, cmd, req.InstanceID, req.IDPID)
|
||||
writeModel, err := cmd.GetIntentWriteModel(ctx, intentID, req.InstanceID)
|
||||
username := "username"
|
||||
lang := language.Make("en")
|
||||
idpUser := ldap.NewUser(
|
||||
req.IDPUserID,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
username,
|
||||
"",
|
||||
false,
|
||||
"",
|
||||
false,
|
||||
lang,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
attributes := map[string][]string{"id": {req.IDPUserID}, "username": {username}, "language": {lang.String()}}
|
||||
token, err := cmd.SucceedLDAPIDPIntent(ctx, writeModel, idpUser, req.UserID, attributes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SuccessfulIntentResponse{
|
||||
intentID,
|
||||
token,
|
||||
writeModel.ChangeDate,
|
||||
writeModel.ProcessedSequence,
|
||||
}, nil
|
||||
}
|
||||
|
@@ -2,8 +2,10 @@
|
||||
|
||||
package sink
|
||||
|
||||
import "github.com/zitadel/zitadel/internal/command"
|
||||
|
||||
// StartServer and its returned close function are a no-op
|
||||
// when the `integration` build tag is disabled.
|
||||
func StartServer() (close func()) {
|
||||
func StartServer(cmd *command.Commands) (close func()) {
|
||||
return func() {}
|
||||
}
|
||||
|
Reference in New Issue
Block a user