diff --git a/automation/automation/jwt_handler.gen.go b/automation/automation/jwt_handler.gen.go new file mode 100644 index 000000000..222e07eeb --- /dev/null +++ b/automation/automation/jwt_handler.gen.go @@ -0,0 +1,184 @@ +package automation + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// automation/automation/jwt_handler.yaml + +import ( + "context" + atypes "github.com/cortezaproject/corteza-server/automation/types" + "github.com/cortezaproject/corteza-server/pkg/expr" + "github.com/cortezaproject/corteza-server/pkg/wfexec" + "io" +) + +var _ wfexec.ExecResponse + +type ( + jwtHandlerRegistry interface { + AddFunctions(ff ...*atypes.Function) + Type(ref string) expr.Type + } +) + +func (h jwtHandler) register() { + h.reg.AddFunctions( + h.Generate(), + ) +} + +type ( + jwtGenerateArgs struct { + hasScope bool + Scope string + + hasHeader bool + Header interface{} + headerVars map[string]expr.TypedValue + headerString string + + hasPayload bool + Payload interface{} + payloadVars map[string]expr.TypedValue + payloadString string + + hasSecret bool + Secret interface{} + secretString string + secretStream io.Reader + } + + jwtGenerateResults struct { + Token string + } +) + +func (a jwtGenerateArgs) GetHeader() (bool, map[string]expr.TypedValue, string) { + return a.hasHeader, a.headerVars, a.headerString +} + +func (a jwtGenerateArgs) GetPayload() (bool, map[string]expr.TypedValue, string) { + return a.hasPayload, a.payloadVars, a.payloadString +} + +func (a jwtGenerateArgs) GetSecret() (bool, string, io.Reader) { + return a.hasSecret, a.secretString, a.secretStream +} + +// Generate function Generate JWT +// +// expects implementation of generate function: +// func (h jwtHandler) generate(ctx context.Context, args *jwtGenerateArgs) (results *jwtGenerateResults, err error) { +// return +// } +func (h jwtHandler) Generate() *atypes.Function { + return &atypes.Function{ + Ref: "jwtGenerate", + Kind: "function", + Labels: map[string]string(nil), + Meta: &atypes.FunctionMeta{ + Short: "Generate JWT", + }, + + Parameters: []*atypes.Param{ + { + Name: "scope", + Types: []string{"String"}, + }, + { + Name: "header", + Types: []string{"Vars", "String"}, Required: true, + }, + { + Name: "payload", + Types: []string{"Vars", "String"}, Required: true, + }, + { + Name: "secret", + Types: []string{"String", "Reader"}, Required: true, + }, + }, + + Results: []*atypes.Param{ + + { + Name: "token", + Types: []string{"String"}, + }, + }, + + Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) { + var ( + args = &jwtGenerateArgs{ + hasScope: in.Has("scope"), + hasHeader: in.Has("header"), + hasPayload: in.Has("payload"), + hasSecret: in.Has("secret"), + } + ) + + if err = in.Decode(args); err != nil { + return + } + + // Converting Header argument + if args.hasHeader { + aux := expr.Must(expr.Select(in, "header")) + switch aux.Type() { + case h.reg.Type("Vars").Type(): + args.headerVars = aux.Get().(map[string]expr.TypedValue) + case h.reg.Type("String").Type(): + args.headerString = aux.Get().(string) + } + } + + // Converting Payload argument + if args.hasPayload { + aux := expr.Must(expr.Select(in, "payload")) + switch aux.Type() { + case h.reg.Type("Vars").Type(): + args.payloadVars = aux.Get().(map[string]expr.TypedValue) + case h.reg.Type("String").Type(): + args.payloadString = aux.Get().(string) + } + } + + // Converting Secret argument + if args.hasSecret { + aux := expr.Must(expr.Select(in, "secret")) + switch aux.Type() { + case h.reg.Type("String").Type(): + args.secretString = aux.Get().(string) + case h.reg.Type("Reader").Type(): + args.secretStream = aux.Get().(io.Reader) + } + } + + var results *jwtGenerateResults + if results, err = h.generate(ctx, args); err != nil { + return + } + + out = &expr.Vars{} + + { + // converting results.Token (string) to String + var ( + tval expr.TypedValue + ) + + if tval, err = h.reg.Type("String").Cast(results.Token); err != nil { + return + } else if err = expr.Assign(out, "token", tval); err != nil { + return + } + } + + return + }, + } +} diff --git a/automation/automation/jwt_handler.go b/automation/automation/jwt_handler.go new file mode 100644 index 000000000..d5c2cde96 --- /dev/null +++ b/automation/automation/jwt_handler.go @@ -0,0 +1,96 @@ +package automation + +import ( + "context" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "strings" + + "github.com/dgrijalva/jwt-go" +) + +type ( + jwtHandler struct { + reg jwtHandlerRegistry + } +) + +func JwtHandler(reg jwtHandlerRegistry) *jwtHandler { + h := &jwtHandler{ + reg: reg, + } + + h.register() + return h +} + +func (h jwtHandler) generate(ctx context.Context, args *jwtGenerateArgs) (res *jwtGenerateResults, err error) { + var ( + secret interface{} + + auxp = make(map[string]interface{}) + auxh = make(map[string]interface{}) + ) + + if !args.hasPayload { + err = fmt.Errorf("could not generate JWT, payload missing") + return + } + + if !args.hasSecret { + err = fmt.Errorf("could not generate JWT, secret or cert missing") + return + } + + for k, v := range args.headerVars { + auxp[k] = v.Get() + } + + for k, v := range args.payloadVars { + auxp[k] = v.Get() + } + + if args.payloadString != "" { + if err = json.Unmarshal([]byte(args.payloadString), &auxp); err != nil { + return + } + } + + if args.headerString != "" { + if err = json.Unmarshal([]byte(args.headerString), &auxh); err != nil { + return + } + } + + // check for delimiters + auxp["scope"] = strings.FieldsFunc(args.Scope, func(r rune) bool { + return r == ' ' || r == ',' + }) + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims(auxp)) + + // merge header with user-provided header + for k, v := range auxh { + token.Header[k] = v + } + + // check if we use cert + { + pemBlock, _ := pem.Decode([]byte(args.secretString)) + + if pemBlock != nil { + if secret, err = x509.ParsePKCS8PrivateKey(pemBlock.Bytes); err != nil { + return + } + } else { + secret = args.secretString + } + } + + res = &jwtGenerateResults{} + res.Token, err = token.SignedString(secret) + + return +} diff --git a/automation/automation/jwt_handler.yaml b/automation/automation/jwt_handler.yaml new file mode 100644 index 000000000..3eab0c147 --- /dev/null +++ b/automation/automation/jwt_handler.yaml @@ -0,0 +1,30 @@ +imports: + - io + +functions: + generate: + meta: + short: Generate JWT + params: + scope: + require: false + types: + - { wf: String } + header: + required: true + types: + - { wf: Vars, suffix: Vars, go: 'map[string]expr.TypedValue' } + - { wf: String, suffix: String } + payload: + required: true + types: + - { wf: Vars, suffix: Vars, go: 'map[string]expr.TypedValue' } + - { wf: String, suffix: String } + secret: + required: true + types: + - { wf: String, suffix: String } + - { wf: Reader, suffix: Stream } + results: + token: + wf: String diff --git a/automation/service/service.go b/automation/service/service.go index 0d940a195..84a5cc6e1 100644 --- a/automation/service/service.go +++ b/automation/service/service.go @@ -125,6 +125,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, ws websock automation.LoopHandler(Registry(), DefaultWorkflow.parser) automation.CorredorHandler(Registry(), corredor.Service()) automation.EmailHandler(Registry()) + automation.JwtHandler(Registry()) return } diff --git a/pkg/expr/func_num.go b/pkg/expr/func_num.go index 63a743d27..fde3a357a 100644 --- a/pkg/expr/func_num.go +++ b/pkg/expr/func_num.go @@ -1,10 +1,12 @@ package expr import ( - "github.com/PaesslerAG/gval" - "github.com/pkg/errors" "math" "math/rand" + + "github.com/PaesslerAG/gval" + "github.com/pkg/errors" + "github.com/spf13/cast" ) func NumericFunctions() []gval.Language { @@ -21,6 +23,7 @@ func NumericFunctions() []gval.Language { gval.Function("sum", sum), gval.Function("average", average), gval.Function("random", random), + gval.Function("int", toInt64), } } @@ -141,3 +144,7 @@ func random(v ...float64) (out float64, err error) { out = from + rand.Float64()*(to-from) return } + +func toInt64(aa interface{}) (i int64) { + return cast.ToInt64(aa) +} diff --git a/pkg/expr/func_num_test.go b/pkg/expr/func_num_test.go index feb448694..389b9a662 100644 --- a/pkg/expr/func_num_test.go +++ b/pkg/expr/func_num_test.go @@ -90,3 +90,24 @@ func Example_randomWithTwoInput() { // output: // 1 } + +func Example_castStringToInt() { + eval(`int("abc")`, nil) + + // output: + // 0 +} + +func Example_castStringNumberToInt() { + eval(`int("42690")`, nil) + + // output: + // 42690 +} + +func Example_castFloatToInt() { + eval(`int("42.690")`, nil) + + // output: + // 0 +} diff --git a/pkg/expr/func_time.go b/pkg/expr/func_time.go index 84b9877a6..0b63e94c7 100644 --- a/pkg/expr/func_time.go +++ b/pkg/expr/func_time.go @@ -190,10 +190,15 @@ func prepMod(base interface{}, mod interface{}) (*time.Time, int, error) { // https://github.com/lestrrat-go/strftime#supported-conversion-specifications func strfTime(base interface{}, f string) (string, error) { t, _, err := prepMod(base, 0) + if err != nil { return "", err } - o, _ := strftime.Format(f, *t, strftime.WithMilliseconds('b')) + + o, _ := strftime.Format(f, *t, + strftime.WithMilliseconds('b'), + strftime.WithUnixSeconds('L')) + return o, nil } diff --git a/pkg/expr/func_time_test.go b/pkg/expr/func_time_test.go index 9ccbfbd12..d333a47e1 100644 --- a/pkg/expr/func_time_test.go +++ b/pkg/expr/func_time_test.go @@ -134,3 +134,10 @@ func Example_subErrorCheck() { // output: // error: can not evaluate sub(ghd, hgp): expecting 2nd input to be less than 1st input } + +func Example_unix() { + eval(`strftime(ghd, "%L")`, exampleTimeParams) + + // output: + // 728650800 +} diff --git a/tests/workflows/jwt_generate_test.go b/tests/workflows/jwt_generate_test.go new file mode 100644 index 000000000..95753c55d --- /dev/null +++ b/tests/workflows/jwt_generate_test.go @@ -0,0 +1,34 @@ +package workflows + +import ( + "context" + _ "embed" + "strings" + "testing" + + "github.com/cortezaproject/corteza-server/automation/types" + "github.com/stretchr/testify/require" +) + +//go:embed testdata/jwt_generate/match_token +var tkn string + +func Test_jwt_generate(t *testing.T) { + var ( + ctx = bypassRBAC(context.Background()) + req = require.New(t) + + aux = struct { + Token string + }{} + ) + + req.NoError(defStore.TruncateAttachments(ctx)) + + loadNewScenario(ctx, t) + + vars, _ := mustExecWorkflow(ctx, t, "jwt_generate", types.WorkflowExecParams{}) + + req.NoError(vars.Decode(&aux)) + req.Equal(strings.TrimSuffix(tkn, "\n"), aux.Token) +} diff --git a/tests/workflows/testdata/jwt_generate/match_token b/tests/workflows/testdata/jwt_generate/match_token new file mode 100644 index 000000000..a5b1fc691 --- /dev/null +++ b/tests/workflows/testdata/jwt_generate/match_token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsImZvbyI6ImJhciIsInR5cCI6IkpXVCJ9.eyJleHAiOjcyODY1NDQwMCwiaWF0Ijo3Mjg2NTA4MDAsInNjb3BlIjpbXSwic3ViIjoiMTIzNDU2Nzg5MCJ9.Tk19aAqkcSuVd54OHSSX6VOjLk4MFhxoeufiEDGo8U7wtsW6EL3YGFCiWtcMpjrK03aNVUqQwue2BjB8ysqq3NsNvFVWiJFDUZR-4jaRHrz4FALZpb43do95YQFhJHliAnzmAEgbFQY704FTd9PYP9IGuw0EUwq_WOAtIiaKNHG20rPIRvltr_p1t54rDG0Xx_9iGCU-J-GJSWuYV7-XNHsdz0xJqxRAUeaH4Ly5BzjHBX8HSB0VezopZly2sJDlqfZCUBkL80eMvECJWc7g4znN9dPHKDcKclqvFI5sPSdQIQ7zZVeSbTizqXe-KXz9TykVTDOudnY4iRXzNerZ0_rzq7qT253qGoypkM0_N3oidFU0UkSnLgWIyeuw09bFraDyWuYEhYbsDif7qT8tP-w6YaKv0mFZ8BwRKl6yZafR8Ji7UNWhVQ3Qz8rJjAclX4elEFGq0deK7B2Kiwo6U33tNyx2Xi56QRMs6r26S9QoA3fNbA7C2HrFwy3coZxyJAKVCAHcfYOue-u1oersq0vAk_HGXytBrdiz6xL6wbRjNqfrT--I-uq2MfUEsZSh2Xy0SCEUt8IJeX6vt7nGeKqu6AUuKbCpA6CrXRI7K9jShuAsFeyTdKKjJsKCXwZRQp9ZyTmWYXcyXlDHf63I8JXNP2wzPM_3p0CCQ313S_Q diff --git a/tests/workflows/testdata/jwt_generate/workflow.yaml b/tests/workflows/testdata/jwt_generate/workflow.yaml new file mode 100644 index 000000000..6ad0d27f2 --- /dev/null +++ b/tests/workflows/testdata/jwt_generate/workflow.yaml @@ -0,0 +1,93 @@ +workflows: + jwt_generate: + enabled: true + trace: true + + triggers: + - enabled: true + stepID: 5 + + steps: + - stepID: 5 + kind: function + ref: jwtGenerate + + arguments: + - target: scope + type: String + + - target: header + value: |- + { + "foo": "bar" + } + type: String + + - target: payload + expr: |- + { + "sub": "1234567890", + "iat": int(strftime(date("1993-02-02T06:00:00-05:00"), "%L")), + "exp": int(strftime(modTime(date("1993-02-02T06:00:00-05:00"), "+1h"), "%L")) + } + type: Vars + + - target: secret + value: |- + -----BEGIN PRIVATE KEY----- + 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 + C/p2c759krBsxv/zOxJJsWDEF1AolQI= + -----END PRIVATE KEY----- + type: String + + results: + - target: token + expr: token