mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 03:37:34 +00:00
chore: move the go code into a subfolder
This commit is contained in:
9
apps/api/internal/integration/sink/channel.go
Normal file
9
apps/api/internal/integration/sink/channel.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package sink
|
||||
|
||||
//go:generate enumer -type Channel -trimprefix Channel -transform snake
|
||||
type Channel int
|
||||
|
||||
const (
|
||||
ChannelMilestone Channel = iota
|
||||
ChannelQuota
|
||||
)
|
78
apps/api/internal/integration/sink/channel_enumer.go
Normal file
78
apps/api/internal/integration/sink/channel_enumer.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Code generated by "enumer -type Channel -trimprefix Channel -transform snake"; DO NOT EDIT.
|
||||
|
||||
package sink
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _ChannelName = "milestonequota"
|
||||
|
||||
var _ChannelIndex = [...]uint8{0, 9, 14}
|
||||
|
||||
const _ChannelLowerName = "milestonequota"
|
||||
|
||||
func (i Channel) String() string {
|
||||
if i < 0 || i >= Channel(len(_ChannelIndex)-1) {
|
||||
return fmt.Sprintf("Channel(%d)", i)
|
||||
}
|
||||
return _ChannelName[_ChannelIndex[i]:_ChannelIndex[i+1]]
|
||||
}
|
||||
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
func _ChannelNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[ChannelMilestone-(0)]
|
||||
_ = x[ChannelQuota-(1)]
|
||||
}
|
||||
|
||||
var _ChannelValues = []Channel{ChannelMilestone, ChannelQuota}
|
||||
|
||||
var _ChannelNameToValueMap = map[string]Channel{
|
||||
_ChannelName[0:9]: ChannelMilestone,
|
||||
_ChannelLowerName[0:9]: ChannelMilestone,
|
||||
_ChannelName[9:14]: ChannelQuota,
|
||||
_ChannelLowerName[9:14]: ChannelQuota,
|
||||
}
|
||||
|
||||
var _ChannelNames = []string{
|
||||
_ChannelName[0:9],
|
||||
_ChannelName[9:14],
|
||||
}
|
||||
|
||||
// ChannelString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func ChannelString(s string) (Channel, error) {
|
||||
if val, ok := _ChannelNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _ChannelNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to Channel values", s)
|
||||
}
|
||||
|
||||
// ChannelValues returns all values of the enum
|
||||
func ChannelValues() []Channel {
|
||||
return _ChannelValues
|
||||
}
|
||||
|
||||
// ChannelStrings returns a slice of all String values of the enum
|
||||
func ChannelStrings() []string {
|
||||
strs := make([]string, len(_ChannelNames))
|
||||
copy(strs, _ChannelNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsAChannel returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i Channel) IsAChannel() bool {
|
||||
for _, v := range _ChannelValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
551
apps/api/internal/integration/sink/server.go
Normal file
551
apps/api/internal/integration/sink/server.go
Normal file
@@ -0,0 +1,551 @@
|
||||
//go:build integration
|
||||
|
||||
package sink
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
crewjam_saml "github.com/crewjam/saml"
|
||||
"github.com/go-chi/chi/v5"
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
"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"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"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"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/saml"
|
||||
)
|
||||
|
||||
const (
|
||||
port = "8081"
|
||||
listenAddr = "127.0.0.1:" + port
|
||||
host = "localhost:" + port
|
||||
)
|
||||
|
||||
// CallURL returns the full URL to the handler of a [Channel].
|
||||
func CallURL(ch Channel) string {
|
||||
u := url.URL{
|
||||
Scheme: "http",
|
||||
Host: host,
|
||||
Path: rootPath(ch),
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (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,
|
||||
Expiry: expiry,
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", time.Time{}, uint64(0), err
|
||||
}
|
||||
return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil
|
||||
}
|
||||
|
||||
func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) {
|
||||
u := url.URL{
|
||||
Scheme: "http",
|
||||
Host: host,
|
||||
Path: successfulIntentOIDCPath(),
|
||||
}
|
||||
resp, err := callIntent(u.String(), &SuccessfulIntentRequest{
|
||||
InstanceID: instanceID,
|
||||
IDPID: idpID,
|
||||
IDPUserID: idpUserID,
|
||||
UserID: userID,
|
||||
Expiry: expiry,
|
||||
})
|
||||
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, expiry time.Time) (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,
|
||||
Expiry: expiry,
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
func SuccessfulJWTIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) {
|
||||
u := url.URL{
|
||||
Scheme: "http",
|
||||
Host: host,
|
||||
Path: successfulIntentJWTPath(),
|
||||
}
|
||||
resp, err := callIntent(u.String(), &SuccessfulIntentRequest{
|
||||
InstanceID: instanceID,
|
||||
IDPID: idpID,
|
||||
IDPUserID: idpUserID,
|
||||
UserID: userID,
|
||||
Expiry: expiry,
|
||||
})
|
||||
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.
|
||||
// For each [Channel] a route is registered on http://localhost:8081/<channel_name>.
|
||||
// The route must be used to send the HTTP request to be validated.
|
||||
// [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(commands *command.Commands) (close func()) {
|
||||
router := chi.NewRouter()
|
||||
for _, ch := range ChannelValues() {
|
||||
fwd := &forwarder{
|
||||
channelID: ch,
|
||||
subscribers: make(map[int64]chan<- *Request),
|
||||
}
|
||||
router.HandleFunc(rootPath(ch), fwd.receiveHandler)
|
||||
router.HandleFunc(subscribePath(ch), fwd.subscriptionHandler)
|
||||
router.HandleFunc(successfulIntentOAuthPath(), successfulIntentHandler(commands, createSuccessfulOAuthIntent))
|
||||
router.HandleFunc(successfulIntentOIDCPath(), successfulIntentHandler(commands, createSuccessfulOIDCIntent))
|
||||
router.HandleFunc(successfulIntentSAMLPath(), successfulIntentHandler(commands, createSuccessfulSAMLIntent))
|
||||
router.HandleFunc(successfulIntentLDAPPath(), successfulIntentHandler(commands, createSuccessfulLDAPIntent))
|
||||
router.HandleFunc(successfulIntentJWTPath(), successfulIntentHandler(commands, createSuccessfulJWTIntent))
|
||||
}
|
||||
s := &http.Server{
|
||||
Addr: listenAddr,
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
logging.WithFields("listen_addr", listenAddr).Warn("!!!! A sink server is started which may expose sensitive data on a public endpoint. Make sure the `integration` build tag is disabled for production builds. !!!!")
|
||||
go func() {
|
||||
err := s.ListenAndServe()
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
logging.WithError(err).Fatal("sink server")
|
||||
}
|
||||
}()
|
||||
return func() {
|
||||
logging.OnError(s.Close()).Error("sink server")
|
||||
}
|
||||
}
|
||||
|
||||
func rootPath(c Channel) string {
|
||||
return path.Join("/", c.String())
|
||||
}
|
||||
|
||||
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 successfulIntentOIDCPath() string {
|
||||
return path.Join(successfulIntentPath(), "/", "oidc")
|
||||
}
|
||||
|
||||
func successfulIntentSAMLPath() string {
|
||||
return path.Join(successfulIntentPath(), "/", "saml")
|
||||
}
|
||||
|
||||
func successfulIntentLDAPPath() string {
|
||||
return path.Join(successfulIntentPath(), "/", "ldap")
|
||||
}
|
||||
|
||||
func successfulIntentJWTPath() string {
|
||||
return path.Join(successfulIntentPath(), "/", "jwt")
|
||||
}
|
||||
|
||||
// forwarder handles incoming HTTP requests from ZITADEL and
|
||||
// forwards them to all subscribed web sockets.
|
||||
type forwarder struct {
|
||||
channelID Channel
|
||||
id atomic.Int64
|
||||
mtx sync.RWMutex
|
||||
subscribers map[int64]chan<- *Request
|
||||
upgrader websocket.Upgrader
|
||||
}
|
||||
|
||||
// receiveHandler receives a simple HTTP for a single [Channel]
|
||||
// and forwards them on all active subscribers of that Channel.
|
||||
func (c *forwarder) receiveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
req := &Request{
|
||||
Header: r.Header.Clone(),
|
||||
}
|
||||
var err error
|
||||
req.Body, err = io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
c.mtx.RLock()
|
||||
for _, reqChan := range c.subscribers {
|
||||
reqChan <- req
|
||||
}
|
||||
c.mtx.RUnlock()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// subscriptionHandler upgrades HTTP request to a websocket connection for subscribers.
|
||||
// All received HTTP requests on a subscriber's channel are send on the websocket to the client.
|
||||
func (c *forwarder) subscriptionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := c.upgrader.Upgrade(w, r, nil)
|
||||
logging.OnError(err).Error("websocket upgrade")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
done := readLoop(ws)
|
||||
|
||||
id := c.id.Add(1)
|
||||
reqChannel := make(chan *Request, 100)
|
||||
|
||||
c.mtx.Lock()
|
||||
c.subscribers[id] = reqChannel
|
||||
c.mtx.Unlock()
|
||||
|
||||
logging.WithFields("id", id, "channel", c.channelID).Info("websocket opened")
|
||||
|
||||
defer func() {
|
||||
c.mtx.Lock()
|
||||
delete(c.subscribers, id)
|
||||
c.mtx.Unlock()
|
||||
|
||||
ws.Close()
|
||||
close(reqChannel)
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-done:
|
||||
logging.WithError(err).WithFields(logrus.Fields{"id": id, "channel": c.channelID}).Info("websocket closed")
|
||||
return
|
||||
case req := <-reqChannel:
|
||||
if err := ws.WriteJSON(req); err != nil {
|
||||
logging.WithError(err).WithFields(logrus.Fields{"id": id, "channel": c.channelID}).Error("websocket write json")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readLoop makes sure we can receive close messages
|
||||
func readLoop(ws *websocket.Conn) (done chan error) {
|
||||
done = make(chan error, 1)
|
||||
|
||||
go func(done chan<- error) {
|
||||
for {
|
||||
_, _, err := ws.NextReader()
|
||||
if err != nil {
|
||||
done <- err
|
||||
break
|
||||
}
|
||||
}
|
||||
close(done)
|
||||
}(done)
|
||||
|
||||
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"`
|
||||
Expiry time.Time `json:"expiry"`
|
||||
}
|
||||
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, nil)
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
writeModel, err := cmd.GetIntentWriteModel(ctx, intentID, req.InstanceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idAttribute := "id"
|
||||
idpUser := oauth.NewUserMapper(idAttribute)
|
||||
idpUser.RawInfo = map[string]interface{}{
|
||||
idAttribute: req.IDPUserID,
|
||||
"preferred_username": "username",
|
||||
}
|
||||
idpSession := &oauth.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: &oauth2.Token{
|
||||
AccessToken: "accessToken",
|
||||
Expiry: req.Expiry,
|
||||
},
|
||||
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 createSuccessfulOIDCIntent(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",
|
||||
Expiry: req.Expiry,
|
||||
},
|
||||
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"}},
|
||||
}
|
||||
session := &saml.Session{
|
||||
Assertion: &crewjam_saml.Assertion{
|
||||
ID: "id",
|
||||
Conditions: &crewjam_saml.Conditions{
|
||||
NotOnOrAfter: req.Expiry,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
token, err := cmd.SucceedSAMLIDPIntent(ctx, writeModel, idpUser, req.UserID, session)
|
||||
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,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
session := &ldap.Session{Entry: &goldap.Entry{
|
||||
Attributes: []*goldap.EntryAttribute{
|
||||
{Name: "id", Values: []string{req.IDPUserID}},
|
||||
{Name: "username", Values: []string{username}},
|
||||
{Name: "language", Values: []string{lang.String()}},
|
||||
},
|
||||
}}
|
||||
token, err := cmd.SucceedLDAPIDPIntent(ctx, writeModel, idpUser, req.UserID, session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SuccessfulIntentResponse{
|
||||
intentID,
|
||||
token,
|
||||
writeModel.ChangeDate,
|
||||
writeModel.ProcessedSequence,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func createSuccessfulJWTIntent(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 := &jwt.User{
|
||||
IDTokenClaims: &oidc.IDTokenClaims{
|
||||
TokenClaims: oidc.TokenClaims{
|
||||
Subject: req.IDPUserID,
|
||||
},
|
||||
},
|
||||
}
|
||||
session := &jwt.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
IDToken: "idToken",
|
||||
},
|
||||
}
|
||||
token, err := cmd.SucceedIDPIntent(ctx, writeModel, idpUser, session, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SuccessfulIntentResponse{
|
||||
intentID,
|
||||
token,
|
||||
writeModel.ChangeDate,
|
||||
writeModel.ProcessedSequence,
|
||||
}, nil
|
||||
}
|
4
apps/api/internal/integration/sink/sink.go
Normal file
4
apps/api/internal/integration/sink/sink.go
Normal file
@@ -0,0 +1,4 @@
|
||||
// Package sink provides a simple HTTP server where Zitadel can send HTTP based messages,
|
||||
// which are then possible to be observed using observers on websockets.
|
||||
// The contents of this package become available when the `integration` build tag is enabled.
|
||||
package sink
|
11
apps/api/internal/integration/sink/stub.go
Normal file
11
apps/api/internal/integration/sink/stub.go
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build !integration
|
||||
|
||||
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(cmd *command.Commands) (close func()) {
|
||||
return func() {}
|
||||
}
|
90
apps/api/internal/integration/sink/subscription.go
Normal file
90
apps/api/internal/integration/sink/subscription.go
Normal file
@@ -0,0 +1,90 @@
|
||||
//go:build integration
|
||||
|
||||
package sink
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/zitadel/logging"
|
||||
)
|
||||
|
||||
// Request is a message forwarded from the handler to [Subscription]s.
|
||||
type Request struct {
|
||||
Header http.Header
|
||||
Body json.RawMessage
|
||||
}
|
||||
|
||||
// Subscription is a websocket client to which [Request]s are forwarded by the server.
|
||||
type Subscription struct {
|
||||
conn *websocket.Conn
|
||||
closed atomic.Bool
|
||||
reqChannel chan *Request
|
||||
}
|
||||
|
||||
// Subscribe to a channel.
|
||||
// The subscription forwards all requests it received on the channel's
|
||||
// handler, after Subscribe has returned.
|
||||
// Multiple subscription may be active on a single channel.
|
||||
// Each request is always forwarded to each Subscription.
|
||||
// Close must be called to cleanup up the Subscription's channel and go routine.
|
||||
func Subscribe(ctx context.Context, ch Channel) *Subscription {
|
||||
u := url.URL{
|
||||
Scheme: "ws",
|
||||
Host: listenAddr,
|
||||
Path: subscribePath(ch),
|
||||
}
|
||||
conn, resp, err := websocket.DefaultDialer.DialContext(ctx, u.String(), nil)
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
err = fmt.Errorf("subscribe: %w, status: %s, body: %s", err, resp.Status, body)
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sub := &Subscription{
|
||||
conn: conn,
|
||||
reqChannel: make(chan *Request, 10),
|
||||
}
|
||||
go sub.readToChan()
|
||||
return sub
|
||||
}
|
||||
|
||||
func (s *Subscription) readToChan() {
|
||||
for {
|
||||
if s.closed.Load() {
|
||||
break
|
||||
}
|
||||
req := new(Request)
|
||||
if err := s.conn.ReadJSON(req); err != nil {
|
||||
opErr := new(net.OpError)
|
||||
if errors.As(err, &opErr) {
|
||||
break
|
||||
}
|
||||
logging.WithError(err).Error("subscription read")
|
||||
break
|
||||
}
|
||||
s.reqChannel <- req
|
||||
}
|
||||
close(s.reqChannel)
|
||||
}
|
||||
|
||||
// Recv returns the channel over which [Request]s are send.
|
||||
func (s *Subscription) Recv() <-chan *Request {
|
||||
return s.reqChannel
|
||||
}
|
||||
|
||||
func (s *Subscription) Close() error {
|
||||
s.closed.Store(true)
|
||||
return s.conn.Close()
|
||||
}
|
Reference in New Issue
Block a user