mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 04:57:33 +00:00
feat(actions): add token customization flow and extend functionally with modules (#4337)
* fix: potential memory leak * feat(actions): possibility to parse json feat(actions): possibility to perform http calls * add query call * feat(api): list flow and trigger types fix(api): switch flow and trigger types to dynamic objects * fix(translations): add action translations * use `domain.FlowType` * localizers * localization * trigger types * options on `query.Action` * add functions for actions * feat: management api: add list flow and trigger (#4352) * console changes * cleanup * fix: wrong localization Co-authored-by: Max Peintner <max@caos.ch> * id token works * check if claims not nil * feat(actions): metadata api * refactor(actions): modules * fix: allow prerelease * fix: test * feat(actions): deny list for http hosts * feat(actions): deny list for http hosts * refactor: actions * fix: different error ids * fix: rename statusCode to status * Actions objects as options (#4418) * fix: rename statusCode to status * fix(actions): objects as options * fix(actions): objects as options * fix(actions): set fields * add http client to old actions * fix(actions): add log module * fix(actions): add user to context where possible * fix(actions): add user to ctx in external authorization/pre creation * fix(actions): query correct flow in claims * test: actions * fix(id-generator): panic if no machine id * tests * maybe this? * fix linting * refactor: improve code * fix: metadata and usergrant usage in actions * fix: appendUserGrant * fix: allowedToFail and timeout in action execution * fix: allowed to fail in token complement flow * docs: add action log claim * Update defaults.yaml * fix log claim * remove prerelease build Co-authored-by: Max Peintner <max@caos.ch> Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
@@ -2,10 +2,15 @@ package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/actions"
|
||||
"github.com/zitadel/zitadel/internal/actions/object"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
iam_model "github.com/zitadel/zitadel/internal/iam/model"
|
||||
@@ -24,10 +29,95 @@ func (l *Login) customExternalUserMapping(ctx context.Context, user *domain.Exte
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
actionCtx := (&actions.Context{}).SetToken(tokens)
|
||||
api := (&actions.API{}).SetExternalUser(user).SetMetadata(&user.Metadatas)
|
||||
|
||||
ctxFields := actions.SetContextFields(
|
||||
actions.SetFields("accessToken", tokens.AccessToken),
|
||||
actions.SetFields("idToken", tokens.IDToken),
|
||||
actions.SetFields("getClaim", func(claim string) interface{} {
|
||||
return tokens.IDTokenClaims.GetClaim(claim)
|
||||
}),
|
||||
actions.SetFields("claimsJSON", func() (string, error) {
|
||||
c, err := json.Marshal(tokens.IDTokenClaims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(c), nil
|
||||
}),
|
||||
actions.SetFields("v1",
|
||||
actions.SetFields("externalUser", func(c *actions.FieldConfig) interface{} {
|
||||
return object.UserFromExternalUser(c, user)
|
||||
}),
|
||||
),
|
||||
)
|
||||
apiFields := actions.WithAPIFields(
|
||||
actions.SetFields("setFirstName", func(firstName string) {
|
||||
user.FirstName = firstName
|
||||
}),
|
||||
actions.SetFields("setLastName", func(lastName string) {
|
||||
user.LastName = lastName
|
||||
}),
|
||||
actions.SetFields("setNickName", func(nickName string) {
|
||||
user.NickName = nickName
|
||||
}),
|
||||
actions.SetFields("setDisplayName", func(displayName string) {
|
||||
user.DisplayName = displayName
|
||||
}),
|
||||
actions.SetFields("setPreferredLanguage", func(preferredLanguage string) {
|
||||
user.PreferredLanguage = language.Make(preferredLanguage)
|
||||
}),
|
||||
actions.SetFields("setPreferredUsername", func(username string) {
|
||||
user.PreferredUsername = username
|
||||
}),
|
||||
actions.SetFields("setEmail", func(email string) {
|
||||
user.Email = email
|
||||
}),
|
||||
actions.SetFields("setEmailVerified", func(verified bool) {
|
||||
user.IsEmailVerified = verified
|
||||
}),
|
||||
actions.SetFields("setPhone", func(phone string) {
|
||||
user.Phone = phone
|
||||
}),
|
||||
actions.SetFields("setPhoneVerified", func(verified bool) {
|
||||
user.IsPhoneVerified = verified
|
||||
}),
|
||||
actions.SetFields("metadata", &user.Metadatas),
|
||||
actions.SetFields("v1",
|
||||
actions.SetFields("user",
|
||||
actions.SetFields("appendMetadata", func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) != 2 {
|
||||
panic("exactly 2 (key, value) arguments expected")
|
||||
}
|
||||
key := call.Arguments[0].Export().(string)
|
||||
val := call.Arguments[1].Export()
|
||||
|
||||
value, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
logging.WithError(err).Debug("unable to marshal")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
user.Metadatas = append(user.Metadatas,
|
||||
&domain.Metadata{
|
||||
Key: key,
|
||||
Value: value,
|
||||
})
|
||||
return nil
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
for _, a := range triggerActions {
|
||||
err = actions.Run(actionCtx, api, a.Script, a.Name, a.Timeout, a.AllowedToFail)
|
||||
actionCtx, cancel := context.WithTimeout(ctx, a.Timeout())
|
||||
err = actions.Run(
|
||||
actionCtx,
|
||||
ctxFields,
|
||||
apiFields,
|
||||
a.Script,
|
||||
a.Name,
|
||||
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithLogger(actions.ServerLog))...,
|
||||
)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -40,10 +130,98 @@ func (l *Login) customExternalUserToLoginUserMapping(ctx context.Context, user *
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
actionCtx := (&actions.Context{}).SetToken(tokens)
|
||||
api := (&actions.API{}).SetHuman(user).SetMetadata(&metadata)
|
||||
|
||||
ctxOpts := actions.SetContextFields(
|
||||
actions.SetFields("v1",
|
||||
actions.SetFields("user", func(c *actions.FieldConfig) interface{} {
|
||||
return object.UserFromHuman(c, user)
|
||||
}),
|
||||
),
|
||||
)
|
||||
apiFields := actions.WithAPIFields(
|
||||
actions.SetFields("setFirstName", func(firstName string) {
|
||||
user.FirstName = firstName
|
||||
}),
|
||||
actions.SetFields("setLastName", func(lastName string) {
|
||||
user.LastName = lastName
|
||||
}),
|
||||
actions.SetFields("setNickName", func(nickName string) {
|
||||
user.NickName = nickName
|
||||
}),
|
||||
actions.SetFields("setDisplayName", func(displayName string) {
|
||||
user.DisplayName = displayName
|
||||
}),
|
||||
actions.SetFields("setPreferredLanguage", func(preferredLanguage string) {
|
||||
user.PreferredLanguage = language.Make(preferredLanguage)
|
||||
}),
|
||||
actions.SetFields("setGender", func(gender domain.Gender) {
|
||||
user.Gender = gender
|
||||
}),
|
||||
actions.SetFields("setUsername", func(username string) {
|
||||
user.Username = username
|
||||
}),
|
||||
actions.SetFields("setEmail", func(email string) {
|
||||
if user.Email == nil {
|
||||
user.Email = &domain.Email{}
|
||||
}
|
||||
user.Email.EmailAddress = email
|
||||
}),
|
||||
actions.SetFields("setEmailVerified", func(verified bool) {
|
||||
if user.Email == nil {
|
||||
return
|
||||
}
|
||||
user.Email.IsEmailVerified = verified
|
||||
}),
|
||||
actions.SetFields("setPhone", func(email string) {
|
||||
if user.Phone == nil {
|
||||
user.Phone = &domain.Phone{}
|
||||
}
|
||||
user.Phone.PhoneNumber = email
|
||||
}),
|
||||
actions.SetFields("setPhoneVerified", func(verified bool) {
|
||||
if user.Phone == nil {
|
||||
return
|
||||
}
|
||||
user.Phone.IsPhoneVerified = verified
|
||||
}),
|
||||
actions.SetFields("metadata", metadata),
|
||||
actions.SetFields("v1",
|
||||
actions.SetFields("user",
|
||||
actions.SetFields("appendMetadata", func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) != 2 {
|
||||
panic("exactly 2 (key, value) arguments expected")
|
||||
}
|
||||
key := call.Arguments[0].Export().(string)
|
||||
val := call.Arguments[1].Export()
|
||||
|
||||
value, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
logging.WithError(err).Debug("unable to marshal")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
metadata = append(metadata,
|
||||
&domain.Metadata{
|
||||
Key: key,
|
||||
Value: value,
|
||||
})
|
||||
return nil
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
for _, a := range triggerActions {
|
||||
err = actions.Run(actionCtx, api, a.Script, a.Name, a.Timeout, a.AllowedToFail)
|
||||
actionCtx, cancel := context.WithTimeout(ctx, a.Timeout())
|
||||
err = actions.Run(
|
||||
actionCtx,
|
||||
ctxOpts,
|
||||
apiFields,
|
||||
a.Script,
|
||||
a.Name,
|
||||
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithLogger(actions.ServerLog))...,
|
||||
)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -56,11 +234,78 @@ func (l *Login) customGrants(ctx context.Context, userID string, tokens *oidc.To
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
actionCtx := (&actions.Context{}).SetToken(tokens)
|
||||
|
||||
actionUserGrants := make([]actions.UserGrant, 0)
|
||||
api := (&actions.API{}).SetUserGrants(&actionUserGrants)
|
||||
|
||||
apiFields := actions.WithAPIFields(
|
||||
actions.SetFields("userGrants", &actionUserGrants),
|
||||
actions.SetFields("v1",
|
||||
actions.SetFields("appendUserGrant", func(c *actions.FieldConfig) interface{} {
|
||||
return func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) != 1 {
|
||||
panic("exactly one argument expected")
|
||||
}
|
||||
object := call.Arguments[0].ToObject(c.Runtime)
|
||||
if object == nil {
|
||||
panic("unable to unmarshal arg")
|
||||
}
|
||||
grant := actions.UserGrant{}
|
||||
|
||||
for _, key := range object.Keys() {
|
||||
switch key {
|
||||
case "projectId":
|
||||
grant.ProjectID = object.Get(key).String()
|
||||
case "projectGrantId":
|
||||
grant.ProjectGrantID = object.Get(key).String()
|
||||
case "roles":
|
||||
if roles, ok := object.Get(key).Export().([]interface{}); ok {
|
||||
for _, role := range roles {
|
||||
if r, ok := role.(string); ok {
|
||||
grant.Roles = append(grant.Roles, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if grant.ProjectID == "" {
|
||||
panic("projectId not set")
|
||||
}
|
||||
|
||||
actionUserGrants = append(actionUserGrants, grant)
|
||||
|
||||
return nil
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
for _, a := range triggerActions {
|
||||
err = actions.Run(actionCtx, api, a.Script, a.Name, a.Timeout, a.AllowedToFail)
|
||||
actionCtx, cancel := context.WithTimeout(ctx, a.Timeout())
|
||||
|
||||
ctxFields := actions.SetContextFields(
|
||||
actions.SetFields("v1",
|
||||
actions.SetFields("getUser", func(c *actions.FieldConfig) interface{} {
|
||||
return func(call goja.FunctionCall) goja.Value {
|
||||
user, err := l.query.GetUserByID(actionCtx, true, userID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return object.UserFromQuery(c, user)
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
err = actions.Run(
|
||||
actionCtx,
|
||||
ctxFields,
|
||||
apiFields,
|
||||
a.Script,
|
||||
a.Name,
|
||||
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithLogger(actions.ServerLog))...,
|
||||
)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
caos_errors "github.com/zitadel/zitadel/internal/errors"
|
||||
iam_model "github.com/zitadel/zitadel/internal/iam/model"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
@@ -121,7 +120,7 @@ func (l *Login) handleJWTAuthorize(w http.ResponseWriter, r *http.Request, authR
|
||||
q.Set(QueryAuthRequestID, authReq.ID)
|
||||
userAgentID, ok := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
if !ok {
|
||||
l.renderLogin(w, r, authReq, caos_errors.ThrowPreconditionFailed(nil, "LOGIN-dsgg3", "Errors.AuthRequest.UserAgentNotFound"))
|
||||
l.renderLogin(w, r, authReq, errors.ThrowPreconditionFailed(nil, "LOGIN-dsgg3", "Errors.AuthRequest.UserAgentNotFound"))
|
||||
return
|
||||
}
|
||||
nonce, err := l.idpConfigAlg.Encrypt([]byte(userAgentID))
|
||||
@@ -166,7 +165,7 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
|
||||
l.handleExternalUserAuthenticated(w, r, authReq, idpConfig, userAgentID, tokens)
|
||||
return
|
||||
}
|
||||
l.renderError(w, r, authReq, caos_errors.ThrowPreconditionFailed(nil, "RP-asff2", "Errors.ExternalIDP.IDPTypeNotImplemented"))
|
||||
l.renderError(w, r, authReq, errors.ThrowPreconditionFailed(nil, "RP-asff2", "Errors.ExternalIDP.IDPTypeNotImplemented"))
|
||||
}
|
||||
|
||||
func (l *Login) getRPConfig(ctx context.Context, idpConfig *iam_model.IDPConfigView, callbackEndpoint string) (rp.RelyingParty, error) {
|
||||
@@ -178,7 +177,7 @@ func (l *Login) getRPConfig(ctx context.Context, idpConfig *iam_model.IDPConfigV
|
||||
return rp.NewRelyingPartyOIDC(idpConfig.OIDCIssuer, idpConfig.OIDCClientID, oidcClientSecret, l.baseURL(ctx)+callbackEndpoint, idpConfig.OIDCScopes, rp.WithVerifierOpts(rp.WithIssuedAtOffset(3*time.Second)))
|
||||
}
|
||||
if idpConfig.OAuthAuthorizationEndpoint == "" || idpConfig.OAuthTokenEndpoint == "" {
|
||||
return nil, caos_errors.ThrowPreconditionFailed(nil, "RP-4n0fs", "Errors.IdentityProvider.InvalidConfig")
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "RP-4n0fs", "Errors.IdentityProvider.InvalidConfig")
|
||||
}
|
||||
oauth2Config := &oauth2.Config{
|
||||
ClientID: idpConfig.OIDCClientID,
|
||||
@@ -361,7 +360,7 @@ func (l *Login) handleAutoRegister(w http.ResponseWriter, r *http.Request, authR
|
||||
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
if len(authReq.LinkingUsers) == 0 {
|
||||
l.renderError(w, r, authReq, caos_errors.ThrowPreconditionFailed(nil, "LOGIN-asfg3", "Errors.ExternalIDP.NoExternalUserData"))
|
||||
l.renderError(w, r, authReq, errors.ThrowPreconditionFailed(nil, "LOGIN-asfg3", "Errors.ExternalIDP.NoExternalUserData"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -407,19 +406,19 @@ func (l *Login) handleAutoRegister(w http.ResponseWriter, r *http.Request, authR
|
||||
}
|
||||
|
||||
func (l *Login) mapExternalNotFoundOptionFormDataToLoginUser(formData *externalNotFoundOptionFormData) *domain.ExternalUser {
|
||||
isEmailVerified := formData.externalRegisterFormData.ExternalEmailVerified && formData.externalRegisterFormData.Email == formData.externalRegisterFormData.ExternalEmail
|
||||
isPhoneVerified := formData.externalRegisterFormData.ExternalPhoneVerified && formData.externalRegisterFormData.Phone == formData.externalRegisterFormData.ExternalPhone
|
||||
isEmailVerified := formData.ExternalEmailVerified && formData.Email == formData.ExternalEmail
|
||||
isPhoneVerified := formData.ExternalPhoneVerified && formData.Phone == formData.ExternalPhone
|
||||
return &domain.ExternalUser{
|
||||
IDPConfigID: formData.externalRegisterFormData.ExternalIDPConfigID,
|
||||
ExternalUserID: formData.externalRegisterFormData.ExternalIDPExtUserID,
|
||||
PreferredUsername: formData.externalRegisterFormData.Username,
|
||||
DisplayName: formData.externalRegisterFormData.Email,
|
||||
FirstName: formData.externalRegisterFormData.Firstname,
|
||||
LastName: formData.externalRegisterFormData.Lastname,
|
||||
NickName: formData.externalRegisterFormData.Nickname,
|
||||
Email: formData.externalRegisterFormData.Email,
|
||||
IDPConfigID: formData.ExternalIDPConfigID,
|
||||
ExternalUserID: formData.ExternalIDPExtUserID,
|
||||
PreferredUsername: formData.Username,
|
||||
DisplayName: formData.Email,
|
||||
FirstName: formData.Firstname,
|
||||
LastName: formData.Lastname,
|
||||
NickName: formData.Nickname,
|
||||
Email: formData.Email,
|
||||
IsEmailVerified: isEmailVerified,
|
||||
Phone: formData.externalRegisterFormData.Phone,
|
||||
Phone: formData.Phone,
|
||||
IsPhoneVerified: isPhoneVerified,
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user