mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 08:57:35 +00:00
chore: move the go code into a subfolder
This commit is contained in:
141
apps/api/internal/actions/actions.go
Normal file
141
apps/api/internal/actions/actions.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/dop251/goja_nodejs/require"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
HTTP HTTPConfig
|
||||
}
|
||||
|
||||
var ErrHalt = errors.New("interrupt")
|
||||
|
||||
type jsAction func(fields, fields) error
|
||||
|
||||
const (
|
||||
actionStartedMessage = "action run started"
|
||||
actionSucceededMessage = "action run succeeded"
|
||||
)
|
||||
|
||||
func actionFailedMessage(err error) string {
|
||||
return fmt.Sprintf("action run failed: %s", err.Error())
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, ctxParam contextFields, apiParam apiFields, script, name string, opts ...Option) (err error) {
|
||||
config := newRunConfig(ctx, append(opts, withLogger(ctx))...)
|
||||
if config.functionTimeout == 0 {
|
||||
return zerrors.ThrowInternal(nil, "ACTIO-uCpCx", "Errrors.Internal")
|
||||
}
|
||||
|
||||
remaining := logstoreService.Limit(ctx, config.instanceID)
|
||||
config.cutTimeouts(remaining)
|
||||
|
||||
config.logger.Log(actionStartedMessage)
|
||||
if remaining != nil && *remaining == 0 {
|
||||
return zerrors.ThrowResourceExhausted(nil, "ACTIO-f19Ii", "Errors.Quota.Execution.Exhausted")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
config.logger.log(actionFailedMessage(err), logrus.ErrorLevel, true)
|
||||
} else {
|
||||
config.logger.log(actionSucceededMessage, logrus.InfoLevel, true)
|
||||
}
|
||||
if config.allowedToFail {
|
||||
err = nil
|
||||
}
|
||||
}()
|
||||
|
||||
if err := executeScript(config, ctxParam, apiParam, script); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var fn jsAction
|
||||
jsFn := config.vm.Get(name)
|
||||
if jsFn == nil {
|
||||
return errors.New("function not found")
|
||||
}
|
||||
if err := config.vm.ExportTo(jsFn, &fn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := config.StartFunction()
|
||||
defer func() {
|
||||
t.Stop()
|
||||
}()
|
||||
|
||||
return executeFn(config, fn)
|
||||
}
|
||||
|
||||
func executeScript(config *runConfig, ctxParam contextFields, apiParam apiFields, script string) (err error) {
|
||||
t := config.StartScript()
|
||||
defer func() {
|
||||
t.Stop()
|
||||
}()
|
||||
|
||||
if ctxParam != nil {
|
||||
ctxParam(config.ctxParam)
|
||||
}
|
||||
if apiParam != nil {
|
||||
apiParam(config.apiParam)
|
||||
}
|
||||
|
||||
registry := new(require.Registry)
|
||||
registry.Enable(config.vm)
|
||||
|
||||
for name, loader := range config.modules {
|
||||
registry.RegisterNativeModule(name, loader)
|
||||
}
|
||||
// overload error if function panics
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
err = r.(error)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = config.vm.RunString(script)
|
||||
return err
|
||||
}
|
||||
|
||||
func executeFn(config *runConfig, fn jsAction) (err error) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
var ok bool
|
||||
if err, ok = r.(error); ok {
|
||||
return
|
||||
}
|
||||
|
||||
e, ok := r.(string)
|
||||
if ok {
|
||||
err = errors.New(e)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("unknown error occurred: %v", r)
|
||||
}()
|
||||
|
||||
if err = fn(config.ctxParam.fields, config.apiParam.fields); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ActionToOptions(a *query.Action) []Option {
|
||||
opts := make([]Option, 0, 1)
|
||||
if a.AllowedToFail {
|
||||
opts = append(opts, WithAllowedToFail())
|
||||
}
|
||||
return opts
|
||||
}
|
73
apps/api/internal/actions/actions_test.go
Normal file
73
apps/api/internal/actions/actions_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/logstore"
|
||||
"github.com/zitadel/zitadel/internal/logstore/record"
|
||||
)
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
SetLogstoreService(logstore.New[*record.ExecutionLog](nil, nil))
|
||||
type args struct {
|
||||
timeout time.Duration
|
||||
api apiFields
|
||||
ctx contextFields
|
||||
script string
|
||||
name string
|
||||
opts []Option
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "simple script",
|
||||
args: args{
|
||||
api: nil,
|
||||
script: `
|
||||
function testFunc() {
|
||||
for (i = 0; i < 10; i++) {}
|
||||
}`,
|
||||
name: "testFunc",
|
||||
opts: []Option{},
|
||||
},
|
||||
wantErr: func(err error) bool { return err == nil },
|
||||
},
|
||||
{
|
||||
name: "throw error",
|
||||
args: args{
|
||||
api: nil,
|
||||
script: "function testFunc() {throw 'some error'}",
|
||||
name: "testFunc",
|
||||
opts: []Option{},
|
||||
},
|
||||
wantErr: func(err error) bool {
|
||||
gojaErr := new(goja.Exception)
|
||||
if errors.As(err, &gojaErr) {
|
||||
return gojaErr.Value().String() == "some error"
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.timeout == 0 {
|
||||
tt.args.timeout = 10 * time.Second
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), tt.args.timeout)
|
||||
if err := Run(ctx, tt.args.ctx, tt.args.api, tt.args.script, tt.args.name, tt.args.opts...); !tt.wantErr(err) {
|
||||
t.Errorf("Run() unexpected error = (%[1]T) %[1]v", err)
|
||||
}
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
}
|
19
apps/api/internal/actions/api.go
Normal file
19
apps/api/internal/actions/api.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package actions
|
||||
|
||||
type apiConfig struct {
|
||||
FieldConfig
|
||||
}
|
||||
|
||||
type apiFields func(*apiConfig)
|
||||
|
||||
func WithAPIFields(opts ...FieldOption) apiFields {
|
||||
return func(p *apiConfig) {
|
||||
if p.fields == nil {
|
||||
p.fields = fields{}
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(&p.FieldConfig)
|
||||
}
|
||||
}
|
||||
}
|
101
apps/api/internal/actions/config.go
Normal file
101
apps/api/internal/actions/config.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/dop251/goja_nodejs/require"
|
||||
"github.com/zitadel/logging"
|
||||
)
|
||||
|
||||
const (
|
||||
maxPrepareTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
type Option func(*runConfig)
|
||||
|
||||
func WithAllowedToFail() Option {
|
||||
return func(c *runConfig) {
|
||||
c.allowedToFail = true
|
||||
}
|
||||
}
|
||||
|
||||
type runConfig struct {
|
||||
allowedToFail bool
|
||||
functionTimeout,
|
||||
scriptTimeout time.Duration
|
||||
modules map[string]require.ModuleLoader
|
||||
logger *logger
|
||||
instanceID string
|
||||
vm *goja.Runtime
|
||||
ctxParam *ctxConfig
|
||||
apiParam *apiConfig
|
||||
}
|
||||
|
||||
func newRunConfig(ctx context.Context, opts ...Option) *runConfig {
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
logging.Warn("no timeout set on action run")
|
||||
}
|
||||
|
||||
vm := goja.New()
|
||||
vm.SetFieldNameMapper(goja.UncapFieldNameMapper())
|
||||
|
||||
config := &runConfig{
|
||||
functionTimeout: time.Until(deadline),
|
||||
scriptTimeout: maxPrepareTimeout,
|
||||
modules: map[string]require.ModuleLoader{},
|
||||
vm: vm,
|
||||
ctxParam: &ctxConfig{
|
||||
FieldConfig: FieldConfig{
|
||||
Runtime: vm,
|
||||
fields: fields{},
|
||||
},
|
||||
},
|
||||
apiParam: &apiConfig{
|
||||
FieldConfig: FieldConfig{
|
||||
Runtime: vm,
|
||||
fields: fields{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(config)
|
||||
}
|
||||
|
||||
if config.scriptTimeout > config.functionTimeout {
|
||||
config.scriptTimeout = config.functionTimeout
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func (c *runConfig) StartFunction() *time.Timer {
|
||||
c.vm.ClearInterrupt()
|
||||
return time.AfterFunc(c.functionTimeout, func() {
|
||||
c.vm.Interrupt(ErrHalt)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *runConfig) StartScript() *time.Timer {
|
||||
c.vm.ClearInterrupt()
|
||||
return time.AfterFunc(c.scriptTimeout, func() {
|
||||
c.vm.Interrupt(ErrHalt)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *runConfig) cutTimeouts(remainingSeconds *uint64) {
|
||||
if remainingSeconds == nil {
|
||||
return
|
||||
}
|
||||
|
||||
remainingDur := time.Duration(*remainingSeconds) * time.Second
|
||||
if c.functionTimeout > remainingDur {
|
||||
c.functionTimeout = remainingDur
|
||||
}
|
||||
if c.scriptTimeout > remainingDur {
|
||||
c.scriptTimeout = remainingDur
|
||||
}
|
||||
}
|
19
apps/api/internal/actions/context.go
Normal file
19
apps/api/internal/actions/context.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package actions
|
||||
|
||||
type ctxConfig struct {
|
||||
FieldConfig
|
||||
}
|
||||
|
||||
type contextFields func(*ctxConfig)
|
||||
|
||||
func SetContextFields(opts ...FieldOption) contextFields {
|
||||
return func(p *ctxConfig) {
|
||||
if p.fields == nil {
|
||||
p.fields = fields{}
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(&p.FieldConfig)
|
||||
}
|
||||
}
|
||||
}
|
73
apps/api/internal/actions/fields.go
Normal file
73
apps/api/internal/actions/fields.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"github.com/dop251/goja"
|
||||
"github.com/zitadel/logging"
|
||||
)
|
||||
|
||||
type fields map[string]interface{}
|
||||
|
||||
type FieldOption func(*FieldConfig)
|
||||
|
||||
type FieldConfig struct {
|
||||
fields
|
||||
|
||||
Runtime *goja.Runtime
|
||||
}
|
||||
|
||||
func SetFields(name string, values ...interface{}) FieldOption {
|
||||
return func(p *FieldConfig) {
|
||||
if len(values) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, value := range values {
|
||||
val, ok := value.(FieldOption)
|
||||
// is the lowest field and can be set without further checks
|
||||
|
||||
if !ok {
|
||||
// {
|
||||
// "value": "some value"
|
||||
// }
|
||||
p.set(name, value)
|
||||
continue
|
||||
}
|
||||
|
||||
var field fields
|
||||
if f, ok := p.fields[name]; ok {
|
||||
// check if the found field is an object
|
||||
if field, ok = f.(fields); !ok {
|
||||
// panic because overwriting fields is not allowed
|
||||
logging.WithFields("sub", name).Warn("sub is not an object")
|
||||
panic("unable to prepare parameter")
|
||||
}
|
||||
} else {
|
||||
// field does not exist so far.
|
||||
// sub object for field can be created
|
||||
field = fields{}
|
||||
p.fields[name] = field
|
||||
}
|
||||
|
||||
fieldParam := FieldConfig{
|
||||
Runtime: p.Runtime,
|
||||
fields: field,
|
||||
}
|
||||
val(&fieldParam)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FieldConfig) set(name string, value interface{}) {
|
||||
if _, ok := f.fields[name]; ok {
|
||||
logging.WithFields("name", name).Error("tried to overwrite field")
|
||||
panic("tried to overwrite field")
|
||||
}
|
||||
switch v := value.(type) {
|
||||
case func(config *FieldConfig) interface{}:
|
||||
f.fields[name] = v(f)
|
||||
case func(config *FieldConfig) func(call goja.FunctionCall) goja.Value:
|
||||
f.fields[name] = v(f)
|
||||
default:
|
||||
f.fields[name] = value
|
||||
}
|
||||
}
|
190
apps/api/internal/actions/fields_test.go
Normal file
190
apps/api/internal/actions/fields_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/logstore"
|
||||
"github.com/zitadel/zitadel/internal/logstore/record"
|
||||
)
|
||||
|
||||
func TestSetFields(t *testing.T) {
|
||||
SetLogstoreService(logstore.New[*record.ExecutionLog](nil, nil))
|
||||
primitveFn := func(a string) { fmt.Println(a) }
|
||||
complexFn := func(*FieldConfig) interface{} {
|
||||
return primitveFn
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
setFields FieldOption
|
||||
want fields
|
||||
shouldPanic bool
|
||||
}{
|
||||
{
|
||||
name: "field is simple value",
|
||||
setFields: SetFields("value", 5),
|
||||
want: fields{
|
||||
"value": 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "field is method",
|
||||
setFields: SetFields("value", primitveFn),
|
||||
want: fields{
|
||||
"value": primitveFn,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "field is complex method",
|
||||
setFields: SetFields("value", complexFn),
|
||||
want: fields{
|
||||
"value": primitveFn,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "field without value",
|
||||
setFields: SetFields("value"),
|
||||
want: fields{},
|
||||
},
|
||||
{
|
||||
name: "field with empty value",
|
||||
setFields: SetFields("value", ""),
|
||||
want: fields{
|
||||
"value": "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested simple value",
|
||||
setFields: SetFields(
|
||||
"field",
|
||||
SetFields("sub", 5),
|
||||
),
|
||||
want: fields{
|
||||
"field": fields{
|
||||
"sub": 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested multiple fields",
|
||||
setFields: SetFields(
|
||||
"field",
|
||||
SetFields("sub1", 5),
|
||||
SetFields("sub2", "asdf"),
|
||||
SetFields("sub3", primitveFn),
|
||||
),
|
||||
want: fields{
|
||||
"field": fields{
|
||||
"sub1": 5,
|
||||
"sub2": "asdf",
|
||||
"sub3": primitveFn,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "try to overwrite field primitives",
|
||||
setFields: SetFields(
|
||||
"field",
|
||||
SetFields("sub", 5),
|
||||
SetFields("sub", primitveFn),
|
||||
),
|
||||
shouldPanic: true,
|
||||
},
|
||||
{
|
||||
name: "try to overwrite primitives with fields",
|
||||
setFields: SetFields(
|
||||
"field",
|
||||
SetFields("sub", 5),
|
||||
SetFields("sub", SetFields("please", "panic")),
|
||||
),
|
||||
shouldPanic: true,
|
||||
},
|
||||
{
|
||||
name: "try to overwrite fields with primitive",
|
||||
setFields: SetFields(
|
||||
"field",
|
||||
SetFields("sub", SetFields("please", "panic")),
|
||||
SetFields("sub", 5),
|
||||
),
|
||||
shouldPanic: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
panicked := false
|
||||
if tt.shouldPanic {
|
||||
defer func() {
|
||||
if panicked != tt.shouldPanic {
|
||||
t.Errorf("wanted panic: %v got %v", tt.shouldPanic, panicked)
|
||||
}
|
||||
}()
|
||||
defer func() {
|
||||
recover()
|
||||
panicked = true
|
||||
}()
|
||||
}
|
||||
config := &FieldConfig{
|
||||
Runtime: goja.New(),
|
||||
fields: fields{},
|
||||
}
|
||||
tt.setFields(config)
|
||||
if !tt.shouldPanic && fmt.Sprint(config.fields) != fmt.Sprint(tt.want) {
|
||||
t.Errorf("SetFields() = %v, want %v", fmt.Sprint(config.fields), fmt.Sprint(tt.want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetFieldsExecuteMethods(t *testing.T) {
|
||||
primitveFn := func(a string) { fmt.Println(a) }
|
||||
complexFn := func(*FieldConfig) interface{} {
|
||||
return primitveFn
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
setFields FieldOption
|
||||
want fields
|
||||
shouldPanic bool
|
||||
}{
|
||||
{
|
||||
name: "field is method",
|
||||
setFields: SetFields("value", primitveFn),
|
||||
want: fields{
|
||||
"value": primitveFn,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "field is complex method",
|
||||
setFields: SetFields("value", complexFn),
|
||||
want: fields{
|
||||
"value": primitveFn,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
panicked := false
|
||||
if tt.shouldPanic {
|
||||
defer func() {
|
||||
if panicked != tt.shouldPanic {
|
||||
t.Errorf("wanted panic: %v got %v", tt.shouldPanic, panicked)
|
||||
}
|
||||
}()
|
||||
defer func() {
|
||||
recover()
|
||||
panicked = true
|
||||
}()
|
||||
}
|
||||
config := &FieldConfig{
|
||||
Runtime: goja.New(),
|
||||
fields: fields{},
|
||||
}
|
||||
tt.setFields(config)
|
||||
if !tt.shouldPanic && fmt.Sprint(config.fields) != fmt.Sprint(tt.want) {
|
||||
t.Errorf("SetFields() = %v, want %v", fmt.Sprint(config.fields), fmt.Sprint(tt.want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
210
apps/api/internal/actions/http_module.go
Normal file
210
apps/api/internal/actions/http_module.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func WithHTTP(ctx context.Context) Option {
|
||||
return func(c *runConfig) {
|
||||
c.modules["zitadel/http"] = func(runtime *goja.Runtime, module *goja.Object) {
|
||||
requireHTTP(ctx, &http.Client{Transport: &transport{lookup: net.LookupIP}}, runtime, module)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type HTTP struct {
|
||||
runtime *goja.Runtime
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func requireHTTP(ctx context.Context, client *http.Client, runtime *goja.Runtime, module *goja.Object) {
|
||||
c := &HTTP{
|
||||
client: client,
|
||||
runtime: runtime,
|
||||
}
|
||||
o := module.Get("exports").(*goja.Object)
|
||||
logging.OnError(o.Set("fetch", c.fetch(ctx))).Warn("unable to set module")
|
||||
}
|
||||
|
||||
type fetchConfig struct {
|
||||
Method string
|
||||
Headers http.Header
|
||||
Body io.Reader
|
||||
}
|
||||
|
||||
var defaultFetchConfig = fetchConfig{
|
||||
Method: http.MethodGet,
|
||||
Headers: http.Header{
|
||||
"Content-Type": []string{"application/json"},
|
||||
"Accept": []string{"application/json"},
|
||||
},
|
||||
}
|
||||
|
||||
func (c *HTTP) fetchConfigFromArg(arg *goja.Object, config *fetchConfig) (err error) {
|
||||
for _, key := range arg.Keys() {
|
||||
switch key {
|
||||
case "headers":
|
||||
config.Headers = parseHeaders(arg.Get(key).ToObject(c.runtime))
|
||||
case "method":
|
||||
config.Method = arg.Get(key).String()
|
||||
case "body":
|
||||
body, err := arg.Get(key).ToObject(c.runtime).MarshalJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.Body = bytes.NewReader(body)
|
||||
default:
|
||||
return zerrors.ThrowInvalidArgument(nil, "ACTIO-OfUeA", "key is invalid")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Body string
|
||||
Status int
|
||||
Headers map[string][]string
|
||||
runtime *goja.Runtime
|
||||
}
|
||||
|
||||
func (r *response) Json() goja.Value {
|
||||
var val interface{}
|
||||
|
||||
if err := json.Unmarshal([]byte(r.Body), &val); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r.runtime.ToValue(val)
|
||||
}
|
||||
|
||||
func (r *response) Text() goja.Value {
|
||||
return r.runtime.ToValue(r.Body)
|
||||
}
|
||||
|
||||
func (c *HTTP) fetch(ctx context.Context) func(call goja.FunctionCall) goja.Value {
|
||||
return func(call goja.FunctionCall) goja.Value {
|
||||
req := c.buildHTTPRequest(ctx, call.Arguments)
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
c.client.Timeout = time.Until(deadline)
|
||||
}
|
||||
|
||||
res, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
logging.WithError(err).Debug("call failed")
|
||||
panic(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logging.WithError(err).Warn("unable to parse body")
|
||||
panic("unable to read response body")
|
||||
}
|
||||
return c.runtime.ToValue(&response{Status: res.StatusCode, Body: string(body), runtime: c.runtime})
|
||||
}
|
||||
}
|
||||
|
||||
// the first argument has to be a string and is required
|
||||
// the second agrument is optional and an object with the following fields possible:
|
||||
// - `Headers`: map with string key and value of type string or string array
|
||||
// - `Body`: json body of the request
|
||||
// - `Method`: http method type
|
||||
func (c *HTTP) buildHTTPRequest(ctx context.Context, args []goja.Value) (req *http.Request) {
|
||||
if len(args) > 2 {
|
||||
logging.WithFields("count", len(args)).Debug("more than 2 args provided")
|
||||
panic("too many args")
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
panic("no url provided")
|
||||
}
|
||||
|
||||
config := defaultFetchConfig
|
||||
var err error
|
||||
if len(args) == 2 {
|
||||
if err = c.fetchConfigFromArg(args[1].ToObject(c.runtime), &config); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
req, err = http.NewRequestWithContext(ctx, config.Method, args[0].Export().(string), config.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
req.Header = config.Headers
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func parseHeaders(headers *goja.Object) http.Header {
|
||||
h := make(http.Header, len(headers.Keys()))
|
||||
for _, k := range headers.Keys() {
|
||||
header := headers.Get(k).Export()
|
||||
var values []string
|
||||
|
||||
switch headerValue := header.(type) {
|
||||
case string:
|
||||
values = strings.Split(headerValue, ",")
|
||||
case []any:
|
||||
for _, v := range headerValue {
|
||||
values = append(values, v.(string))
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range values {
|
||||
h.Add(k, strings.TrimSpace(v))
|
||||
}
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
type transport struct {
|
||||
lookup func(string) ([]net.IP, error)
|
||||
}
|
||||
|
||||
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if httpConfig == nil || len(httpConfig.DenyList) == 0 {
|
||||
return http.DefaultTransport.RoundTrip(req)
|
||||
}
|
||||
if err := t.isHostBlocked(httpConfig.DenyList, req.URL); err != nil {
|
||||
return nil, zerrors.ThrowInvalidArgument(err, "ACTIO-N72d0", "host is denied")
|
||||
}
|
||||
return http.DefaultTransport.RoundTrip(req)
|
||||
}
|
||||
|
||||
func (t *transport) isHostBlocked(denyList []AddressChecker, address *url.URL) error {
|
||||
host := address.Hostname()
|
||||
ip := net.ParseIP(host)
|
||||
ips := []net.IP{ip}
|
||||
// if the hostname is a domain, we need to check resolve the ip(s), since it might be denied
|
||||
if ip == nil {
|
||||
var err error
|
||||
ips, err = t.lookup(host)
|
||||
if err != nil {
|
||||
return zerrors.ThrowInternal(err, "ACTIO-4m9s2", "lookup failed")
|
||||
}
|
||||
}
|
||||
for _, denied := range denyList {
|
||||
if err := denied.IsDenied(ips, host); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AddressChecker interface {
|
||||
IsDenied([]net.IP, string) error
|
||||
}
|
119
apps/api/internal/actions/http_module_config.go
Normal file
119
apps/api/internal/actions/http_module_config.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
func SetHTTPConfig(config *HTTPConfig) {
|
||||
httpConfig = config
|
||||
}
|
||||
|
||||
var httpConfig *HTTPConfig
|
||||
|
||||
type HTTPConfig struct {
|
||||
DenyList []AddressChecker
|
||||
}
|
||||
|
||||
func HTTPConfigDecodeHook(from, to reflect.Value) (interface{}, error) {
|
||||
if to.Type() != reflect.TypeOf(HTTPConfig{}) {
|
||||
return from.Interface(), nil
|
||||
}
|
||||
|
||||
config := struct {
|
||||
DenyList []string
|
||||
}{}
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
|
||||
WeaklyTypedInput: true,
|
||||
Result: &config,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = decoder.Decode(from.Interface()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := HTTPConfig{
|
||||
DenyList: make([]AddressChecker, 0),
|
||||
}
|
||||
|
||||
for _, unsplit := range config.DenyList {
|
||||
for _, split := range strings.Split(unsplit, ",") {
|
||||
parsed, parseErr := NewHostChecker(split)
|
||||
if parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
if parsed != nil {
|
||||
c.DenyList = append(c.DenyList, parsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func NewHostChecker(entry string) (AddressChecker, error) {
|
||||
if entry == "" {
|
||||
return nil, nil
|
||||
}
|
||||
_, network, err := net.ParseCIDR(entry)
|
||||
if err == nil {
|
||||
return &HostChecker{Net: network}, nil
|
||||
}
|
||||
if ip := net.ParseIP(entry); ip != nil {
|
||||
return &HostChecker{IP: ip}, nil
|
||||
}
|
||||
return &HostChecker{Domain: entry}, nil
|
||||
}
|
||||
|
||||
type HostChecker struct {
|
||||
Net *net.IPNet
|
||||
IP net.IP
|
||||
Domain string
|
||||
}
|
||||
|
||||
type AddressDeniedError struct {
|
||||
deniedBy string
|
||||
}
|
||||
|
||||
func NewAddressDeniedError(deniedBy string) *AddressDeniedError {
|
||||
return &AddressDeniedError{deniedBy: deniedBy}
|
||||
}
|
||||
|
||||
func (e *AddressDeniedError) Error() string {
|
||||
return fmt.Sprintf("address is denied by '%s'", e.deniedBy)
|
||||
}
|
||||
|
||||
func (e *AddressDeniedError) Is(target error) bool {
|
||||
var addressDeniedErr *AddressDeniedError
|
||||
if !errors.As(target, &addressDeniedErr) {
|
||||
return false
|
||||
}
|
||||
return e.deniedBy == addressDeniedErr.deniedBy
|
||||
}
|
||||
|
||||
func (c *HostChecker) IsDenied(ips []net.IP, address string) error {
|
||||
// if the address matches the domain, no additional checks as needed
|
||||
if c.Domain == address {
|
||||
return NewAddressDeniedError(c.Domain)
|
||||
}
|
||||
// otherwise we need to check on ips (incl. the resolved ips of the host)
|
||||
for _, ip := range ips {
|
||||
if c.Net != nil && c.Net.Contains(ip) {
|
||||
return NewAddressDeniedError(c.Net.String())
|
||||
}
|
||||
if c.IP != nil && c.IP.Equal(ip) {
|
||||
return NewAddressDeniedError(c.IP.String())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
480
apps/api/internal/actions/http_module_test.go
Normal file
480
apps/api/internal/actions/http_module_test.go
Normal file
@@ -0,0 +1,480 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/logstore"
|
||||
"github.com/zitadel/zitadel/internal/logstore/record"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func Test_isHostBlocked(t *testing.T) {
|
||||
SetLogstoreService(logstore.New[*record.ExecutionLog](nil, nil))
|
||||
var denyList = []AddressChecker{
|
||||
mustNewHostChecker(t, "192.168.5.0/24"),
|
||||
mustNewHostChecker(t, "127.0.0.1"),
|
||||
mustNewHostChecker(t, "test.com"),
|
||||
}
|
||||
type fields struct {
|
||||
lookup func(host string) ([]net.IP, error)
|
||||
}
|
||||
type args struct {
|
||||
address *url.URL
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "in range",
|
||||
args: args{
|
||||
address: mustNewURL(t, "https://192.168.5.4/hodor"),
|
||||
},
|
||||
want: NewAddressDeniedError("192.168.5.0/24"),
|
||||
},
|
||||
{
|
||||
name: "exact ip",
|
||||
args: args{
|
||||
address: mustNewURL(t, "http://127.0.0.1:8080/hodor"),
|
||||
},
|
||||
want: NewAddressDeniedError("127.0.0.1"),
|
||||
},
|
||||
{
|
||||
name: "address match",
|
||||
fields: fields{
|
||||
lookup: func(host string) ([]net.IP, error) {
|
||||
return []net.IP{net.ParseIP("194.264.52.4")}, nil
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
address: mustNewURL(t, "https://test.com:42/hodor"),
|
||||
},
|
||||
want: NewAddressDeniedError("test.com"),
|
||||
},
|
||||
{
|
||||
name: "address not match",
|
||||
fields: fields{
|
||||
lookup: func(host string) ([]net.IP, error) {
|
||||
return []net.IP{net.ParseIP("194.264.52.4")}, nil
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
address: mustNewURL(t, "https://test2.com/hodor"),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "looked up ip matches",
|
||||
fields: fields{
|
||||
lookup: func(host string) ([]net.IP, error) {
|
||||
return []net.IP{net.ParseIP("127.0.0.1")}, nil
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
address: mustNewURL(t, "https://test2.com/hodor"),
|
||||
},
|
||||
want: NewAddressDeniedError("127.0.0.1"),
|
||||
},
|
||||
{
|
||||
name: "looked up failure",
|
||||
fields: fields{
|
||||
lookup: func(host string) ([]net.IP, error) {
|
||||
return nil, errors.New("some error")
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
address: mustNewURL(t, "https://test2.com/hodor"),
|
||||
},
|
||||
want: zerrors.ThrowInternal(nil, "ACTIO-4m9s2", "lookup failed"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
trans := &transport{
|
||||
lookup: tt.fields.lookup,
|
||||
}
|
||||
got := trans.isHostBlocked(denyList, tt.args.address)
|
||||
assert.ErrorIs(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewHostChecker(t *testing.T, ip string) AddressChecker {
|
||||
t.Helper()
|
||||
checker, err := NewHostChecker(ip)
|
||||
if err != nil {
|
||||
t.Errorf("unable to parse cidr of %q because: %v", ip, err)
|
||||
t.FailNow()
|
||||
}
|
||||
return checker
|
||||
}
|
||||
|
||||
func mustNewURL(t *testing.T, raw string) *url.URL {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
t.Errorf("unable to parse address of %q because: %v", raw, err)
|
||||
t.FailNow()
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func TestHTTP_fetchConfigFromArg(t *testing.T) {
|
||||
runtime := goja.New()
|
||||
runtime.SetFieldNameMapper(goja.UncapFieldNameMapper())
|
||||
|
||||
type args struct {
|
||||
arg *goja.Object
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantConfig fetchConfig
|
||||
wantErr func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "no fetch option provided",
|
||||
args: args{
|
||||
arg: runtime.ToValue(
|
||||
struct{}{},
|
||||
).ToObject(runtime),
|
||||
},
|
||||
wantConfig: fetchConfig{},
|
||||
wantErr: func(err error) bool {
|
||||
return err == nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "header set as string",
|
||||
args: args{
|
||||
arg: runtime.ToValue(
|
||||
&struct {
|
||||
Headers map[string]string
|
||||
}{
|
||||
Headers: map[string]string{
|
||||
"Authorization": "Bearer token",
|
||||
},
|
||||
},
|
||||
).ToObject(runtime),
|
||||
},
|
||||
wantConfig: fetchConfig{
|
||||
Headers: http.Header{
|
||||
"Authorization": {"Bearer token"},
|
||||
},
|
||||
},
|
||||
wantErr: func(err error) bool {
|
||||
return err == nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "header set as list",
|
||||
args: args{
|
||||
arg: runtime.ToValue(
|
||||
&struct {
|
||||
Headers map[string][]any
|
||||
}{
|
||||
Headers: map[string][]any{
|
||||
"Authorization": {"Bearer token"},
|
||||
},
|
||||
},
|
||||
).ToObject(runtime),
|
||||
},
|
||||
wantConfig: fetchConfig{
|
||||
Headers: http.Header{
|
||||
"Authorization": {"Bearer token"},
|
||||
},
|
||||
},
|
||||
wantErr: func(err error) bool {
|
||||
return err == nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "method set",
|
||||
args: args{
|
||||
arg: runtime.ToValue(
|
||||
&struct {
|
||||
Method string
|
||||
}{
|
||||
Method: http.MethodPost,
|
||||
},
|
||||
).ToObject(runtime),
|
||||
},
|
||||
wantConfig: fetchConfig{
|
||||
Method: http.MethodPost,
|
||||
},
|
||||
wantErr: func(err error) bool {
|
||||
return err == nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "body set",
|
||||
args: args{
|
||||
arg: runtime.ToValue(
|
||||
&struct {
|
||||
Body struct{ Id string }
|
||||
}{
|
||||
Body: struct{ Id string }{
|
||||
Id: "asdf123",
|
||||
},
|
||||
},
|
||||
).ToObject(runtime),
|
||||
},
|
||||
wantConfig: fetchConfig{
|
||||
Body: bytes.NewReader([]byte(`{"id":"asdf123"}`)),
|
||||
},
|
||||
wantErr: func(err error) bool {
|
||||
return err == nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid header",
|
||||
args: args{
|
||||
arg: runtime.ToValue(
|
||||
&struct {
|
||||
NotExists struct{}
|
||||
}{
|
||||
NotExists: struct{}{},
|
||||
},
|
||||
).ToObject(runtime),
|
||||
},
|
||||
wantConfig: fetchConfig{},
|
||||
wantErr: func(err error) bool {
|
||||
return zerrors.IsErrorInvalidArgument(err)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &HTTP{
|
||||
runtime: runtime,
|
||||
client: http.DefaultClient,
|
||||
}
|
||||
gotConfig := new(fetchConfig)
|
||||
|
||||
err := c.fetchConfigFromArg(tt.args.arg, gotConfig)
|
||||
if !tt.wantErr(err) {
|
||||
t.Errorf("HTTP.fetchConfigFromArg() error = %v", err)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(gotConfig.Headers, tt.wantConfig.Headers) {
|
||||
t.Errorf("config.Headers got = %#v, want %#v", gotConfig.Headers, tt.wantConfig.Headers)
|
||||
}
|
||||
if gotConfig.Method != tt.wantConfig.Method {
|
||||
t.Errorf("config.Method got = %#v, want %#v", gotConfig.Method, tt.wantConfig.Method)
|
||||
}
|
||||
|
||||
if tt.wantConfig.Body == nil {
|
||||
if gotConfig.Body != nil {
|
||||
t.Errorf("didn't expect a body")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
gotBody, _ := io.ReadAll(gotConfig.Body)
|
||||
wantBody, _ := io.ReadAll(tt.wantConfig.Body)
|
||||
|
||||
if !reflect.DeepEqual(gotBody, wantBody) {
|
||||
t.Errorf("config.Body got = %s, want %s", gotBody, wantBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTP_buildHTTPRequest(t *testing.T) {
|
||||
runtime := goja.New()
|
||||
runtime.SetFieldNameMapper(goja.UncapFieldNameMapper())
|
||||
|
||||
type args struct {
|
||||
args []goja.Value
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantReq *http.Request
|
||||
shouldPanic bool
|
||||
}{
|
||||
{
|
||||
name: "only url",
|
||||
args: args{
|
||||
args: []goja.Value{
|
||||
runtime.ToValue("http://my-url.ch"),
|
||||
},
|
||||
},
|
||||
wantReq: &http.Request{
|
||||
Method: http.MethodGet,
|
||||
URL: mustNewURL(t, "http://my-url.ch"),
|
||||
Header: defaultFetchConfig.Headers,
|
||||
Body: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no params",
|
||||
args: args{
|
||||
args: []goja.Value{
|
||||
runtime.ToValue("http://my-url.ch"),
|
||||
runtime.ToValue(&struct{}{}),
|
||||
},
|
||||
},
|
||||
wantReq: &http.Request{
|
||||
Method: http.MethodGet,
|
||||
URL: mustNewURL(t, "http://my-url.ch"),
|
||||
Header: defaultFetchConfig.Headers,
|
||||
Body: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "overwrite headers",
|
||||
args: args{
|
||||
args: []goja.Value{
|
||||
runtime.ToValue("http://my-url.ch"),
|
||||
runtime.ToValue(struct {
|
||||
Headers map[string][]interface{}
|
||||
}{
|
||||
Headers: map[string][]interface{}{"Authorization": {"some token"}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
wantReq: &http.Request{
|
||||
Method: http.MethodGet,
|
||||
URL: mustNewURL(t, "http://my-url.ch"),
|
||||
Header: http.Header{
|
||||
"Authorization": []string{"some token"},
|
||||
},
|
||||
Body: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "post with body",
|
||||
args: args{
|
||||
args: []goja.Value{
|
||||
runtime.ToValue("http://my-url.ch"),
|
||||
runtime.ToValue(struct {
|
||||
Body struct{ MyData string }
|
||||
}{
|
||||
Body: struct{ MyData string }{MyData: "hello world"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
wantReq: &http.Request{
|
||||
Method: http.MethodGet,
|
||||
URL: mustNewURL(t, "http://my-url.ch"),
|
||||
Header: defaultFetchConfig.Headers,
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(`{"myData":"hello world"}`))),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "too many args",
|
||||
args: args{
|
||||
args: []goja.Value{
|
||||
runtime.ToValue("http://my-url.ch"),
|
||||
runtime.ToValue("http://my-url.ch"),
|
||||
runtime.ToValue("http://my-url.ch"),
|
||||
},
|
||||
},
|
||||
wantReq: nil,
|
||||
shouldPanic: true,
|
||||
},
|
||||
{
|
||||
name: "no args",
|
||||
args: args{
|
||||
args: []goja.Value{},
|
||||
},
|
||||
wantReq: nil,
|
||||
shouldPanic: true,
|
||||
},
|
||||
{
|
||||
name: "invalid config",
|
||||
args: args{
|
||||
args: []goja.Value{
|
||||
runtime.ToValue("http://my-url.ch"),
|
||||
runtime.ToValue(struct {
|
||||
Invalid bool
|
||||
}{
|
||||
Invalid: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
wantReq: nil,
|
||||
shouldPanic: true,
|
||||
},
|
||||
{
|
||||
name: "invalid method",
|
||||
args: args{
|
||||
args: []goja.Value{
|
||||
runtime.ToValue("http://my-url.ch"),
|
||||
runtime.ToValue(struct {
|
||||
Method string
|
||||
}{
|
||||
Method: " asdf asdf",
|
||||
}),
|
||||
},
|
||||
},
|
||||
wantReq: nil,
|
||||
shouldPanic: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
panicked := false
|
||||
if tt.shouldPanic {
|
||||
defer func() {
|
||||
if panicked != tt.shouldPanic {
|
||||
t.Errorf("wanted panic: %v got %v", tt.shouldPanic, panicked)
|
||||
}
|
||||
}()
|
||||
defer func() {
|
||||
recover()
|
||||
panicked = true
|
||||
}()
|
||||
}
|
||||
|
||||
c := &HTTP{
|
||||
runtime: runtime,
|
||||
}
|
||||
|
||||
gotReq := c.buildHTTPRequest(context.Background(), tt.args.args)
|
||||
|
||||
if tt.shouldPanic {
|
||||
return
|
||||
}
|
||||
|
||||
if gotReq.URL.String() != tt.wantReq.URL.String() {
|
||||
t.Errorf("url = %s, want %s", gotReq.URL, tt.wantReq.URL)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(gotReq.Header, tt.wantReq.Header) {
|
||||
t.Errorf("headers = %v, want %v", gotReq.Header, tt.wantReq.Header)
|
||||
}
|
||||
|
||||
if gotReq.Method != tt.wantReq.Method {
|
||||
t.Errorf("method = %s, want %s", gotReq.Method, tt.wantReq.Method)
|
||||
}
|
||||
|
||||
if tt.wantReq.Body == nil {
|
||||
if gotReq.Body != nil {
|
||||
t.Errorf("didn't expect a body")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
gotBody, _ := io.ReadAll(gotReq.Body)
|
||||
wantBody, _ := io.ReadAll(tt.wantReq.Body)
|
||||
|
||||
if !reflect.DeepEqual(gotBody, wantBody) {
|
||||
t.Errorf("config.Body got = %s, want %s", gotBody, wantBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
80
apps/api/internal/actions/log_module.go
Normal file
80
apps/api/internal/actions/log_module.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/dop251/goja_nodejs/console"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/logstore"
|
||||
"github.com/zitadel/zitadel/internal/logstore/record"
|
||||
)
|
||||
|
||||
var (
|
||||
logstoreService *logstore.Service[*record.ExecutionLog]
|
||||
_ console.Printer = (*logger)(nil)
|
||||
)
|
||||
|
||||
func SetLogstoreService(svc *logstore.Service[*record.ExecutionLog]) {
|
||||
logstoreService = svc
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
ctx context.Context
|
||||
started time.Time
|
||||
instanceID string
|
||||
}
|
||||
|
||||
// newLogger returns a *logger instance that should only be used for a single action run.
|
||||
// The first log call sets the started field for subsequent log calls
|
||||
func newLogger(ctx context.Context, instanceID string) *logger {
|
||||
return &logger{
|
||||
ctx: ctx,
|
||||
started: time.Time{},
|
||||
instanceID: instanceID,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logger) Log(msg string) {
|
||||
l.log(msg, logrus.InfoLevel, false)
|
||||
}
|
||||
|
||||
func (l *logger) Warn(msg string) {
|
||||
l.log(msg, logrus.WarnLevel, false)
|
||||
}
|
||||
|
||||
func (l *logger) Error(msg string) {
|
||||
l.log(msg, logrus.ErrorLevel, false)
|
||||
}
|
||||
|
||||
func (l *logger) log(msg string, level logrus.Level, last bool) {
|
||||
ts := time.Now()
|
||||
if l.started.IsZero() {
|
||||
l.started = ts
|
||||
}
|
||||
r := &record.ExecutionLog{
|
||||
LogDate: ts,
|
||||
InstanceID: l.instanceID,
|
||||
Message: msg,
|
||||
LogLevel: level,
|
||||
}
|
||||
if last {
|
||||
r.Took = ts.Sub(l.started)
|
||||
}
|
||||
logstoreService.Handle(l.ctx, r)
|
||||
}
|
||||
|
||||
func withLogger(ctx context.Context) Option {
|
||||
instance := authz.GetInstance(ctx)
|
||||
instanceID := instance.InstanceID()
|
||||
return func(c *runConfig) {
|
||||
c.logger = newLogger(ctx, instanceID)
|
||||
c.instanceID = instanceID
|
||||
c.modules["zitadel/log"] = func(runtime *goja.Runtime, module *goja.Object) {
|
||||
console.RequireWithPrinter(c.logger)(runtime, module)
|
||||
}
|
||||
}
|
||||
}
|
138
apps/api/internal/actions/object/auth_request.go
Normal file
138
apps/api/internal/actions/object/auth_request.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/actions"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
// AuthRequestField accepts the domain.AuthRequest by value, so it's not mutated
|
||||
func AuthRequestField(authRequest *domain.AuthRequest) func(c *actions.FieldConfig) interface{} {
|
||||
return func(c *actions.FieldConfig) interface{} {
|
||||
return AuthRequestFromDomain(c, authRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func AuthRequestFromDomain(c *actions.FieldConfig, request *domain.AuthRequest) goja.Value {
|
||||
if request == nil {
|
||||
return c.Runtime.ToValue(nil)
|
||||
}
|
||||
var maxAuthAge *time.Duration
|
||||
if request.MaxAuthAge != nil {
|
||||
maxAuthAgeCopy := *request.MaxAuthAge
|
||||
maxAuthAge = &maxAuthAgeCopy
|
||||
}
|
||||
|
||||
return c.Runtime.ToValue(&authRequest{
|
||||
Id: request.ID,
|
||||
AgentId: request.AgentID,
|
||||
CreationDate: request.CreationDate,
|
||||
ChangeDate: request.ChangeDate,
|
||||
BrowserInfo: browserInfoFromDomain(request.BrowserInfo),
|
||||
ApplicationId: request.ApplicationID,
|
||||
CallbackUri: request.CallbackURI,
|
||||
TransferState: request.TransferState,
|
||||
Prompt: request.Prompt,
|
||||
UiLocales: request.UiLocales,
|
||||
LoginHint: request.LoginHint,
|
||||
MaxAuthAge: maxAuthAge,
|
||||
InstanceId: request.InstanceID,
|
||||
Request: requestFromDomain(request.Request),
|
||||
UserId: request.UserID,
|
||||
UserName: request.UserName,
|
||||
LoginName: request.LoginName,
|
||||
DisplayName: request.DisplayName,
|
||||
ResourceOwner: request.UserOrgID,
|
||||
RequestedOrgId: request.RequestedOrgID,
|
||||
RequestedOrgName: request.RequestedOrgName,
|
||||
RequestedPrimaryDomain: request.RequestedPrimaryDomain,
|
||||
RequestedOrgDomain: request.RequestedOrgDomain,
|
||||
ApplicationResourceOwner: request.ApplicationResourceOwner,
|
||||
PrivateLabelingSetting: request.PrivateLabelingSetting,
|
||||
SelectedIdpConfigId: request.SelectedIDPConfigID,
|
||||
LinkingUsers: externalUsersFromDomain(request.LinkingUsers),
|
||||
PasswordVerified: request.PasswordVerified,
|
||||
MfasVerified: request.MFAsVerified,
|
||||
Audience: request.Audience,
|
||||
AuthTime: request.AuthTime,
|
||||
})
|
||||
}
|
||||
|
||||
type authRequest struct {
|
||||
Id string
|
||||
AgentId string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
BrowserInfo *browserInfo
|
||||
ApplicationId string
|
||||
CallbackUri string
|
||||
TransferState string
|
||||
Prompt []domain.Prompt
|
||||
UiLocales []string
|
||||
LoginHint string
|
||||
MaxAuthAge *time.Duration
|
||||
InstanceId string
|
||||
Request *request
|
||||
UserId string
|
||||
UserName string
|
||||
LoginName string
|
||||
DisplayName string
|
||||
// UserOrgID string
|
||||
ResourceOwner string
|
||||
// requested by scope
|
||||
RequestedOrgId string
|
||||
// requested by scope
|
||||
RequestedOrgName string
|
||||
// requested by scope
|
||||
RequestedPrimaryDomain string
|
||||
// requested by scope
|
||||
RequestedOrgDomain bool
|
||||
// client
|
||||
ApplicationResourceOwner string
|
||||
PrivateLabelingSetting domain.PrivateLabelingSetting
|
||||
SelectedIdpConfigId string
|
||||
LinkingUsers []*externalUser
|
||||
PasswordVerified bool
|
||||
MfasVerified []domain.MFAType
|
||||
Audience []string
|
||||
AuthTime time.Time
|
||||
}
|
||||
|
||||
func browserInfoFromDomain(info *domain.BrowserInfo) *browserInfo {
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
return &browserInfo{
|
||||
UserAgent: info.UserAgent,
|
||||
AcceptLanguage: info.AcceptLanguage,
|
||||
RemoteIp: info.RemoteIP,
|
||||
}
|
||||
}
|
||||
|
||||
func requestFromDomain(req domain.Request) *request {
|
||||
r := new(request)
|
||||
|
||||
if oidcRequest, ok := req.(*domain.AuthRequestOIDC); ok {
|
||||
r.Oidc = OIDCRequest{Scopes: oidcRequest.Scopes}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
type request struct {
|
||||
Oidc OIDCRequest
|
||||
}
|
||||
|
||||
type OIDCRequest struct {
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
type browserInfo struct {
|
||||
UserAgent string
|
||||
AcceptLanguage string
|
||||
RemoteIp net.IP
|
||||
}
|
44
apps/api/internal/actions/object/http_request.go
Normal file
44
apps/api/internal/actions/object/http_request.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/actions"
|
||||
)
|
||||
|
||||
// HTTPRequestField accepts the http.Request by value, so it's not mutated
|
||||
func HTTPRequestField(request *http.Request) func(c *actions.FieldConfig) interface{} {
|
||||
return func(c *actions.FieldConfig) interface{} {
|
||||
return c.Runtime.ToValue(&httpRequest{
|
||||
Method: request.Method,
|
||||
Url: request.URL.String(),
|
||||
Proto: request.Proto,
|
||||
ContentLength: request.ContentLength,
|
||||
Host: request.Host,
|
||||
Form: copyMap(request.Form),
|
||||
PostForm: copyMap(request.PostForm),
|
||||
RemoteAddr: request.RemoteAddr,
|
||||
Headers: copyMap(request.Header),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type httpRequest struct {
|
||||
Method string
|
||||
Url string
|
||||
Proto string
|
||||
ContentLength int64
|
||||
Host string
|
||||
Form map[string][]string
|
||||
PostForm map[string][]string
|
||||
RemoteAddr string
|
||||
Headers map[string][]string
|
||||
}
|
||||
|
||||
func copyMap(src map[string][]string) map[string][]string {
|
||||
dst := make(map[string][]string)
|
||||
for k, v := range src {
|
||||
dst[k] = v
|
||||
}
|
||||
return dst
|
||||
}
|
234
apps/api/internal/actions/object/metadata.go
Normal file
234
apps/api/internal/actions/object/metadata.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/actions"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
func OrgMetadataListFromQuery(c *actions.FieldConfig, orgMetadata *query.OrgMetadataList) goja.Value {
|
||||
result := &metadataList{
|
||||
Count: orgMetadata.Count,
|
||||
Sequence: orgMetadata.Sequence,
|
||||
Timestamp: orgMetadata.LastRun,
|
||||
Metadata: make([]*metadata, len(orgMetadata.Metadata)),
|
||||
}
|
||||
|
||||
for i, md := range orgMetadata.Metadata {
|
||||
result.Metadata[i] = &metadata{
|
||||
CreationDate: md.CreationDate,
|
||||
ChangeDate: md.ChangeDate,
|
||||
ResourceOwner: md.ResourceOwner,
|
||||
Sequence: md.Sequence,
|
||||
Key: md.Key,
|
||||
Value: metadataByteArrayToValue(md.Value, c.Runtime),
|
||||
}
|
||||
}
|
||||
|
||||
return c.Runtime.ToValue(result)
|
||||
}
|
||||
|
||||
func UserMetadataListFromQuery(c *actions.FieldConfig, metadata *query.UserMetadataList) goja.Value {
|
||||
result := &userMetadataList{
|
||||
Count: metadata.Count,
|
||||
Sequence: metadata.Sequence,
|
||||
Timestamp: metadata.LastRun,
|
||||
Metadata: make([]*userMetadata, len(metadata.Metadata)),
|
||||
}
|
||||
|
||||
for i, md := range metadata.Metadata {
|
||||
result.Metadata[i] = &userMetadata{
|
||||
CreationDate: md.CreationDate,
|
||||
ChangeDate: md.ChangeDate,
|
||||
ResourceOwner: md.ResourceOwner,
|
||||
Sequence: md.Sequence,
|
||||
Key: md.Key,
|
||||
Value: metadataByteArrayToValue(md.Value, c.Runtime),
|
||||
}
|
||||
}
|
||||
|
||||
return c.Runtime.ToValue(result)
|
||||
}
|
||||
|
||||
func UserMetadataListFromSlice(c *actions.FieldConfig, metadata []query.UserMetadata) goja.Value {
|
||||
result := &userMetadataList{
|
||||
// Count was the only field ever queried from the DB in the old implementation,
|
||||
// so Sequence and LastRun are omitted.
|
||||
Count: uint64(len(metadata)),
|
||||
Metadata: make([]*userMetadata, len(metadata)),
|
||||
}
|
||||
for i, md := range metadata {
|
||||
result.Metadata[i] = &userMetadata{
|
||||
CreationDate: md.CreationDate,
|
||||
ChangeDate: md.ChangeDate,
|
||||
ResourceOwner: md.ResourceOwner,
|
||||
Sequence: md.Sequence,
|
||||
Key: md.Key,
|
||||
Value: metadataByteArrayToValue(md.Value, c.Runtime),
|
||||
}
|
||||
}
|
||||
|
||||
return c.Runtime.ToValue(result)
|
||||
}
|
||||
|
||||
func GetOrganizationMetadata(ctx context.Context, queries *query.Queries, c *actions.FieldConfig, organizationID string) goja.Value {
|
||||
metadata, err := queries.SearchOrgMetadata(
|
||||
ctx,
|
||||
true,
|
||||
organizationID,
|
||||
&query.OrgMetadataSearchQueries{},
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
logging.WithError(err).Info("unable to get org metadata in action")
|
||||
panic(err)
|
||||
}
|
||||
return OrgMetadataListFromQuery(c, metadata)
|
||||
}
|
||||
|
||||
func metadataByteArrayToValue(val []byte, runtime *goja.Runtime) goja.Value {
|
||||
var value interface{}
|
||||
if !json.Valid(val) {
|
||||
var err error
|
||||
val, err = json.Marshal(string(val))
|
||||
if err != nil {
|
||||
logging.WithError(err).Debug("unable to marshal unknown value")
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
err := json.Unmarshal(val, &value)
|
||||
if err != nil {
|
||||
logging.WithError(err).Debug("unable to unmarshal into map")
|
||||
panic(err)
|
||||
}
|
||||
return runtime.ToValue(value)
|
||||
}
|
||||
|
||||
type metadataList struct {
|
||||
Count uint64
|
||||
Sequence uint64
|
||||
Timestamp time.Time
|
||||
Metadata []*metadata
|
||||
}
|
||||
|
||||
type metadata struct {
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
ResourceOwner string
|
||||
Sequence uint64
|
||||
Key string
|
||||
Value goja.Value
|
||||
}
|
||||
|
||||
type userMetadataList struct {
|
||||
Count uint64
|
||||
Sequence uint64
|
||||
Timestamp time.Time
|
||||
Metadata []*userMetadata
|
||||
}
|
||||
|
||||
type userMetadata struct {
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
ResourceOwner string
|
||||
Sequence uint64
|
||||
Key string
|
||||
Value goja.Value
|
||||
}
|
||||
|
||||
type MetadataList struct {
|
||||
metadata []*Metadata
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
Key string
|
||||
// Value is for exporting to javascript
|
||||
Value goja.Value
|
||||
// value is for mapping to [domain.Metadata]
|
||||
value []byte
|
||||
}
|
||||
|
||||
func (md *MetadataList) AppendMetadataFunc(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) != 2 {
|
||||
panic("exactly 2 (key, value) arguments expected")
|
||||
}
|
||||
|
||||
value, err := json.Marshal(call.Arguments[1].Export())
|
||||
if err != nil {
|
||||
logging.WithError(err).Debug("unable to marshal")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
md.metadata = append(md.metadata,
|
||||
&Metadata{
|
||||
Key: call.Arguments[0].Export().(string),
|
||||
Value: call.Arguments[1],
|
||||
value: value,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (md *MetadataList) MetadataListFromDomain(runtime *goja.Runtime) interface{} {
|
||||
for i, metadata := range md.metadata {
|
||||
md.metadata[i].Value = metadataByteArrayToValue(metadata.value, runtime)
|
||||
}
|
||||
return &md.metadata
|
||||
}
|
||||
|
||||
func MetadataListFromDomain(metadata []*domain.Metadata) *MetadataList {
|
||||
list := &MetadataList{metadata: make([]*Metadata, len(metadata))}
|
||||
|
||||
for i, md := range metadata {
|
||||
list.metadata[i] = &Metadata{
|
||||
Key: md.Key,
|
||||
value: md.Value,
|
||||
}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func MetadataListToDomain(metadataList *MetadataList) []*domain.Metadata {
|
||||
if metadataList == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
list := make([]*domain.Metadata, len(metadataList.metadata))
|
||||
for i, metadata := range metadataList.metadata {
|
||||
value := metadata.value
|
||||
if len(value) == 0 {
|
||||
value = mapBytesToByteArray(metadata.Value.Export())
|
||||
}
|
||||
list[i] = &domain.Metadata{
|
||||
Key: metadata.Key,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
// mapBytesToByteArray is used for backwards compatibility of old metadata.push method
|
||||
// converts the Javascript uint8 array which is exported as []interface{} to a []byte
|
||||
func mapBytesToByteArray(i interface{}) []byte {
|
||||
bytes, ok := i.([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
value := make([]byte, len(bytes))
|
||||
for i, val := range bytes {
|
||||
b, ok := val.(int64)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
value[i] = byte(b)
|
||||
}
|
||||
return value
|
||||
}
|
14
apps/api/internal/actions/object/object.go
Normal file
14
apps/api/internal/actions/object/object.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package object
|
||||
|
||||
import "github.com/dop251/goja"
|
||||
|
||||
func objectFromFirstArgument(call goja.FunctionCall, runtime *goja.Runtime) *goja.Object {
|
||||
if len(call.Arguments) != 1 {
|
||||
panic("exactly one argument expected")
|
||||
}
|
||||
object := call.Arguments[0].ToObject(runtime)
|
||||
if object == nil {
|
||||
panic("unable to unmarshal arg")
|
||||
}
|
||||
return object
|
||||
}
|
181
apps/api/internal/actions/object/user.go
Normal file
181
apps/api/internal/actions/object/user.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/actions"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
func UserFromExternalUser(c *actions.FieldConfig, user *domain.ExternalUser) goja.Value {
|
||||
return c.Runtime.ToValue(externalUserFromDomain(user))
|
||||
}
|
||||
|
||||
func externalUsersFromDomain(users []*domain.ExternalUser) []*externalUser {
|
||||
externalUsers := make([]*externalUser, len(users))
|
||||
|
||||
for i, user := range users {
|
||||
externalUsers[i] = externalUserFromDomain(user)
|
||||
}
|
||||
|
||||
return externalUsers
|
||||
}
|
||||
|
||||
func externalUserFromDomain(user *domain.ExternalUser) *externalUser {
|
||||
return &externalUser{
|
||||
ExternalId: user.ExternalUserID,
|
||||
ExternalIdpId: user.IDPConfigID,
|
||||
Human: human{
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
NickName: user.NickName,
|
||||
DisplayName: user.DisplayName,
|
||||
PreferredLanguage: user.PreferredLanguage.String(),
|
||||
Email: user.Email,
|
||||
IsEmailVerified: user.IsEmailVerified,
|
||||
Phone: user.Phone,
|
||||
IsPhoneVerified: user.IsPhoneVerified,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func UserFromHuman(c *actions.FieldConfig, user *domain.Human) goja.Value {
|
||||
u := &humanUser{
|
||||
Id: user.AggregateID,
|
||||
CreationDate: user.CreationDate,
|
||||
ChangeDate: user.ChangeDate,
|
||||
ResourceOwner: user.ResourceOwner,
|
||||
Sequence: user.Sequence,
|
||||
State: user.State,
|
||||
Username: user.Username,
|
||||
LoginNames: user.LoginNames,
|
||||
PreferredLoginName: user.PreferredLoginName,
|
||||
}
|
||||
|
||||
if user.Profile != nil {
|
||||
u.Human.FirstName = user.Profile.FirstName
|
||||
u.Human.LastName = user.Profile.LastName
|
||||
u.Human.NickName = user.Profile.NickName
|
||||
u.Human.DisplayName = user.Profile.DisplayName
|
||||
u.Human.PreferredLanguage = user.Profile.PreferredLanguage.String()
|
||||
}
|
||||
|
||||
if user.Email != nil {
|
||||
u.Human.Email = user.Email.EmailAddress
|
||||
u.Human.IsEmailVerified = user.Email.IsEmailVerified
|
||||
}
|
||||
|
||||
if user.Phone != nil {
|
||||
u.Human.Phone = user.Phone.PhoneNumber
|
||||
u.Human.IsPhoneVerified = user.Phone.IsPhoneVerified
|
||||
}
|
||||
|
||||
return c.Runtime.ToValue(u)
|
||||
}
|
||||
|
||||
func UserFromQuery(c *actions.FieldConfig, user *query.User) goja.Value {
|
||||
if user.Human != nil {
|
||||
return humanFromQuery(c, user)
|
||||
}
|
||||
return machineFromQuery(c, user)
|
||||
}
|
||||
|
||||
func humanFromQuery(c *actions.FieldConfig, user *query.User) goja.Value {
|
||||
return c.Runtime.ToValue(&humanUser{
|
||||
Id: user.ID,
|
||||
CreationDate: user.CreationDate,
|
||||
ChangeDate: user.ChangeDate,
|
||||
ResourceOwner: user.ResourceOwner,
|
||||
Sequence: user.Sequence,
|
||||
State: user.State,
|
||||
Username: user.Username,
|
||||
LoginNames: user.LoginNames,
|
||||
PreferredLoginName: user.PreferredLoginName,
|
||||
Human: human{
|
||||
FirstName: user.Human.FirstName,
|
||||
LastName: user.Human.LastName,
|
||||
NickName: user.Human.NickName,
|
||||
DisplayName: user.Human.DisplayName,
|
||||
AvatarKey: user.Human.AvatarKey,
|
||||
PreferredLanguage: user.Human.PreferredLanguage.String(),
|
||||
Gender: user.Human.Gender,
|
||||
Email: user.Human.Email,
|
||||
IsEmailVerified: user.Human.IsEmailVerified,
|
||||
Phone: user.Human.Phone,
|
||||
IsPhoneVerified: user.Human.IsPhoneVerified,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func machineFromQuery(c *actions.FieldConfig, user *query.User) goja.Value {
|
||||
return c.Runtime.ToValue(&machineUser{
|
||||
Id: user.ID,
|
||||
CreationDate: user.CreationDate,
|
||||
ChangeDate: user.ChangeDate,
|
||||
ResourceOwner: user.ResourceOwner,
|
||||
Sequence: user.Sequence,
|
||||
State: user.State,
|
||||
Username: user.Username,
|
||||
LoginNames: user.LoginNames,
|
||||
PreferredLoginName: user.PreferredLoginName,
|
||||
Machine: machine{
|
||||
Name: user.Machine.Name,
|
||||
Description: user.Machine.Description,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type externalUser struct {
|
||||
ExternalId string
|
||||
ExternalIdpId string
|
||||
Human human
|
||||
}
|
||||
|
||||
type humanUser struct {
|
||||
Id string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
ResourceOwner string
|
||||
Sequence uint64
|
||||
State domain.UserState
|
||||
Username string
|
||||
LoginNames database.TextArray[string]
|
||||
PreferredLoginName string
|
||||
Human human
|
||||
}
|
||||
|
||||
type human struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
NickName string
|
||||
DisplayName string
|
||||
AvatarKey string
|
||||
PreferredLanguage string
|
||||
Gender domain.Gender
|
||||
Email domain.EmailAddress
|
||||
IsEmailVerified bool
|
||||
Phone domain.PhoneNumber
|
||||
IsPhoneVerified bool
|
||||
}
|
||||
|
||||
type machineUser struct {
|
||||
Id string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
ResourceOwner string
|
||||
Sequence uint64
|
||||
State domain.UserState
|
||||
Username string
|
||||
LoginNames database.TextArray[string]
|
||||
PreferredLoginName string
|
||||
Machine machine
|
||||
}
|
||||
|
||||
type machine struct {
|
||||
Name string
|
||||
Description string
|
||||
}
|
178
apps/api/internal/actions/object/user_grant.go
Normal file
178
apps/api/internal/actions/object/user_grant.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/actions"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
type UserGrants struct {
|
||||
UserGrants []UserGrant
|
||||
}
|
||||
|
||||
type UserGrant struct {
|
||||
ProjectID string
|
||||
ProjectGrantID string
|
||||
Roles []string
|
||||
}
|
||||
|
||||
type userGrantList struct {
|
||||
Count uint64
|
||||
Sequence uint64
|
||||
Timestamp time.Time
|
||||
Grants []*userGrant
|
||||
}
|
||||
|
||||
type userGrant struct {
|
||||
Id string
|
||||
ProjectGrantId string
|
||||
State domain.UserGrantState
|
||||
UserGrantResourceOwner string
|
||||
UserGrantResourceOwnerName string
|
||||
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
Sequence uint64
|
||||
|
||||
UserId string
|
||||
UserResourceOwner string
|
||||
Roles []string
|
||||
|
||||
ProjectId string
|
||||
ProjectName string
|
||||
|
||||
GetOrgMetadata func(goja.FunctionCall) goja.Value
|
||||
}
|
||||
|
||||
func AppendGrantFunc(userGrants *UserGrants) func(c *actions.FieldConfig) func(call goja.FunctionCall) goja.Value {
|
||||
return func(c *actions.FieldConfig) func(call goja.FunctionCall) goja.Value {
|
||||
return func(call goja.FunctionCall) goja.Value {
|
||||
firstArg := objectFromFirstArgument(call, c.Runtime)
|
||||
grant := UserGrant{}
|
||||
mapObjectToGrant(firstArg, &grant)
|
||||
userGrants.UserGrants = append(userGrants.UserGrants, grant)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func UserGrantsFromQuery(ctx context.Context, queries *query.Queries, c *actions.FieldConfig, userGrants *query.UserGrants) goja.Value {
|
||||
if userGrants == nil {
|
||||
return c.Runtime.ToValue(nil)
|
||||
}
|
||||
orgMetadata := make(map[string]goja.Value)
|
||||
grantList := &userGrantList{
|
||||
Count: userGrants.Count,
|
||||
Sequence: userGrants.Sequence,
|
||||
Timestamp: userGrants.LastRun,
|
||||
Grants: make([]*userGrant, len(userGrants.UserGrants)),
|
||||
}
|
||||
|
||||
for i, grant := range userGrants.UserGrants {
|
||||
grantList.Grants[i] = &userGrant{
|
||||
Id: grant.ID,
|
||||
ProjectGrantId: grant.GrantID,
|
||||
State: grant.State,
|
||||
CreationDate: grant.CreationDate,
|
||||
ChangeDate: grant.ChangeDate,
|
||||
Sequence: grant.Sequence,
|
||||
UserId: grant.UserID,
|
||||
Roles: grant.Roles,
|
||||
UserResourceOwner: grant.UserResourceOwner,
|
||||
UserGrantResourceOwner: grant.ResourceOwner,
|
||||
UserGrantResourceOwnerName: grant.OrgName,
|
||||
ProjectId: grant.ProjectID,
|
||||
ProjectName: grant.ProjectName,
|
||||
GetOrgMetadata: func(call goja.FunctionCall) goja.Value {
|
||||
if md, ok := orgMetadata[grant.ResourceOwner]; ok {
|
||||
return md
|
||||
}
|
||||
orgMetadata[grant.ResourceOwner] = GetOrganizationMetadata(ctx, queries, c, grant.ResourceOwner)
|
||||
return orgMetadata[grant.ResourceOwner]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return c.Runtime.ToValue(grantList)
|
||||
}
|
||||
|
||||
func UserGrantsFromSlice(ctx context.Context, queries *query.Queries, c *actions.FieldConfig, userGrants []query.UserGrant) goja.Value {
|
||||
if userGrants == nil {
|
||||
return c.Runtime.ToValue(nil)
|
||||
}
|
||||
orgMetadata := make(map[string]goja.Value)
|
||||
grantList := &userGrantList{
|
||||
Count: uint64(len(userGrants)),
|
||||
Grants: make([]*userGrant, len(userGrants)),
|
||||
}
|
||||
|
||||
for i, grant := range userGrants {
|
||||
grantList.Grants[i] = &userGrant{
|
||||
Id: grant.ID,
|
||||
ProjectGrantId: grant.GrantID,
|
||||
State: grant.State,
|
||||
CreationDate: grant.CreationDate,
|
||||
ChangeDate: grant.ChangeDate,
|
||||
Sequence: grant.Sequence,
|
||||
UserId: grant.UserID,
|
||||
Roles: grant.Roles,
|
||||
UserResourceOwner: grant.UserResourceOwner,
|
||||
UserGrantResourceOwner: grant.ResourceOwner,
|
||||
UserGrantResourceOwnerName: grant.OrgName,
|
||||
ProjectId: grant.ProjectID,
|
||||
ProjectName: grant.ProjectName,
|
||||
GetOrgMetadata: func(goja.FunctionCall) goja.Value {
|
||||
if md, ok := orgMetadata[grant.ResourceOwner]; ok {
|
||||
return md
|
||||
}
|
||||
orgMetadata[grant.ResourceOwner] = GetOrganizationMetadata(ctx, queries, c, grant.ResourceOwner)
|
||||
return orgMetadata[grant.ResourceOwner]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return c.Runtime.ToValue(grantList)
|
||||
}
|
||||
|
||||
func UserGrantsToDomain(userID string, actionUserGrants []UserGrant) []*domain.UserGrant {
|
||||
if actionUserGrants == nil {
|
||||
return 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
|
||||
}
|
||||
|
||||
func mapObjectToGrant(object *goja.Object, grant *UserGrant) {
|
||||
for _, key := range object.Keys() {
|
||||
switch key {
|
||||
case "projectId", "projectID":
|
||||
grant.ProjectID = object.Get(key).String()
|
||||
case "projectGrantId", "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")
|
||||
}
|
||||
}
|
83
apps/api/internal/actions/uuid_module.go
Normal file
83
apps/api/internal/actions/uuid_module.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/google/uuid"
|
||||
"github.com/zitadel/logging"
|
||||
)
|
||||
|
||||
func WithUUID(ctx context.Context) Option {
|
||||
return func(c *runConfig) {
|
||||
c.modules["zitadel/uuid"] = func(runtime *goja.Runtime, module *goja.Object) {
|
||||
requireUUID(ctx, runtime, module)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func requireUUID(_ context.Context, runtime *goja.Runtime, module *goja.Object) {
|
||||
o := module.Get("exports").(*goja.Object)
|
||||
logging.OnError(o.Set("v1", inRuntime(uuid.NewUUID, runtime))).Warn("unable to set module")
|
||||
logging.OnError(o.Set("v3", inRuntimeHash(uuid.NewMD5, runtime))).Warn("unable to set module")
|
||||
logging.OnError(o.Set("v4", inRuntime(uuid.NewRandom, runtime))).Warn("unable to set module")
|
||||
logging.OnError(o.Set("v5", inRuntimeHash(uuid.NewSHA1, runtime))).Warn("unable to set module")
|
||||
logging.OnError(o.Set("namespaceDNS", uuid.NameSpaceDNS)).Warn("unable to set namespace")
|
||||
logging.OnError(o.Set("namespaceURL", uuid.NameSpaceURL)).Warn("unable to set namespace")
|
||||
logging.OnError(o.Set("namespaceOID", uuid.NameSpaceOID)).Warn("unable to set namespace")
|
||||
logging.OnError(o.Set("namespaceX500", uuid.NameSpaceX500)).Warn("unable to set namespace")
|
||||
}
|
||||
|
||||
func inRuntime(function func() (uuid.UUID, error), runtime *goja.Runtime) func(call goja.FunctionCall) goja.Value {
|
||||
return func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) != 0 {
|
||||
panic("invalid arg count")
|
||||
}
|
||||
|
||||
uuid, err := function()
|
||||
if err != nil {
|
||||
logging.WithError(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return runtime.ToValue(uuid.String())
|
||||
}
|
||||
}
|
||||
|
||||
func inRuntimeHash(function func(uuid.UUID, []byte) uuid.UUID, runtime *goja.Runtime) func(call goja.FunctionCall) goja.Value {
|
||||
return func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) != 2 {
|
||||
logging.WithFields("count", len(call.Arguments)).Debug("other than 2 args provided")
|
||||
panic("invalid arg count")
|
||||
}
|
||||
|
||||
var err error
|
||||
var namespace uuid.UUID
|
||||
switch n := call.Arguments[0].Export().(type) {
|
||||
case string:
|
||||
namespace, err = uuid.Parse(n)
|
||||
if err != nil {
|
||||
logging.WithError(err).Debug("namespace failed parsing as UUID")
|
||||
panic(err)
|
||||
}
|
||||
case uuid.UUID:
|
||||
namespace = n
|
||||
default:
|
||||
logging.WithError(err).Debug("invalid type for namespace")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var data []byte
|
||||
switch d := call.Arguments[1].Export().(type) {
|
||||
case string:
|
||||
data = []byte(d)
|
||||
case []byte:
|
||||
data = d
|
||||
default:
|
||||
logging.WithError(err).Debug("invalid type for data")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return runtime.ToValue(function(namespace, data).String())
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user