feat(actions): local users (#5089)

Actions are extended to to local users. It's possible to run custom code during registration and authentication of local users.
This commit is contained in:
Silvan
2023-01-25 14:08:01 +01:00
committed by GitHub
parent 19621acfd3
commit c54ddc71a2
48 changed files with 704 additions and 188 deletions

View File

@@ -5,7 +5,6 @@ import (
"encoding/json"
"github.com/dop251/goja"
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v2/pkg/oidc"
"golang.org/x/text/language"
@@ -30,25 +29,7 @@ func (l *Login) customExternalUserMapping(ctx context.Context, user *domain.Exte
return nil, err
}
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)
}),
),
)
metadataList := object.MetadataListFromDomain(user.Metadatas)
apiFields := actions.WithAPIFields(
actions.SetFields("setFirstName", func(firstName string) {
user.FirstName = firstName
@@ -80,35 +61,28 @@ func (l *Login) customExternalUserMapping(ctx context.Context, user *domain.Exte
actions.SetFields("setPhoneVerified", func(verified bool) {
user.IsPhoneVerified = verified
}),
actions.SetFields("metadata", &user.Metadatas),
actions.SetFields("metadata", &metadataList.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)
}
user.Metadatas = append(user.Metadatas,
&domain.Metadata{
Key: key,
Value: value,
})
return nil
}),
actions.SetFields("appendMetadata", metadataList.AppendMetadataFunc),
),
),
)
for _, a := range triggerActions {
actionCtx, cancel := context.WithTimeout(ctx, a.Timeout())
ctxFieldOptions := append(tokenCtxFields(tokens),
actions.SetFields("v1",
actions.SetFields("externalUser", func(c *actions.FieldConfig) interface{} {
return object.UserFromExternalUser(c, user)
}),
actions.SetFields("authRequest", object.AuthRequestField(req)),
),
)
ctxFields := actions.SetContextFields(ctxFieldOptions...)
err = actions.Run(
actionCtx,
ctxFields,
@@ -122,22 +96,78 @@ func (l *Login) customExternalUserMapping(ctx context.Context, user *domain.Exte
return nil, err
}
}
user.Metadatas = object.MetadataListToDomain(metadataList)
return user, err
}
func (l *Login) customExternalUserToLoginUserMapping(ctx context.Context, user *domain.Human, tokens *oidc.Tokens, req *domain.AuthRequest, config *iam_model.IDPConfigView, metadata []*domain.Metadata, resourceOwner string) (*domain.Human, []*domain.Metadata, error) {
triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeExternalAuthentication, domain.TriggerTypePreCreation, resourceOwner, false)
type authMethod string
const (
authMethodPassword authMethod = "password"
authMethodOTP authMethod = "OTP"
authMethodU2F authMethod = "U2F"
authMethodPasswordless authMethod = "passwordless"
)
func (l *Login) triggerPostLocalAuthentication(ctx context.Context, req *domain.AuthRequest, authMethod authMethod, authenticationError error) ([]*domain.Metadata, error) {
resourceOwner := req.RequestedOrgID
if resourceOwner == "" {
resourceOwner = req.UserOrgID
}
triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeInternalAuthentication, domain.TriggerTypePostAuthentication, resourceOwner, false)
if err != nil {
return nil, err
}
metadataList := object.MetadataListFromDomain(nil)
apiFields := actions.WithAPIFields(
actions.SetFields("metadata", &metadataList.Metadata),
actions.SetFields("v1",
actions.SetFields("user",
actions.SetFields("appendMetadata", metadataList.AppendMetadataFunc),
),
),
)
for _, a := range triggerActions {
actionCtx, cancel := context.WithTimeout(ctx, a.Timeout())
authErrStr := "none"
if authenticationError != nil {
authErrStr = authenticationError.Error()
}
ctxFields := actions.SetContextFields(
actions.SetFields("v1",
actions.SetFields("authMethod", authMethod),
actions.SetFields("authError", authErrStr),
actions.SetFields("authRequest", object.AuthRequestField(req)),
),
)
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
}
}
return object.MetadataListToDomain(metadataList), err
}
func (l *Login) customUserToLoginUserMapping(ctx context.Context, authRequest *domain.AuthRequest, user *domain.Human, metadata []*domain.Metadata, resourceOwner string, flowType domain.FlowType) (*domain.Human, []*domain.Metadata, error) {
triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(ctx, flowType, domain.TriggerTypePreCreation, resourceOwner, false)
if err != nil {
return nil, nil, err
}
ctxOpts := actions.SetContextFields(
actions.SetFields("v1",
actions.SetFields("user", func(c *actions.FieldConfig) interface{} {
return object.UserFromHuman(c, user)
}),
),
)
metadataList := object.MetadataListFromDomain(metadata)
apiFields := actions.WithAPIFields(
actions.SetFields("setFirstName", func(firstName string) {
user.FirstName = firstName
@@ -184,35 +214,26 @@ func (l *Login) customExternalUserToLoginUserMapping(ctx context.Context, user *
}
user.Phone.IsPhoneVerified = verified
}),
actions.SetFields("metadata", metadata),
actions.SetFields("metadata", &metadataList.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
}),
actions.SetFields("appendMetadata", metadataList.AppendMetadataFunc),
),
),
)
for _, a := range triggerActions {
actionCtx, cancel := context.WithTimeout(ctx, a.Timeout())
ctxOpts := actions.SetContextFields(
actions.SetFields("v1",
actions.SetFields("user", func(c *actions.FieldConfig) interface{} {
return object.UserFromHuman(c, user)
}),
actions.SetFields("authRequest", object.AuthRequestField(authRequest)),
),
)
err = actions.Run(
actionCtx,
ctxOpts,
@@ -226,57 +247,21 @@ func (l *Login) customExternalUserToLoginUserMapping(ctx context.Context, user *
return nil, nil, err
}
}
return user, metadata, err
return user, object.MetadataListToDomain(metadataList), err
}
func (l *Login) customGrants(ctx context.Context, userID string, tokens *oidc.Tokens, req *domain.AuthRequest, config *iam_model.IDPConfigView, resourceOwner string) ([]*domain.UserGrant, error) {
triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeExternalAuthentication, domain.TriggerTypePostCreation, resourceOwner, false)
func (l *Login) customGrants(ctx context.Context, userID string, authRequest *domain.AuthRequest, resourceOwner string, flowType domain.FlowType) ([]*domain.UserGrant, error) {
triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(ctx, flowType, domain.TriggerTypePostCreation, resourceOwner, false)
if err != nil {
return nil, err
}
actionUserGrants := make([]actions.UserGrant, 0)
mutableUserGrants := &object.UserGrants{UserGrants: make([]object.UserGrant, 0)}
apiFields := actions.WithAPIFields(
actions.SetFields("userGrants", &actionUserGrants),
actions.SetFields("userGrants", &mutableUserGrants.UserGrants),
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
}
}),
actions.SetFields("appendUserGrant", object.AppendGrantFunc(mutableUserGrants)),
),
)
@@ -287,13 +272,14 @@ func (l *Login) customGrants(ctx context.Context, userID string, tokens *oidc.To
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, false)
user, err := l.query.GetUserByID(actionCtx, true, authRequest.UserID, false)
if err != nil {
panic(err)
}
return object.UserFromQuery(c, user)
}
}),
actions.SetFields("authRequest", object.AuthRequestField(authRequest)),
),
)
@@ -310,21 +296,22 @@ func (l *Login) customGrants(ctx context.Context, userID string, tokens *oidc.To
return nil, err
}
}
return actionUserGrantsToDomain(userID, actionUserGrants), err
return object.UserGrantsToDomain(userID, mutableUserGrants.UserGrants), err
}
func actionUserGrantsToDomain(userID string, actionUserGrants []actions.UserGrant) []*domain.UserGrant {
if actionUserGrants == nil {
return nil
func tokenCtxFields(tokens *oidc.Tokens) []actions.FieldOption {
return []actions.FieldOption{
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
}),
}
userGrants := make([]*domain.UserGrant, len(actionUserGrants))
for i, grant := range actionUserGrants {
userGrants[i] = &domain.UserGrant{
UserID: userID,
ProjectID: grant.ProjectID,
ProjectGrantID: grant.ProjectGrantID,
RoleKeys: grant.Roles,
}
}
return userGrants
}