From b9833aa6048ada6b62dd4f0c828773115b099828 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Wed, 25 Mar 2020 11:34:49 +0100 Subject: [PATCH 01/20] change ports --- cmd/zitadel/startup.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/zitadel/startup.yaml b/cmd/zitadel/startup.yaml index 9a8a4bce69..c84f9015a1 100644 --- a/cmd/zitadel/startup.yaml +++ b/cmd/zitadel/startup.yaml @@ -12,30 +12,30 @@ Log: Mgmt: API: GRPC: - ServerPort: 60020 - GatewayPort: 60021 + ServerPort: 50010 + GatewayPort: 50011 CustomHeaders: - x-caos- Auth: API: GRPC: - ServerPort: 60050 - GatewayPort: 60051 + ServerPort: 50020 + GatewayPort: 50021 CustomHeaders: - x-caos- Login: - +# will get port range 5003x Admin: API: GRPC: - ServerPort: 60090 - GatewayPort: 60091 + ServerPort: 50040 + GatewayPort: 50041 CustomHeaders: - x-caos- Console: - Port: '9090' - StaticDir: '/app/console/dist' + Port: 50050 + StaticDir: /app/console/dist From 0faaaa98b889cacde3da59adabc94753438ea830 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Fri, 27 Mar 2020 13:33:44 +0100 Subject: [PATCH 02/20] docs: logo --- README.md | 4 +++- raw/img/zitadel-logo-01-lightdesign@2x.png | Bin 0 -> 6906 bytes 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 raw/img/zitadel-logo-01-lightdesign@2x.png diff --git a/README.md b/README.md index eb9c8971e9..f78af629d0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# Zitadel +![ZITADEL](./raw/img/zitadel-logo-01-lightdesign@2x.png) + +# ZITADEL [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) ![Release Badge](https://github.com/caos/zitadel/workflows/Test,%20Build,%20Release/badge.svg) diff --git a/raw/img/zitadel-logo-01-lightdesign@2x.png b/raw/img/zitadel-logo-01-lightdesign@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9ff71a999054d789689b0428862544a5bf40a512 GIT binary patch literal 6906 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 r($o(b=L6@ Date: Fri, 27 Mar 2020 13:44:50 +0100 Subject: [PATCH 03/20] clarify array_flag.go --- internal/config/array_flag.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/config/array_flag.go b/internal/config/array_flag.go index bb99d0a52c..5a779545d0 100644 --- a/internal/config/array_flag.go +++ b/internal/config/array_flag.go @@ -1,7 +1,14 @@ package config -import "strings" +import ( + "flag" + "strings" +) +var _ flag.Value = (*ArrayFlags)(nil) + +//ArrayFlags implements the flag/Value interface +//allowing to set multiple string flags with the same name type ArrayFlags []string func (i *ArrayFlags) String() string { From f280da5a760972ea8ef9631be0d2008448552c97 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 27 Mar 2020 13:45:08 +0100 Subject: [PATCH 04/20] remove empty lines --- internal/api/auth/authorization_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/api/auth/authorization_test.go b/internal/api/auth/authorization_test.go index 5282e10c92..ac1193385e 100644 --- a/internal/api/auth/authorization_test.go +++ b/internal/api/auth/authorization_test.go @@ -210,7 +210,6 @@ func Test_GetFieldFromReq(t *testing.T) { } func Test_HasGlobalPermission(t *testing.T) { - type args struct { perms []string } @@ -245,7 +244,6 @@ func Test_HasGlobalPermission(t *testing.T) { } func Test_GetPermissionCtxIDs(t *testing.T) { - type args struct { perms []string } From f5af4461ad812bf3d4db2f83a84fdece000270e9 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 27 Mar 2020 13:57:16 +0100 Subject: [PATCH 05/20] remove pointers on configs --- cmd/zitadel/main.go | 12 ++++++------ internal/api/grpc/config.go | 8 ++++---- pkg/admin/admin.go | 2 +- pkg/auth/auth.go | 2 +- pkg/console/console.go | 2 +- pkg/login/login.go | 6 +++--- pkg/management/api/config.go | 2 +- pkg/management/management.go | 6 +++--- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/cmd/zitadel/main.go b/cmd/zitadel/main.go index e56940361d..5d0b1e6a9c 100644 --- a/cmd/zitadel/main.go +++ b/cmd/zitadel/main.go @@ -16,15 +16,15 @@ import ( ) type Config struct { - Mgmt *management.Config - Auth *auth.Config - Login *login.Config - Admin *admin.Config - Console *console.Config + Mgmt management.Config + Auth auth.Config + Login login.Config + Admin admin.Config + Console console.Config //Log //TODO: add //Tracing tracing.TracingConfig //TODO: add - AuthZ *authz.Config + AuthZ authz.Config } func main() { diff --git a/internal/api/grpc/config.go b/internal/api/grpc/config.go index 1b3179d5a5..03b743fdc1 100644 --- a/internal/api/grpc/config.go +++ b/internal/api/grpc/config.go @@ -6,14 +6,14 @@ type Config struct { CustomHeaders []string } -func (c *Config) ToServerConfig() *ServerConfig { - return &ServerConfig{ +func (c Config) ToServerConfig() ServerConfig { + return ServerConfig{ Port: c.ServerPort, } } -func (c *Config) ToGatewayConfig() *GatewayConfig { - return &GatewayConfig{ +func (c Config) ToGatewayConfig() GatewayConfig { + return GatewayConfig{ Port: c.GatewayPort, GRPCEndpoint: c.ServerPort, CustomHeaders: c.CustomHeaders, diff --git a/pkg/admin/admin.go b/pkg/admin/admin.go index 968bb7fa75..bdf9010253 100644 --- a/pkg/admin/admin.go +++ b/pkg/admin/admin.go @@ -14,6 +14,6 @@ type Config struct { API *api.Config } -func Start(ctx context.Context, config *Config, authZ *auth.Config) error { +func Start(ctx context.Context, config Config, authZ auth.Config) error { return errors.ThrowUnimplemented(nil, "ADMIN-n8vw5", "not implemented yet") //TODO: implement } diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 21f3229f67..43527dc43b 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -14,6 +14,6 @@ type Config struct { API *api.Config } -func Start(ctx context.Context, config *Config, authZ *auth.Config) error { +func Start(ctx context.Context, config Config, authZ auth.Config) error { return errors.ThrowUnimplemented(nil, "AUTH-l7Hdx", "not implemented yet") //TODO: implement } diff --git a/pkg/console/console.go b/pkg/console/console.go index 8a17d33c2f..663a1197a7 100644 --- a/pkg/console/console.go +++ b/pkg/console/console.go @@ -11,6 +11,6 @@ type Config struct { StaticDir string } -func Start(ctx context.Context, config *Config) error { +func Start(ctx context.Context, config Config) error { return errors.ThrowUnimplemented(nil, "CONSO-4cT5D", "not implemented yet") //TODO: implement } diff --git a/pkg/login/login.go b/pkg/login/login.go index 8a593092a3..462ba37aa1 100644 --- a/pkg/login/login.go +++ b/pkg/login/login.go @@ -9,10 +9,10 @@ import ( ) type Config struct { - App *app.Config - API *api.Config + App app.Config + API api.Config } -func Start(ctx context.Context, config *Config) error { +func Start(ctx context.Context, config Config) error { return errors.ThrowUnimplemented(nil, "LOGIN-3fwvD", "not implemented yet") //TODO: implement } diff --git a/pkg/management/api/config.go b/pkg/management/api/config.go index b63086cc83..8fce0aca62 100644 --- a/pkg/management/api/config.go +++ b/pkg/management/api/config.go @@ -3,5 +3,5 @@ package api import "github.com/caos/zitadel/internal/api/grpc" type Config struct { - GRPC *grpc.Config + GRPC grpc.Config } diff --git a/pkg/management/management.go b/pkg/management/management.go index 2ba24ff227..9f6af48229 100644 --- a/pkg/management/management.go +++ b/pkg/management/management.go @@ -10,10 +10,10 @@ import ( ) type Config struct { - App *app.Config - API *api.Config + App app.Config + API api.Config } -func Start(ctx context.Context, config *Config, authZ *auth.Config) error { +func Start(ctx context.Context, config Config, authZ auth.Config) error { return errors.ThrowUnimplemented(nil, "MANAG-h3k3x", "not implemented yet") //TODO: implement } From 993e6dd97e3006f6ff7d4d5479b17f130a464f28 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Fri, 27 Mar 2020 14:20:32 +0100 Subject: [PATCH 06/20] docs: goreport --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f78af629d0..17e7c6f63f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ ![Release Badge](https://github.com/caos/zitadel/workflows/Test,%20Build,%20Release/badge.svg) [![GitHub license](https://img.shields.io/github/license/caos/zitadel)](https://github.com/caos/zitadel/blob/master/LICENSE) [![GitHub release](https://img.shields.io/github/release/caos/zitadel)](https://gitHub.com/caos/zitadel/releases/) +[![Go Report Card](https://goreportcard.com/badge/github.com/caos/zitadel)](https://goreportcard.com/report/github.com/caos/zitadel) > This project is in alpha state. The API will continue breaking until version 1.0.0 is released From b8eaf58427a5c5413f595357cef48a08cd7abc75 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Fri, 27 Mar 2020 14:35:06 +0100 Subject: [PATCH 07/20] docs: use correct logo --- README.md | 4 ++-- raw/img/zitadel-logo-01-lightdesign@2x.png | Bin 6906 -> 0 bytes raw/img/zitadel-logo-oneline-lightdesign@2x.png | Bin 0 -> 52791 bytes 3 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 raw/img/zitadel-logo-01-lightdesign@2x.png create mode 100644 raw/img/zitadel-logo-oneline-lightdesign@2x.png diff --git a/README.md b/README.md index 17e7c6f63f..756974021f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -![ZITADEL](./raw/img/zitadel-logo-01-lightdesign@2x.png) +![ZITADEL](./raw/img/zitadel-logo-oneline-lightdesign@2x.png) -# ZITADEL +# Zitadel [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) ![Release Badge](https://github.com/caos/zitadel/workflows/Test,%20Build,%20Release/badge.svg) diff --git a/raw/img/zitadel-logo-01-lightdesign@2x.png b/raw/img/zitadel-logo-01-lightdesign@2x.png deleted file mode 100644 index 9ff71a999054d789689b0428862544a5bf40a512..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6906 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

>l17ll;g(O0i#>P{aLE>&5gDb{J;ZMrIb@jF%(C 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 Date: Mon, 30 Mar 2020 07:04:21 +0200 Subject: [PATCH 08/20] improve some functions --- internal/api/html/i18n.go | 14 +++++++------- internal/api/html/renderer.go | 5 ++--- internal/api/http/cookie.go | 5 +---- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/internal/api/html/i18n.go b/internal/api/html/i18n.go index 17f1cb4e14..79b35ccc2e 100644 --- a/internal/api/html/i18n.go +++ b/internal/api/html/i18n.go @@ -98,12 +98,12 @@ func (t *Translator) localizer(langs ...string) *i18n.Localizer { func (t *Translator) langsFromRequest(r *http.Request) []string { langs := make([]string, 0) - if r != nil { - lang, err := t.cookieHandler.GetCookieValue(r, t.cookieName) - if err == nil { - langs = append(langs, lang) - } - langs = append(langs, r.Header.Get(api.AcceptLanguage)) + if r == nil { + return langs } - return langs + lang, err := t.cookieHandler.GetCookieValue(r, t.cookieName) + if err == nil { + langs = append(langs, lang) + } + return append(langs, r.Header.Get(api.AcceptLanguage)) } diff --git a/internal/api/html/renderer.go b/internal/api/html/renderer.go index 182087e31d..97a684263a 100644 --- a/internal/api/html/renderer.go +++ b/internal/api/html/renderer.go @@ -31,9 +31,8 @@ func NewRenderer(templatesDir string, tmplMapping map[string]string, funcs map[s func (r *Renderer) RenderTemplate(w http.ResponseWriter, req *http.Request, tmpl *template.Template, data interface{}, reqFuncs map[string]interface{}) { reqFuncs = r.registerTranslateFn(req, reqFuncs) - if err := tmpl.Funcs(reqFuncs).Execute(w, data); err != nil { - logging.Log("HTML-lF8F6w").WithError(err).WithField("template", tmpl.Name).Error("error rendering template") - } + err := tmpl.Funcs(reqFuncs).Execute(w, data) + logging.LogWithFields("HTML-lF8F6w", "template", tmpl.Name).OnError(err).Error("error rendering template") } func (r *Renderer) Localize(id string, args map[string]interface{}) string { diff --git a/internal/api/http/cookie.go b/internal/api/http/cookie.go index a28791aafc..f2e28b5294 100644 --- a/internal/api/http/cookie.go +++ b/internal/api/http/cookie.go @@ -85,10 +85,7 @@ func (c *CookieHandler) GetEncryptedCookieValue(r *http.Request, name string, va if c.securecookie == nil { return errors.ThrowInternal(nil, "HTTP-X6XpnL", "securecookie not configured") } - if err := c.securecookie.Decode(name, cookie.Value, value); err != nil { - return err - } - return nil + return c.securecookie.Decode(name, cookie.Value, value) } func (c *CookieHandler) SetCookie(w http.ResponseWriter, name string, value string) { From fa750a506829774a4418e6c78b89f92f37815af2 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 30 Mar 2020 07:23:43 +0200 Subject: [PATCH 09/20] improve some functions --- internal/proto/struct.go | 7 +------ internal/tracing/caller.go | 10 ++++------ internal/tracing/config/config.go | 12 +++++------- internal/tracing/google/config.go | 5 ++--- internal/tracing/span.go | 4 +++- pkg/admin/admin.go | 4 ++-- pkg/admin/api/config.go | 2 +- pkg/auth/api/config.go | 2 +- pkg/auth/auth.go | 4 ++-- 9 files changed, 21 insertions(+), 29 deletions(-) diff --git a/internal/proto/struct.go b/internal/proto/struct.go index 24a6ce0e36..f1b5eb29f9 100644 --- a/internal/proto/struct.go +++ b/internal/proto/struct.go @@ -13,7 +13,6 @@ import ( func MustToPBStruct(object interface{}) *pb_struct.Struct { s, err := ToPBStruct(object) logging.Log("PROTO-7Aa3t").OnError(err).Panic("unable to map object to pb-struct") - return s } @@ -24,15 +23,12 @@ func BytesToPBStruct(b []byte) (*pb_struct.Struct, error) { } func ToPBStruct(object interface{}) (*pb_struct.Struct, error) { - fields := new(pb_struct.Struct) - marshalled, err := json.Marshal(object) if err != nil { return nil, err } - + fields := new(pb_struct.Struct) err = jsonpb.Unmarshal(bytes.NewReader(marshalled), fields) - return fields, err } @@ -47,6 +43,5 @@ func FromPBStruct(object interface{}, s *pb_struct.Struct) error { if err != nil { return err } - return json.Unmarshal([]byte(jsonString), object) } diff --git a/internal/tracing/caller.go b/internal/tracing/caller.go index c5dfb9a319..c8ab9071b0 100644 --- a/internal/tracing/caller.go +++ b/internal/tracing/caller.go @@ -8,17 +8,15 @@ import ( func GetCaller() string { fpcs := make([]uintptr, 1) - n := runtime.Callers(3, fpcs) if n == 0 { - logging.Log("HELPE-rWjfC").Debug("no caller") + logging.Log("TRACE-rWjfC").Debug("no caller") + return "" } - caller := runtime.FuncForPC(fpcs[0] - 1) if caller == nil { - logging.Log("HELPE-25POw").Debug("caller was nil") + logging.Log("TRACE-25POw").Debug("caller was nil") + return "" } - - // Print the name of the function return caller.Name() } diff --git a/internal/tracing/config/config.go b/internal/tracing/config/config.go index e2310fbce3..3681ddf107 100644 --- a/internal/tracing/config/config.go +++ b/internal/tracing/config/config.go @@ -3,9 +3,7 @@ package config import ( "encoding/json" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/tracing" tracing_g "github.com/caos/zitadel/internal/tracing/google" tracing_log "github.com/caos/zitadel/internal/tracing/log" @@ -28,7 +26,7 @@ func (c *TracingConfig) UnmarshalJSON(data []byte) error { } if err := json.Unmarshal(data, &rc); err != nil { - return status.Errorf(codes.Internal, "%v parse config: %v", "TRACE-vmjS", err) + return errors.ThrowInternal(err, "TRACE-vmjS", "error parsing config") } c.Type = rc.Type @@ -36,7 +34,7 @@ func (c *TracingConfig) UnmarshalJSON(data []byte) error { var err error c.Config, err = newTracingConfig(c.Type, rc.Config) if err != nil { - return status.Errorf(codes.Internal, "%v parse config: %v", "TRACE-Ws9E", err) + return err } return c.Config.NewTracer() @@ -45,7 +43,7 @@ func (c *TracingConfig) UnmarshalJSON(data []byte) error { func newTracingConfig(tracerType string, configData []byte) (tracing.Config, error) { t, ok := tracer[tracerType] if !ok { - return nil, status.Errorf(codes.Internal, "%v No config: %v", "TRACE-HMEJ", tracerType) + return nil, errors.ThrowInternalf(nil, "TRACE-HMEJ", "config type %s not supported", tracerType) } tracingConfig := t() @@ -54,7 +52,7 @@ func newTracingConfig(tracerType string, configData []byte) (tracing.Config, err } if err := json.Unmarshal(configData, tracingConfig); err != nil { - return nil, status.Errorf(codes.Internal, "%v Could not read conifg: %v", "TRACE-1tSS", err) + return nil, errors.ThrowInternal(err, "TRACE-1tSS", "Could not read config: %v") } return tracingConfig, nil diff --git a/internal/tracing/google/config.go b/internal/tracing/google/config.go index 92d93f61bd..64ee3223ba 100644 --- a/internal/tracing/google/config.go +++ b/internal/tracing/google/config.go @@ -2,9 +2,8 @@ package google import ( "go.opencensus.io/trace" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/tracing" ) @@ -16,7 +15,7 @@ type Config struct { func (c *Config) NewTracer() error { if !envIsSet() { - return status.Error(codes.InvalidArgument, "env not properly set, GOOGLE_APPLICATION_CREDENTIALS is misconfigured or missing") + return errors.ThrowInvalidArgument(nil, "GOOGL-sdh3a", "env not properly set, GOOGLE_APPLICATION_CREDENTIALS is misconfigured or missing") } tracing.T = &Tracer{projectID: c.ProjectID, metricPrefix: c.MetricPrefix, sampler: trace.ProbabilitySampler(c.Fraction)} diff --git a/internal/tracing/span.go b/internal/tracing/span.go index d958cb4b52..1ad46ca0b1 100644 --- a/internal/tracing/span.go +++ b/internal/tracing/span.go @@ -7,6 +7,8 @@ import ( "go.opencensus.io/trace" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + + "github.com/caos/zitadel/internal/errors" ) type Span struct { @@ -80,7 +82,7 @@ func toTraceAttribute(key string, value interface{}) (attr trace.Attribute, err if valueInt, err := convertToInt64(value); err == nil { return trace.Int64Attribute(key, valueInt), nil } - return attr, status.Error(codes.InvalidArgument, "Attribute is not of type bool, string or int64") + return attr, errors.ThrowInternal(nil, "TRACE-jlq3s", "Attribute is not of type bool, string or int64") } func convertToInt64(value interface{}) (int64, error) { diff --git a/pkg/admin/admin.go b/pkg/admin/admin.go index bdf9010253..0a18a4b145 100644 --- a/pkg/admin/admin.go +++ b/pkg/admin/admin.go @@ -10,8 +10,8 @@ import ( ) type Config struct { - App *app.Config - API *api.Config + App app.Config + API api.Config } func Start(ctx context.Context, config Config, authZ auth.Config) error { diff --git a/pkg/admin/api/config.go b/pkg/admin/api/config.go index b63086cc83..8fce0aca62 100644 --- a/pkg/admin/api/config.go +++ b/pkg/admin/api/config.go @@ -3,5 +3,5 @@ package api import "github.com/caos/zitadel/internal/api/grpc" type Config struct { - GRPC *grpc.Config + GRPC grpc.Config } diff --git a/pkg/auth/api/config.go b/pkg/auth/api/config.go index b63086cc83..8fce0aca62 100644 --- a/pkg/auth/api/config.go +++ b/pkg/auth/api/config.go @@ -3,5 +3,5 @@ package api import "github.com/caos/zitadel/internal/api/grpc" type Config struct { - GRPC *grpc.Config + GRPC grpc.Config } diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 43527dc43b..f8838f83c0 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -10,8 +10,8 @@ import ( ) type Config struct { - App *app.Config - API *api.Config + App app.Config + API api.Config } func Start(ctx context.Context, config Config, authZ auth.Config) error { From 59dc4dbe85485982e5123f70e843d1d95f1b1a18 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 30 Mar 2020 09:28:00 +0200 Subject: [PATCH 10/20] improve some functions --- internal/api/auth/permissions_test.go | 7 +- internal/crypto/aes.go | 2 +- internal/crypto/bcrypt.go | 2 +- internal/crypto/code.go | 16 ++--- internal/crypto/code_test.go | 6 +- internal/crypto/crypto.go | 24 +++---- internal/crypto/crypto_mock.go | 98 +++++++++++++-------------- internal/crypto/crypto_test.go | 10 +-- 8 files changed, 81 insertions(+), 84 deletions(-) diff --git a/internal/api/auth/permissions_test.go b/internal/api/auth/permissions_test.go index 6d0857d792..83e5a7f784 100644 --- a/internal/api/auth/permissions_test.go +++ b/internal/api/auth/permissions_test.go @@ -74,10 +74,8 @@ func Test_GetUserMethodPermissions(t *testing.T) { }, }, wantErr: true, - errFunc: func(err error) bool { - return caos_errs.IsUnauthenticated(err) - }, - result: []string{"project.read"}, + errFunc: caos_errs.IsUnauthenticated, + result: []string{"project.read"}, }, { name: "No Grants", @@ -393,7 +391,6 @@ func Test_AddRoleContextIDToPerm(t *testing.T) { } func Test_ExistisPerm(t *testing.T) { - type args struct { existing []string perm string diff --git a/internal/crypto/aes.go b/internal/crypto/aes.go index 31d3e46847..0aca064941 100644 --- a/internal/crypto/aes.go +++ b/internal/crypto/aes.go @@ -10,7 +10,7 @@ import ( "github.com/caos/zitadel/internal/errors" ) -var _ EncryptionAlg = (*AESCrypto)(nil) +var _ EncryptionAlgorithm = (*AESCrypto)(nil) type AESCrypto struct { keys map[string]string diff --git a/internal/crypto/bcrypt.go b/internal/crypto/bcrypt.go index e7daffb27d..d9b172478f 100644 --- a/internal/crypto/bcrypt.go +++ b/internal/crypto/bcrypt.go @@ -4,7 +4,7 @@ import ( "golang.org/x/crypto/bcrypt" ) -var _ HashAlg = (*BCrypt)(nil) +var _ HashAlgorithm = (*BCrypt)(nil) type BCrypt struct { cost int diff --git a/internal/crypto/code.go b/internal/crypto/code.go index 0537710da6..8738aef074 100644 --- a/internal/crypto/code.go +++ b/internal/crypto/code.go @@ -24,7 +24,7 @@ type Generator interface { type EncryptionGenerator struct { length uint expiry time.Duration - alg EncryptionAlg + alg EncryptionAlgorithm runes []rune } @@ -44,7 +44,7 @@ func (g *EncryptionGenerator) Runes() []rune { return g.runes } -func NewEncryptionGenerator(length uint, expiry time.Duration, alg EncryptionAlg, runes []rune) *EncryptionGenerator { +func NewEncryptionGenerator(length uint, expiry time.Duration, alg EncryptionAlgorithm, runes []rune) *EncryptionGenerator { return &EncryptionGenerator{ length: length, expiry: expiry, @@ -56,7 +56,7 @@ func NewEncryptionGenerator(length uint, expiry time.Duration, alg EncryptionAlg type HashGenerator struct { length uint expiry time.Duration - alg HashAlg + alg HashAlgorithm runes []rune } @@ -76,7 +76,7 @@ func (g *HashGenerator) Runes() []rune { return g.runes } -func NewHashGenerator(length uint, expiry time.Duration, alg HashAlg, runes []rune) *HashGenerator { +func NewHashGenerator(length uint, expiry time.Duration, alg HashAlgorithm, runes []rune) *HashGenerator { return &HashGenerator{ length: length, expiry: expiry, @@ -106,9 +106,9 @@ func VerifyCode(creationDate time.Time, expiry time.Duration, cryptoCode *Crypto return errors.ThrowPreconditionFailed(nil, "CODE-QvUQ4P", "verification code is expired") } switch alg := g.Alg().(type) { - case EncryptionAlg: + case EncryptionAlgorithm: return verifyEncryptedCode(cryptoCode, verificationCode, alg) - case HashAlg: + case HashAlgorithm: return verifyHashedCode(cryptoCode, verificationCode, alg) } return errors.ThrowInvalidArgument(nil, "CODE-fW2gNa", "generator alg is not supported") @@ -136,7 +136,7 @@ func generateRandomString(length uint, chars []rune) (string, error) { return "", nil } -func verifyEncryptedCode(cryptoCode *CryptoValue, verificationCode string, alg EncryptionAlg) error { +func verifyEncryptedCode(cryptoCode *CryptoValue, verificationCode string, alg EncryptionAlgorithm) error { code, err := DecryptString(cryptoCode, alg) if err != nil { return err @@ -148,6 +148,6 @@ func verifyEncryptedCode(cryptoCode *CryptoValue, verificationCode string, alg E return nil } -func verifyHashedCode(cryptoCode *CryptoValue, verificationCode string, alg HashAlg) error { +func verifyHashedCode(cryptoCode *CryptoValue, verificationCode string, alg HashAlgorithm) error { return CompareHash(cryptoCode, []byte(verificationCode), alg) } diff --git a/internal/crypto/code_test.go b/internal/crypto/code_test.go index f7f55390e7..4f62f22898 100644 --- a/internal/crypto/code_test.go +++ b/internal/crypto/code_test.go @@ -9,7 +9,7 @@ import ( ) func Test_Encrypted_OK(t *testing.T) { - mCrypto := NewMockEncryptionAlg(gomock.NewController(t)) + mCrypto := NewMockEncryptionAlgorithm(gomock.NewController(t)) mCrypto.EXPECT().Algorithm().AnyTimes().Return("enc") mCrypto.EXPECT().EncryptionKeyID().AnyTimes().Return("id") mCrypto.EXPECT().DecryptionKeyIDs().AnyTimes().Return([]string{"id"}) @@ -35,7 +35,7 @@ func Test_Encrypted_OK(t *testing.T) { } func Test_Verify_Encrypted_OK(t *testing.T) { - mCrypto := NewMockEncryptionAlg(gomock.NewController(t)) + mCrypto := NewMockEncryptionAlgorithm(gomock.NewController(t)) mCrypto.EXPECT().Algorithm().AnyTimes().Return("enc") mCrypto.EXPECT().EncryptionKeyID().AnyTimes().Return("id") mCrypto.EXPECT().DecryptionKeyIDs().AnyTimes().Return([]string{"id"}) @@ -57,7 +57,7 @@ func Test_Verify_Encrypted_OK(t *testing.T) { assert.NoError(t, err) } func Test_Verify_Encrypted_Invalid_Err(t *testing.T) { - mCrypto := NewMockEncryptionAlg(gomock.NewController(t)) + mCrypto := NewMockEncryptionAlgorithm(gomock.NewController(t)) mCrypto.EXPECT().Algorithm().AnyTimes().Return("enc") mCrypto.EXPECT().EncryptionKeyID().AnyTimes().Return("id") mCrypto.EXPECT().DecryptionKeyIDs().AnyTimes().Return([]string{"id"}) diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go index 38ee06577b..c1b46d69d5 100644 --- a/internal/crypto/crypto.go +++ b/internal/crypto/crypto.go @@ -13,7 +13,7 @@ type Crypto interface { Algorithm() string } -type EncryptionAlg interface { +type EncryptionAlgorithm interface { Crypto EncryptionKeyID() string DecryptionKeyIDs() []string @@ -22,7 +22,7 @@ type EncryptionAlg interface { DecryptString(hashed []byte, keyID string) (string, error) } -type HashAlg interface { +type HashAlgorithm interface { Crypto Hash(value []byte) ([]byte, error) CompareHash(hashed, comparer []byte) error @@ -39,15 +39,15 @@ type CryptoType int func Crypt(value []byte, c Crypto) (*CryptoValue, error) { switch alg := c.(type) { - case EncryptionAlg: + case EncryptionAlgorithm: return Encrypt(value, alg) - case HashAlg: + case HashAlgorithm: return Hash(value, alg) } return nil, errors.ThrowInternal(nil, "CRYPT-r4IaHZ", "algorithm not supported") } -func Encrypt(value []byte, alg EncryptionAlg) (*CryptoValue, error) { +func Encrypt(value []byte, alg EncryptionAlgorithm) (*CryptoValue, error) { encrypted, err := alg.Encrypt(value) if err != nil { return nil, errors.ThrowInternal(err, "CRYPT-qCD0JB", "error encrypting value") @@ -60,21 +60,21 @@ func Encrypt(value []byte, alg EncryptionAlg) (*CryptoValue, error) { }, nil } -func Decrypt(value *CryptoValue, alg EncryptionAlg) ([]byte, error) { - if err := checkEncAlg(value, alg); err != nil { +func Decrypt(value *CryptoValue, alg EncryptionAlgorithm) ([]byte, error) { + if err := checkEncryptionAlgorithm(value, alg); err != nil { return nil, err } return alg.Decrypt(value.Crypted, value.KeyID) } -func DecryptString(value *CryptoValue, alg EncryptionAlg) (string, error) { - if err := checkEncAlg(value, alg); err != nil { +func DecryptString(value *CryptoValue, alg EncryptionAlgorithm) (string, error) { + if err := checkEncryptionAlgorithm(value, alg); err != nil { return "", err } return alg.DecryptString(value.Crypted, value.KeyID) } -func checkEncAlg(value *CryptoValue, alg EncryptionAlg) error { +func checkEncryptionAlgorithm(value *CryptoValue, alg EncryptionAlgorithm) error { if value.Algorithm != alg.Algorithm() { return errors.ThrowInvalidArgument(nil, "CRYPT-Nx7XlT", "value was encrypted with a different key") } @@ -86,7 +86,7 @@ func checkEncAlg(value *CryptoValue, alg EncryptionAlg) error { return errors.ThrowInvalidArgument(nil, "CRYPT-Kq12vn", "value was encrypted with a different key") } -func Hash(value []byte, alg HashAlg) (*CryptoValue, error) { +func Hash(value []byte, alg HashAlgorithm) (*CryptoValue, error) { hashed, err := alg.Hash(value) if err != nil { return nil, errors.ThrowInternal(err, "CRYPT-rBVaJU", "error hashing value") @@ -98,6 +98,6 @@ func Hash(value []byte, alg HashAlg) (*CryptoValue, error) { }, nil } -func CompareHash(value *CryptoValue, comparer []byte, alg HashAlg) error { +func CompareHash(value *CryptoValue, comparer []byte, alg HashAlgorithm) error { return alg.CompareHash(value.Crypted, comparer) } diff --git a/internal/crypto/crypto_mock.go b/internal/crypto/crypto_mock.go index 3785c285ae..439db21969 100644 --- a/internal/crypto/crypto_mock.go +++ b/internal/crypto/crypto_mock.go @@ -46,31 +46,31 @@ func (mr *MockCryptoMockRecorder) Algorithm() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Algorithm", reflect.TypeOf((*MockCrypto)(nil).Algorithm)) } -// MockEncryptionAlg is a mock of EncryptionAlg interface -type MockEncryptionAlg struct { +// MockEncryptionAlgorithm is a mock of EncryptionAlgorithm interface +type MockEncryptionAlgorithm struct { ctrl *gomock.Controller - recorder *MockEncryptionAlgMockRecorder + recorder *MockEncryptionAlgorithmMockRecorder } -// MockEncryptionAlgMockRecorder is the mock recorder for MockEncryptionAlg -type MockEncryptionAlgMockRecorder struct { - mock *MockEncryptionAlg +// MockEncryptionAlgorithmMockRecorder is the mock recorder for MockEncryptionAlgorithm +type MockEncryptionAlgorithmMockRecorder struct { + mock *MockEncryptionAlgorithm } -// NewMockEncryptionAlg creates a new mock instance -func NewMockEncryptionAlg(ctrl *gomock.Controller) *MockEncryptionAlg { - mock := &MockEncryptionAlg{ctrl: ctrl} - mock.recorder = &MockEncryptionAlgMockRecorder{mock} +// NewMockEncryptionAlgorithm creates a new mock instance +func NewMockEncryptionAlgorithm(ctrl *gomock.Controller) *MockEncryptionAlgorithm { + mock := &MockEncryptionAlgorithm{ctrl: ctrl} + mock.recorder = &MockEncryptionAlgorithmMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use -func (m *MockEncryptionAlg) EXPECT() *MockEncryptionAlgMockRecorder { +func (m *MockEncryptionAlgorithm) EXPECT() *MockEncryptionAlgorithmMockRecorder { return m.recorder } // Algorithm mocks base method -func (m *MockEncryptionAlg) Algorithm() string { +func (m *MockEncryptionAlgorithm) Algorithm() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Algorithm") ret0, _ := ret[0].(string) @@ -78,13 +78,13 @@ func (m *MockEncryptionAlg) Algorithm() string { } // Algorithm indicates an expected call of Algorithm -func (mr *MockEncryptionAlgMockRecorder) Algorithm() *gomock.Call { +func (mr *MockEncryptionAlgorithmMockRecorder) Algorithm() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Algorithm", reflect.TypeOf((*MockEncryptionAlg)(nil).Algorithm)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Algorithm", reflect.TypeOf((*MockEncryptionAlgorithm)(nil).Algorithm)) } // EncryptionKeyID mocks base method -func (m *MockEncryptionAlg) EncryptionKeyID() string { +func (m *MockEncryptionAlgorithm) EncryptionKeyID() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "EncryptionKeyID") ret0, _ := ret[0].(string) @@ -92,13 +92,13 @@ func (m *MockEncryptionAlg) EncryptionKeyID() string { } // EncryptionKeyID indicates an expected call of EncryptionKeyID -func (mr *MockEncryptionAlgMockRecorder) EncryptionKeyID() *gomock.Call { +func (mr *MockEncryptionAlgorithmMockRecorder) EncryptionKeyID() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EncryptionKeyID", reflect.TypeOf((*MockEncryptionAlg)(nil).EncryptionKeyID)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EncryptionKeyID", reflect.TypeOf((*MockEncryptionAlgorithm)(nil).EncryptionKeyID)) } // DecryptionKeyIDs mocks base method -func (m *MockEncryptionAlg) DecryptionKeyIDs() []string { +func (m *MockEncryptionAlgorithm) DecryptionKeyIDs() []string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DecryptionKeyIDs") ret0, _ := ret[0].([]string) @@ -106,13 +106,13 @@ func (m *MockEncryptionAlg) DecryptionKeyIDs() []string { } // DecryptionKeyIDs indicates an expected call of DecryptionKeyIDs -func (mr *MockEncryptionAlgMockRecorder) DecryptionKeyIDs() *gomock.Call { +func (mr *MockEncryptionAlgorithmMockRecorder) DecryptionKeyIDs() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecryptionKeyIDs", reflect.TypeOf((*MockEncryptionAlg)(nil).DecryptionKeyIDs)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecryptionKeyIDs", reflect.TypeOf((*MockEncryptionAlgorithm)(nil).DecryptionKeyIDs)) } // Encrypt mocks base method -func (m *MockEncryptionAlg) Encrypt(value []byte) ([]byte, error) { +func (m *MockEncryptionAlgorithm) Encrypt(value []byte) ([]byte, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Encrypt", value) ret0, _ := ret[0].([]byte) @@ -121,13 +121,13 @@ func (m *MockEncryptionAlg) Encrypt(value []byte) ([]byte, error) { } // Encrypt indicates an expected call of Encrypt -func (mr *MockEncryptionAlgMockRecorder) Encrypt(value interface{}) *gomock.Call { +func (mr *MockEncryptionAlgorithmMockRecorder) Encrypt(value interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Encrypt", reflect.TypeOf((*MockEncryptionAlg)(nil).Encrypt), value) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Encrypt", reflect.TypeOf((*MockEncryptionAlgorithm)(nil).Encrypt), value) } // Decrypt mocks base method -func (m *MockEncryptionAlg) Decrypt(hashed []byte, keyID string) ([]byte, error) { +func (m *MockEncryptionAlgorithm) Decrypt(hashed []byte, keyID string) ([]byte, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Decrypt", hashed, keyID) ret0, _ := ret[0].([]byte) @@ -136,13 +136,13 @@ func (m *MockEncryptionAlg) Decrypt(hashed []byte, keyID string) ([]byte, error) } // Decrypt indicates an expected call of Decrypt -func (mr *MockEncryptionAlgMockRecorder) Decrypt(hashed, keyID interface{}) *gomock.Call { +func (mr *MockEncryptionAlgorithmMockRecorder) Decrypt(hashed, keyID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Decrypt", reflect.TypeOf((*MockEncryptionAlg)(nil).Decrypt), hashed, keyID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Decrypt", reflect.TypeOf((*MockEncryptionAlgorithm)(nil).Decrypt), hashed, keyID) } // DecryptString mocks base method -func (m *MockEncryptionAlg) DecryptString(hashed []byte, keyID string) (string, error) { +func (m *MockEncryptionAlgorithm) DecryptString(hashed []byte, keyID string) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DecryptString", hashed, keyID) ret0, _ := ret[0].(string) @@ -151,36 +151,36 @@ func (m *MockEncryptionAlg) DecryptString(hashed []byte, keyID string) (string, } // DecryptString indicates an expected call of DecryptString -func (mr *MockEncryptionAlgMockRecorder) DecryptString(hashed, keyID interface{}) *gomock.Call { +func (mr *MockEncryptionAlgorithmMockRecorder) DecryptString(hashed, keyID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecryptString", reflect.TypeOf((*MockEncryptionAlg)(nil).DecryptString), hashed, keyID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecryptString", reflect.TypeOf((*MockEncryptionAlgorithm)(nil).DecryptString), hashed, keyID) } -// MockHashAlg is a mock of HashAlg interface -type MockHashAlg struct { +// MockHashAlgorithm is a mock of HashAlgorithm interface +type MockHashAlgorithm struct { ctrl *gomock.Controller - recorder *MockHashAlgMockRecorder + recorder *MockHashAlgorithmMockRecorder } -// MockHashAlgMockRecorder is the mock recorder for MockHashAlg -type MockHashAlgMockRecorder struct { - mock *MockHashAlg +// MockHashAlgorithmMockRecorder is the mock recorder for MockHashAlgorithm +type MockHashAlgorithmMockRecorder struct { + mock *MockHashAlgorithm } -// NewMockHashAlg creates a new mock instance -func NewMockHashAlg(ctrl *gomock.Controller) *MockHashAlg { - mock := &MockHashAlg{ctrl: ctrl} - mock.recorder = &MockHashAlgMockRecorder{mock} +// NewMockHashAlgorithm creates a new mock instance +func NewMockHashAlgorithm(ctrl *gomock.Controller) *MockHashAlgorithm { + mock := &MockHashAlgorithm{ctrl: ctrl} + mock.recorder = &MockHashAlgorithmMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use -func (m *MockHashAlg) EXPECT() *MockHashAlgMockRecorder { +func (m *MockHashAlgorithm) EXPECT() *MockHashAlgorithmMockRecorder { return m.recorder } // Algorithm mocks base method -func (m *MockHashAlg) Algorithm() string { +func (m *MockHashAlgorithm) Algorithm() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Algorithm") ret0, _ := ret[0].(string) @@ -188,13 +188,13 @@ func (m *MockHashAlg) Algorithm() string { } // Algorithm indicates an expected call of Algorithm -func (mr *MockHashAlgMockRecorder) Algorithm() *gomock.Call { +func (mr *MockHashAlgorithmMockRecorder) Algorithm() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Algorithm", reflect.TypeOf((*MockHashAlg)(nil).Algorithm)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Algorithm", reflect.TypeOf((*MockHashAlgorithm)(nil).Algorithm)) } // Hash mocks base method -func (m *MockHashAlg) Hash(value []byte) ([]byte, error) { +func (m *MockHashAlgorithm) Hash(value []byte) ([]byte, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Hash", value) ret0, _ := ret[0].([]byte) @@ -203,13 +203,13 @@ func (m *MockHashAlg) Hash(value []byte) ([]byte, error) { } // Hash indicates an expected call of Hash -func (mr *MockHashAlgMockRecorder) Hash(value interface{}) *gomock.Call { +func (mr *MockHashAlgorithmMockRecorder) Hash(value interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Hash", reflect.TypeOf((*MockHashAlg)(nil).Hash), value) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Hash", reflect.TypeOf((*MockHashAlgorithm)(nil).Hash), value) } // CompareHash mocks base method -func (m *MockHashAlg) CompareHash(hashed, comparer []byte) error { +func (m *MockHashAlgorithm) CompareHash(hashed, comparer []byte) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CompareHash", hashed, comparer) ret0, _ := ret[0].(error) @@ -217,7 +217,7 @@ func (m *MockHashAlg) CompareHash(hashed, comparer []byte) error { } // CompareHash indicates an expected call of CompareHash -func (mr *MockHashAlgMockRecorder) CompareHash(hashed, comparer interface{}) *gomock.Call { +func (mr *MockHashAlgorithmMockRecorder) CompareHash(hashed, comparer interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompareHash", reflect.TypeOf((*MockHashAlg)(nil).CompareHash), hashed, comparer) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompareHash", reflect.TypeOf((*MockHashAlgorithm)(nil).CompareHash), hashed, comparer) } diff --git a/internal/crypto/crypto_test.go b/internal/crypto/crypto_test.go index 76334884b9..fa5c4a5194 100644 --- a/internal/crypto/crypto_test.go +++ b/internal/crypto/crypto_test.go @@ -104,7 +104,7 @@ func TestCrypt(t *testing.T) { func TestEncrypt(t *testing.T) { type args struct { value []byte - c EncryptionAlg + c EncryptionAlgorithm } tests := []struct { name string @@ -136,7 +136,7 @@ func TestEncrypt(t *testing.T) { func TestDecrypt(t *testing.T) { type args struct { value *CryptoValue - c EncryptionAlg + c EncryptionAlgorithm } tests := []struct { name string @@ -174,7 +174,7 @@ func TestDecrypt(t *testing.T) { func TestDecryptString(t *testing.T) { type args struct { value *CryptoValue - c EncryptionAlg + c EncryptionAlgorithm } tests := []struct { name string @@ -212,7 +212,7 @@ func TestDecryptString(t *testing.T) { func TestHash(t *testing.T) { type args struct { value []byte - c HashAlg + c HashAlgorithm } tests := []struct { name string @@ -245,7 +245,7 @@ func TestCompareHash(t *testing.T) { type args struct { value *CryptoValue comparer []byte - c HashAlg + c HashAlgorithm } tests := []struct { name string From 501d45382224a06deac734d15fca1819a67ead4e Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 30 Mar 2020 09:58:55 +0200 Subject: [PATCH 11/20] remove x-grpc-web header in cors --- internal/api/http/middleware/cors_interceptor.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/api/http/middleware/cors_interceptor.go b/internal/api/http/middleware/cors_interceptor.go index 6fd154b469..379fbc38b7 100644 --- a/internal/api/http/middleware/cors_interceptor.go +++ b/internal/api/http/middleware/cors_interceptor.go @@ -18,7 +18,6 @@ var ( api.AcceptLanguage, api.Authorization, api.ZitadelOrgID, - "x-grpc-web", //TODO: needed }, AllowedMethods: []string{ http.MethodOptions, From e5e39b3a6a54e0e9a92ad9dddcf482a3c96b7784 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 30 Mar 2020 10:06:48 +0200 Subject: [PATCH 12/20] remove pointer on ctxData --- internal/api/auth/context.go | 20 +++++--------------- internal/api/auth/permissions_test.go | 2 +- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/internal/api/auth/context.go b/internal/api/auth/context.go index b17affec28..78ef0bc252 100644 --- a/internal/api/auth/context.go +++ b/internal/api/auth/context.go @@ -2,7 +2,6 @@ package auth import ( "context" - "time" "github.com/caos/logging" ) @@ -43,24 +42,15 @@ func VerifyTokenAndWriteCtxData(ctx context.Context, token, orgID string, t Toke projectID, err := t.GetProjectIDByClientID(ctx, clientID) logging.LogWithFields("AUTH-GfAoV", "clientID", clientID).OnError(err).Warn("could not read projectid by clientid") - return context.WithValue(ctx, CtxKeyData{}, &CtxData{UserID: userID, OrgID: orgID, ProjectID: projectID, AgentID: agentID}), nil + return context.WithValue(ctx, CtxKeyData{}, CtxData{UserID: userID, OrgID: orgID, ProjectID: projectID, AgentID: agentID}), nil } func GetCtxData(ctx context.Context) CtxData { - if data := ctx.Value(CtxKeyData{}); data != nil { - ctxData, ok := data.(*CtxData) - if ok { - return *ctxData - } - time.Now() - } - return CtxData{} + ctxData, _ := ctx.Value(CtxKeyData{}).(CtxData) + return ctxData } func GetPermissionsFromCtx(ctx context.Context) []string { - if data := ctx.Value(CtxKeyPermissions{}); data != nil { - ctxPermission, _ := data.([]string) - return ctxPermission - } - return nil + ctxPermission, _ := ctx.Value(CtxKeyPermissions{}).([]string) + return ctxPermission } diff --git a/internal/api/auth/permissions_test.go b/internal/api/auth/permissions_test.go index 83e5a7f784..0a433f43c6 100644 --- a/internal/api/auth/permissions_test.go +++ b/internal/api/auth/permissions_test.go @@ -8,7 +8,7 @@ import ( ) func getTestCtx(userID, orgID string) context.Context { - return context.WithValue(context.Background(), CtxKeyData{}, &CtxData{UserID: userID, OrgID: orgID}) + return context.WithValue(context.Background(), CtxKeyData{}, CtxData{UserID: userID, OrgID: orgID}) } type testVerifier struct { From 106a6ec1435a59f3e3df29a1c9f701f587e68df5 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 30 Mar 2020 10:09:38 +0200 Subject: [PATCH 13/20] fix test --- internal/api/auth/permissions_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/api/auth/permissions_test.go b/internal/api/auth/permissions_test.go index 0a433f43c6..d2afc36dfc 100644 --- a/internal/api/auth/permissions_test.go +++ b/internal/api/auth/permissions_test.go @@ -27,7 +27,7 @@ func (v *testVerifier) GetProjectIDByClientID(ctx context.Context, clientID stri return "", nil } -func EqualStringArray(a, b []string) bool { +func equalStringArray(a, b []string) bool { if len(a) != len(b) { return false } @@ -133,7 +133,7 @@ func Test_GetUserMethodPermissions(t *testing.T) { t.Errorf("got wrong err: %v ", err) } - if !tt.wantErr && !EqualStringArray(perms, tt.result) { + if !tt.wantErr && !equalStringArray(perms, tt.result) { t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, perms) } }) @@ -239,7 +239,7 @@ func Test_MapGrantsToPermissions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := mapGrantsToPermissions(tt.args.requiredPerm, tt.args.grants, tt.args.authConfig) - if !EqualStringArray(result, tt.result) { + if !equalStringArray(result, tt.result) { t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result) } }) @@ -346,7 +346,7 @@ func Test_MapRoleToPerm(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := mapRoleToPerm(tt.args.requiredPerm, tt.args.actualRole, tt.args.authConfig, tt.args.resolvedPermissions) - if !EqualStringArray(result, tt.result) { + if !equalStringArray(result, tt.result) { t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result) } }) From e04c0116f5e5c9d5ef53b83d43d862fddcb19d4a Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 30 Mar 2020 11:26:02 +0200 Subject: [PATCH 14/20] refactor crypto tests --- internal/crypto/code.go | 6 + internal/crypto/code_test.go | 376 +++++++++++++++++++++++++++++------ internal/crypto/crypto.go | 3 + internal/crypto/generate.go | 1 + 4 files changed, 328 insertions(+), 58 deletions(-) diff --git a/internal/crypto/code.go b/internal/crypto/code.go index 8738aef074..7cff29a746 100644 --- a/internal/crypto/code.go +++ b/internal/crypto/code.go @@ -137,6 +137,9 @@ func generateRandomString(length uint, chars []rune) (string, error) { } func verifyEncryptedCode(cryptoCode *CryptoValue, verificationCode string, alg EncryptionAlgorithm) error { + if cryptoCode == nil { + return errors.ThrowInvalidArgument(nil, "CRYPT-aqrFV", "cryptoCode must not be nil") + } code, err := DecryptString(cryptoCode, alg) if err != nil { return err @@ -149,5 +152,8 @@ func verifyEncryptedCode(cryptoCode *CryptoValue, verificationCode string, alg E } func verifyHashedCode(cryptoCode *CryptoValue, verificationCode string, alg HashAlgorithm) error { + if cryptoCode == nil { + return errors.ThrowInvalidArgument(nil, "CRYPT-2q3r", "cryptoCode must not be nil") + } return CompareHash(cryptoCode, []byte(verificationCode), alg) } diff --git a/internal/crypto/code_test.go b/internal/crypto/code_test.go index 4f62f22898..8e1947c2d8 100644 --- a/internal/crypto/code_test.go +++ b/internal/crypto/code_test.go @@ -5,10 +5,11 @@ import ( "time" "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/errors" ) -func Test_Encrypted_OK(t *testing.T) { +func createMockEncryptionAlg(t *testing.T) EncryptionAlgorithm { mCrypto := NewMockEncryptionAlgorithm(gomock.NewController(t)) mCrypto.EXPECT().Algorithm().AnyTimes().Return("enc") mCrypto.EXPECT().EncryptionKeyID().AnyTimes().Return("id") @@ -19,74 +20,333 @@ func Test_Encrypted_OK(t *testing.T) { }, ) mCrypto.EXPECT().DecryptString(gomock.Any(), gomock.Any()).DoAndReturn( - func(code []byte, _ string) (string, error) { + func(code []byte, keyID string) (string, error) { + if keyID != "id" { + return "", errors.ThrowInternal(nil, "id", "invalid key id") + } return string(code), nil }, ) - generator := NewEncryptionGenerator(6, 0, mCrypto, Digits) - - crypto, code, err := NewCode(generator) - assert.NoError(t, err) - - decrypted, err := DecryptString(crypto, generator.alg) - assert.NoError(t, err) - assert.Equal(t, code, decrypted) - assert.Equal(t, 6, len(decrypted)) + return mCrypto } -func Test_Verify_Encrypted_OK(t *testing.T) { - mCrypto := NewMockEncryptionAlgorithm(gomock.NewController(t)) - mCrypto.EXPECT().Algorithm().AnyTimes().Return("enc") - mCrypto.EXPECT().EncryptionKeyID().AnyTimes().Return("id") - mCrypto.EXPECT().DecryptionKeyIDs().AnyTimes().Return([]string{"id"}) - mCrypto.EXPECT().DecryptString(gomock.Any(), gomock.Any()).DoAndReturn( - func(code []byte, _ string) (string, error) { - return string(code), nil +func createMockHashAlg(t *testing.T) HashAlgorithm { + mCrypto := NewMockHashAlgorithm(gomock.NewController(t)) + mCrypto.EXPECT().Algorithm().AnyTimes().Return("hash") + mCrypto.EXPECT().Hash(gomock.Any()).DoAndReturn( + func(code []byte) ([]byte, error) { + return code, nil }, ) - creationDate := time.Now() - code := &CryptoValue{ - CryptoType: TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("code"), + mCrypto.EXPECT().CompareHash(gomock.Any(), gomock.Any()).DoAndReturn( + func(hashed, comparer []byte) error { + if string(hashed) != string(comparer) { + return errors.ThrowInternal(nil, "id", "invalid") + } + return nil + }, + ) + return mCrypto +} + +func createMockCrypto(t *testing.T) Crypto { + mCrypto := NewMockCrypto(gomock.NewController(t)) + mCrypto.EXPECT().Algorithm().AnyTimes().Return("crypto") + return mCrypto +} + +func createMockGenerator(t *testing.T, crypto Crypto) Generator { + mGenerator := NewMockGenerator(gomock.NewController(t)) + mGenerator.EXPECT().Alg().AnyTimes().Return(crypto) + return mGenerator +} + +func TestIsCodeExpired(t *testing.T) { + type args struct { + creationDate time.Time + expiry time.Duration } - generator := NewEncryptionGenerator(6, 0, mCrypto, Digits) - - err := VerifyCode(creationDate, 1*time.Hour, code, "code", generator) - assert.NoError(t, err) -} -func Test_Verify_Encrypted_Invalid_Err(t *testing.T) { - mCrypto := NewMockEncryptionAlgorithm(gomock.NewController(t)) - mCrypto.EXPECT().Algorithm().AnyTimes().Return("enc") - mCrypto.EXPECT().EncryptionKeyID().AnyTimes().Return("id") - mCrypto.EXPECT().DecryptionKeyIDs().AnyTimes().Return([]string{"id"}) - mCrypto.EXPECT().DecryptString(gomock.Any(), gomock.Any()).DoAndReturn( - func(code []byte, _ string) (string, error) { - return string(code), nil + tests := []struct { + name string + args args + want bool + }{ + { + "not expired", + args{ + creationDate: time.Now(), + expiry: time.Duration(5 * time.Minute), + }, + false, + }, + { + "expired", + args{ + creationDate: time.Now().Add(-5 * time.Minute), + expiry: time.Duration(5 * time.Minute), + }, + true, }, - ) - creationDate := time.Now() - code := &CryptoValue{ - CryptoType: TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("code"), } - generator := NewEncryptionGenerator(6, 0, mCrypto, Digits) - - err := VerifyCode(creationDate, 1*time.Hour, code, "wrong", generator) - assert.Error(t, err) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsCodeExpired(tt.args.creationDate, tt.args.expiry); got != tt.want { + t.Errorf("IsCodeExpired() = %v, want %v", got, tt.want) + } + }) + } } -func TestIsCodeExpired_Expired(t *testing.T) { - creationDate := time.Date(2019, time.April, 1, 0, 0, 0, 0, time.UTC) - expired := IsCodeExpired(creationDate, 1*time.Hour) - assert.True(t, expired) +func TestVerifyCode(t *testing.T) { + type args struct { + creationDate time.Time + expiry time.Duration + cryptoCode *CryptoValue + verificationCode string + g Generator + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + "expired", + args{ + creationDate: time.Now().Add(-5 * time.Minute), + expiry: 5 * time.Minute, + cryptoCode: nil, + verificationCode: "", + g: nil, + }, + true, + }, + { + "unsupported alg err", + args{ + creationDate: time.Now(), + expiry: 5 * time.Minute, + cryptoCode: nil, + verificationCode: "code", + g: createMockGenerator(t, createMockCrypto(t)), + }, + true, + }, + { + "encryption alg ok", + args{ + creationDate: time.Now(), + expiry: 5 * time.Minute, + cryptoCode: &CryptoValue{ + CryptoType: TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + verificationCode: "code", + g: createMockGenerator(t, createMockEncryptionAlg(t)), + }, + false, + }, + { + "hash alg ok", + args{ + creationDate: time.Now(), + expiry: 5 * time.Minute, + cryptoCode: &CryptoValue{ + CryptoType: TypeHash, + Algorithm: "hash", + Crypted: []byte("code"), + }, + verificationCode: "code", + g: createMockGenerator(t, createMockHashAlg(t)), + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := VerifyCode(tt.args.creationDate, tt.args.expiry, tt.args.cryptoCode, tt.args.verificationCode, tt.args.g); (err != nil) != tt.wantErr { + t.Errorf("VerifyCode() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } } -func TestIsCodeExpired_NotExpired(t *testing.T) { - creationDate := time.Now() - expired := IsCodeExpired(creationDate, 1*time.Hour) - assert.False(t, expired) +func Test_verifyEncryptedCode(t *testing.T) { + type args struct { + cryptoCode *CryptoValue + verificationCode string + alg EncryptionAlgorithm + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + "nil error", + args{ + cryptoCode: nil, + verificationCode: "", + alg: createMockEncryptionAlg(t), + }, + true, + }, + { + "wrong cryptotype error", + args{ + cryptoCode: &CryptoValue{ + CryptoType: TypeHash, + Crypted: nil, + }, + verificationCode: "", + alg: createMockEncryptionAlg(t), + }, + true, + }, + { + "wrong algorithm error", + args{ + cryptoCode: &CryptoValue{ + CryptoType: TypeEncryption, + Algorithm: "enc2", + Crypted: nil, + }, + verificationCode: "", + alg: createMockEncryptionAlg(t), + }, + true, + }, + { + "wrong key id error", + args{ + cryptoCode: &CryptoValue{ + CryptoType: TypeEncryption, + Algorithm: "enc", + Crypted: nil, + }, + verificationCode: "wrong", + alg: createMockEncryptionAlg(t), + }, + true, + }, + { + "wrong verification code error", + args{ + cryptoCode: &CryptoValue{ + CryptoType: TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + verificationCode: "wrong", + alg: createMockEncryptionAlg(t), + }, + true, + }, + { + "verification code ok", + args{ + cryptoCode: &CryptoValue{ + CryptoType: TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + verificationCode: "code", + alg: createMockEncryptionAlg(t), + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := verifyEncryptedCode(tt.args.cryptoCode, tt.args.verificationCode, tt.args.alg); (err != nil) != tt.wantErr { + t.Errorf("verifyEncryptedCode() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_verifyHashedCode(t *testing.T) { + type args struct { + cryptoCode *CryptoValue + verificationCode string + alg HashAlgorithm + } + tests := []struct { + name string + args args + wantErr bool + }{ + + { + "nil error", + args{ + cryptoCode: nil, + verificationCode: "", + alg: createMockHashAlg(t), + }, + true, + }, + { + "wrong cryptotype error", + args{ + cryptoCode: &CryptoValue{ + CryptoType: TypeEncryption, + Crypted: nil, + }, + verificationCode: "", + alg: createMockHashAlg(t), + }, + true, + }, + { + "wrong algorithm error", + args{ + cryptoCode: &CryptoValue{ + CryptoType: TypeHash, + Algorithm: "hash2", + Crypted: nil, + }, + verificationCode: "", + alg: createMockHashAlg(t), + }, + true, + }, + { + "wrong verification code error", + args{ + cryptoCode: &CryptoValue{ + CryptoType: TypeHash, + Algorithm: "hash", + Crypted: []byte("code"), + }, + verificationCode: "wrong", + alg: createMockHashAlg(t), + }, + true, + }, + { + "verification code ok", + args{ + cryptoCode: &CryptoValue{ + CryptoType: TypeHash, + Algorithm: "hash", + Crypted: []byte("code"), + }, + verificationCode: "code", + alg: createMockHashAlg(t), + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := verifyHashedCode(tt.args.cryptoCode, tt.args.verificationCode, tt.args.alg); (err != nil) != tt.wantErr { + t.Errorf("verifyHashedCode() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } } diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go index c1b46d69d5..d7f3269321 100644 --- a/internal/crypto/crypto.go +++ b/internal/crypto/crypto.go @@ -99,5 +99,8 @@ func Hash(value []byte, alg HashAlgorithm) (*CryptoValue, error) { } func CompareHash(value *CryptoValue, comparer []byte, alg HashAlgorithm) error { + if value.Algorithm != alg.Algorithm() { + return errors.ThrowInvalidArgument(nil, "CRYPT-HF32f", "value was hash with a different algorithm") + } return alg.CompareHash(value.Crypted, comparer) } diff --git a/internal/crypto/generate.go b/internal/crypto/generate.go index 4c43e7e9b3..fd3de9f759 100644 --- a/internal/crypto/generate.go +++ b/internal/crypto/generate.go @@ -1,3 +1,4 @@ package crypto //go:generate mockgen -source crypto.go -destination ./crypto_mock.go -package crypto +//go:generate mockgen -source code.go -destination ./code_mock.go -package crypto From 78e6d1909834bf748ffd9573eaa26975cbfea1f0 Mon Sep 17 00:00:00 2001 From: Fabiennne Date: Mon, 30 Mar 2020 11:42:22 +0200 Subject: [PATCH 15/20] fix: reformat test --- internal/proto/struct_test.go | 144 ++++++++++++++++++++++++---------- 1 file changed, 103 insertions(+), 41 deletions(-) diff --git a/internal/proto/struct_test.go b/internal/proto/struct_test.go index eeed7c63f3..53e2503f1c 100644 --- a/internal/proto/struct_test.go +++ b/internal/proto/struct_test.go @@ -4,50 +4,112 @@ import ( "testing" pb_struct "github.com/golang/protobuf/ptypes/struct" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -func TestToPBStruct(t *testing.T) { - obj := struct { - ID string - Name string - Seq uint64 - }{ - ID: "asdf", - Name: "ueli", - Seq: 208582075, +func Test_ToPBStruct(t *testing.T) { + type obj struct { + ID string + Seq uint64 + } + type args struct { + obj obj + } + tests := []struct { + name string + args args + wantErr bool + length int + result obj + }{ + { + name: "to pb stuct", + args: args{ + obj: obj{ID: "ID", Seq: 12345}, + }, + wantErr: false, + length: 2, + result: obj{ID: "ID", Seq: 12345}, + }, + { + name: "empty struct", + args: args{ + obj: obj{}, + }, + wantErr: false, + length: 2, + result: obj{ID: "", Seq: 0}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fields, err := ToPBStruct(tt.args.obj) + if tt.wantErr && err == nil { + t.Errorf("got wrong result, should get err: actual: %v ", err) + } + if !tt.wantErr && len(fields.Fields) != tt.length { + t.Errorf("got wrong result length, expecting: %v, actual: %v ", tt.length, len(fields.Fields)) + } + if !tt.wantErr && tt.result.ID != fields.Fields["ID"].GetStringValue() { + t.Errorf("got wrong result, ID should be same: expecting: %v, actual: %v ", tt.result.ID, fields.Fields["ID"].GetStringValue()) + } + if !tt.wantErr && int(tt.result.Seq) != int(fields.Fields["Seq"].GetNumberValue()) { + t.Errorf("got wrong result, Seq should be same: expecting: %v, actual: %v ", tt.result.Seq, fields.Fields["Seq"].GetStringValue()) + } + }) } - fields, err := ToPBStruct(&obj) - require.NoError(t, err) - require.Len(t, fields.Fields, 3) - - assert.Equal(t, obj.ID, fields.Fields["ID"].GetStringValue()) - assert.Equal(t, int(obj.Seq), int(fields.Fields["Seq"].GetNumberValue())) - assert.Equal(t, obj.Name, fields.Fields["Name"].GetStringValue()) } -func TestFromPBStruct(t *testing.T) { - name := "ueli" - id := "asdf" - seq := float64(208582075) - s := &pb_struct.Struct{Fields: map[string]*pb_struct.Value{ - "ID": &pb_struct.Value{Kind: &pb_struct.Value_StringValue{StringValue: id}}, - "Name": &pb_struct.Value{Kind: &pb_struct.Value_StringValue{StringValue: name}}, - "Seq": &pb_struct.Value{Kind: &pb_struct.Value_NumberValue{NumberValue: seq}}, - }} - - obj := struct { - ID string - Name string - Seq uint64 - }{} - - err := FromPBStruct(&obj, s) - require.NoError(t, err) - - assert.Equal(t, id, obj.ID) - assert.Equal(t, name, obj.Name) - assert.Equal(t, int(seq), int(obj.Seq)) +func Test_FromPBStruct(t *testing.T) { + type obj struct { + ID string + Seq uint64 + } + type args struct { + obj *obj + fields *pb_struct.Struct + } + tests := []struct { + name string + args args + wantErr bool + result obj + }{ + { + name: "from pb stuct", + args: args{ + obj: &obj{}, + fields: &pb_struct.Struct{Fields: map[string]*pb_struct.Value{ + "ID": &pb_struct.Value{Kind: &pb_struct.Value_StringValue{StringValue: "ID"}}, + "Seq": &pb_struct.Value{Kind: &pb_struct.Value_NumberValue{NumberValue: 12345}}, + }, + }, + }, + wantErr: false, + result: obj{ID: "ID", Seq: 12345}, + }, + { + name: "no fields", + args: args{ + obj: &obj{}, + fields: &pb_struct.Struct{Fields: map[string]*pb_struct.Value{}, + }, + }, + wantErr: false, + result: obj{ID: "", Seq: 0}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := FromPBStruct(tt.args.obj, tt.args.fields) + if tt.wantErr && err == nil { + t.Errorf("got wrong result, should get err: actual: %v ", err) + } + if !tt.wantErr && tt.result.ID != tt.args.obj.ID { + t.Errorf("got wrong result, ID should be same: expecting: %v, actual: %v ", tt.result.ID, tt.args.obj.ID) + } + if !tt.wantErr && int(tt.result.Seq) != int(tt.args.obj.Seq) { + t.Errorf("got wrong result, Seq should be same: expecting: %v, actual: %v ", tt.result.Seq, tt.args.obj.Seq) + } + }) + } } From 96b1817d62a0877061dbc675fe9a2f70dd501d2b Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 30 Mar 2020 11:44:17 +0200 Subject: [PATCH 16/20] add multi files config test and some more --- internal/config/config_test.go | 37 +++++++++- internal/config/testdata/more_data.yaml | 1 + internal/crypto/aes_test.go | 1 + internal/crypto/code_mock.go | 90 +++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 internal/config/testdata/more_data.yaml create mode 100644 internal/crypto/code_mock.go diff --git a/internal/config/config_test.go b/internal/config/config_test.go index ba517b6061..98221851dc 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -17,6 +17,11 @@ type validatable struct { Test bool } +type multiple struct { + Test bool + MoreData string +} + func (v *validatable) Validate() error { if v.Test { return nil @@ -33,22 +38,25 @@ func TestRead(t *testing.T) { name string args args wantErr bool + want interface{} }{ { "not supoorted config file error", args{ configFiles: []string{"notsupported.unknown"}, - obj: nil, + obj: &test{}, }, true, + &test{}, }, { "non existing config file error", args{ configFiles: []string{"nonexisting.yaml"}, - obj: nil, + obj: &test{}, }, true, + &test{}, }, { "non parsable config file error", @@ -57,6 +65,7 @@ func TestRead(t *testing.T) { obj: &test{}, }, true, + &test{}, }, { "invalid parsable config file error", @@ -65,6 +74,16 @@ func TestRead(t *testing.T) { obj: &validatable{}, }, true, + &validatable{}, + }, + { + "multiple files, one non parsable error ", + args{ + configFiles: []string{"./testdata/non_parsable.json", "./testdata/more_data.yaml"}, + obj: &multiple{}, + }, + true, + &multiple{}, }, { "parsable config file ok", @@ -73,6 +92,16 @@ func TestRead(t *testing.T) { obj: &test{}, }, false, + &test{Test: true}, + }, + { + "multiple parsable config files ok", + args{ + configFiles: []string{"./testdata/valid.json", "./testdata/more_data.yaml"}, + obj: &multiple{}, + }, + false, + &multiple{Test: true, MoreData: "data"}, }, { "valid parsable config file ok", @@ -81,6 +110,7 @@ func TestRead(t *testing.T) { obj: &validatable{}, }, false, + &validatable{Test: true}, }, } for _, tt := range tests { @@ -88,6 +118,9 @@ func TestRead(t *testing.T) { if err := Read(tt.args.obj, tt.args.configFiles...); (err != nil) != tt.wantErr { t.Errorf("Read() error = %v, wantErr %v", err, tt.wantErr) } + if !reflect.DeepEqual(tt.args.obj, tt.want) { + t.Errorf("Read() got = %v, want = %v", tt.args.obj, tt.want) + } }) } } diff --git a/internal/config/testdata/more_data.yaml b/internal/config/testdata/more_data.yaml new file mode 100644 index 0000000000..d8fc5877da --- /dev/null +++ b/internal/config/testdata/more_data.yaml @@ -0,0 +1 @@ +MoreData: data diff --git a/internal/crypto/aes_test.go b/internal/crypto/aes_test.go index c7e31de3d5..87af88d0be 100644 --- a/internal/crypto/aes_test.go +++ b/internal/crypto/aes_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" ) +//TODO: refactor test style func TestDecrypt_OK(t *testing.T) { encryptedpw, err := EncryptAESString("ThisIsMySecretPw", "passphrasewhichneedstobe32bytes!") assert.NoError(t, err) diff --git a/internal/crypto/code_mock.go b/internal/crypto/code_mock.go new file mode 100644 index 0000000000..916a7c225d --- /dev/null +++ b/internal/crypto/code_mock.go @@ -0,0 +1,90 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: code.go + +// Package crypto is a generated GoMock package. +package crypto + +import ( + gomock "github.com/golang/mock/gomock" + reflect "reflect" + time "time" +) + +// MockGenerator is a mock of Generator interface +type MockGenerator struct { + ctrl *gomock.Controller + recorder *MockGeneratorMockRecorder +} + +// MockGeneratorMockRecorder is the mock recorder for MockGenerator +type MockGeneratorMockRecorder struct { + mock *MockGenerator +} + +// NewMockGenerator creates a new mock instance +func NewMockGenerator(ctrl *gomock.Controller) *MockGenerator { + mock := &MockGenerator{ctrl: ctrl} + mock.recorder = &MockGeneratorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockGenerator) EXPECT() *MockGeneratorMockRecorder { + return m.recorder +} + +// Length mocks base method +func (m *MockGenerator) Length() uint { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Length") + ret0, _ := ret[0].(uint) + return ret0 +} + +// Length indicates an expected call of Length +func (mr *MockGeneratorMockRecorder) Length() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Length", reflect.TypeOf((*MockGenerator)(nil).Length)) +} + +// Expiry mocks base method +func (m *MockGenerator) Expiry() time.Duration { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Expiry") + ret0, _ := ret[0].(time.Duration) + return ret0 +} + +// Expiry indicates an expected call of Expiry +func (mr *MockGeneratorMockRecorder) Expiry() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Expiry", reflect.TypeOf((*MockGenerator)(nil).Expiry)) +} + +// Alg mocks base method +func (m *MockGenerator) Alg() Crypto { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Alg") + ret0, _ := ret[0].(Crypto) + return ret0 +} + +// Alg indicates an expected call of Alg +func (mr *MockGeneratorMockRecorder) Alg() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Alg", reflect.TypeOf((*MockGenerator)(nil).Alg)) +} + +// Runes mocks base method +func (m *MockGenerator) Runes() []rune { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Runes") + ret0, _ := ret[0].([]rune) + return ret0 +} + +// Runes indicates an expected call of Runes +func (mr *MockGeneratorMockRecorder) Runes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Runes", reflect.TypeOf((*MockGenerator)(nil).Runes)) +} From 40e4f69ca3711d04e406f6af303034d5fc7fb155 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 30 Mar 2020 11:52:08 +0200 Subject: [PATCH 17/20] change context keys and fix tests --- internal/api/auth/authorization_test.go | 2 +- internal/api/auth/context.go | 14 +++++++++----- internal/api/auth/permissions.go | 2 +- internal/api/auth/permissions_test.go | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/internal/api/auth/authorization_test.go b/internal/api/auth/authorization_test.go index ac1193385e..d140758321 100644 --- a/internal/api/auth/authorization_test.go +++ b/internal/api/auth/authorization_test.go @@ -270,7 +270,7 @@ func Test_GetPermissionCtxIDs(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := GetPermissionCtxIDs(tt.args.perms) - if !EqualStringArray(result, tt.result) { + if !equalStringArray(result, tt.result) { t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result) } }) diff --git a/internal/api/auth/context.go b/internal/api/auth/context.go index 78ef0bc252..e0c87929e5 100644 --- a/internal/api/auth/context.go +++ b/internal/api/auth/context.go @@ -6,8 +6,12 @@ import ( "github.com/caos/logging" ) -type CtxKeyPermissions struct{} -type CtxKeyData struct{} +type key int + +var ( + permissionsKey key + dataKey key +) type CtxData struct { UserID string @@ -42,15 +46,15 @@ func VerifyTokenAndWriteCtxData(ctx context.Context, token, orgID string, t Toke projectID, err := t.GetProjectIDByClientID(ctx, clientID) logging.LogWithFields("AUTH-GfAoV", "clientID", clientID).OnError(err).Warn("could not read projectid by clientid") - return context.WithValue(ctx, CtxKeyData{}, CtxData{UserID: userID, OrgID: orgID, ProjectID: projectID, AgentID: agentID}), nil + return context.WithValue(ctx, dataKey, CtxData{UserID: userID, OrgID: orgID, ProjectID: projectID, AgentID: agentID}), nil } func GetCtxData(ctx context.Context) CtxData { - ctxData, _ := ctx.Value(CtxKeyData{}).(CtxData) + ctxData, _ := ctx.Value(dataKey).(CtxData) return ctxData } func GetPermissionsFromCtx(ctx context.Context) []string { - ctxPermission, _ := ctx.Value(CtxKeyPermissions{}).([]string) + ctxPermission, _ := ctx.Value(permissionsKey).([]string) return ctxPermission } diff --git a/internal/api/auth/permissions.go b/internal/api/auth/permissions.go index d7ca516a9d..04c6713915 100644 --- a/internal/api/auth/permissions.go +++ b/internal/api/auth/permissions.go @@ -16,7 +16,7 @@ func getUserMethodPermissions(ctx context.Context, t TokenVerifier, requiredPerm return nil, nil, err } permissions := mapGrantsToPermissions(requiredPerm, grants, authConfig) - return context.WithValue(ctx, CtxKeyPermissions{}, permissions), permissions, nil + return context.WithValue(ctx, permissionsKey, permissions), permissions, nil } func mapGrantsToPermissions(requiredPerm string, grants []*Grant, authConfig *Config) []string { diff --git a/internal/api/auth/permissions_test.go b/internal/api/auth/permissions_test.go index d2afc36dfc..ba67f0d25f 100644 --- a/internal/api/auth/permissions_test.go +++ b/internal/api/auth/permissions_test.go @@ -8,7 +8,7 @@ import ( ) func getTestCtx(userID, orgID string) context.Context { - return context.WithValue(context.Background(), CtxKeyData{}, CtxData{UserID: userID, OrgID: orgID}) + return context.WithValue(context.Background(), dataKey, CtxData{UserID: userID, OrgID: orgID}) } type testVerifier struct { From affd2da40a47d01e483a8f12f852db51a8ae0b2c Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 30 Mar 2020 11:57:56 +0200 Subject: [PATCH 18/20] update logging version --- cmd/zitadel/main.go | 7 ++++--- cmd/zitadel/startup.yaml | 3 ++- go.mod | 5 +++-- go.sum | 10 ++++++---- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/cmd/zitadel/main.go b/cmd/zitadel/main.go index 5d0b1e6a9c..fc5f748139 100644 --- a/cmd/zitadel/main.go +++ b/cmd/zitadel/main.go @@ -8,6 +8,7 @@ import ( authz "github.com/caos/zitadel/internal/api/auth" "github.com/caos/zitadel/internal/config" + tracing "github.com/caos/zitadel/internal/tracing/config" "github.com/caos/zitadel/pkg/admin" "github.com/caos/zitadel/pkg/auth" "github.com/caos/zitadel/pkg/console" @@ -22,9 +23,9 @@ type Config struct { Admin admin.Config Console console.Config - //Log //TODO: add - //Tracing tracing.TracingConfig //TODO: add - AuthZ authz.Config + Log logging.Config + Tracing tracing.TracingConfig + AuthZ authz.Config } func main() { diff --git a/cmd/zitadel/startup.yaml b/cmd/zitadel/startup.yaml index c84f9015a1..abf809b34b 100644 --- a/cmd/zitadel/startup.yaml +++ b/cmd/zitadel/startup.yaml @@ -7,7 +7,8 @@ Tracing: Log: Level: debug - Formatter: text + Formatter: + Format: text Mgmt: API: diff --git a/go.mod b/go.mod index 731d99ee77..4f7af60649 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/sprig v2.22.0+incompatible github.com/aws/aws-sdk-go v1.29.16 // indirect - github.com/caos/logging v0.0.0-20191210002624-b3260f690a6a + github.com/caos/logging v0.0.1 github.com/ghodss/yaml v1.0.0 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b github.com/golang/mock v1.4.3 @@ -25,11 +25,12 @@ require ( github.com/mitchellh/copystructure v1.0.0 // indirect github.com/nicksnyder/go-i18n/v2 v2.0.3 github.com/rs/cors v1.7.0 + github.com/sirupsen/logrus v1.5.0 // indirect github.com/stretchr/testify v1.5.1 go.opencensus.io v0.22.3 golang.org/x/crypto v0.0.0-20200320181102-891825fb96df golang.org/x/net v0.0.0-20200320220750-118fecf932d8 // indirect - golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae // indirect + golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775 // indirect golang.org/x/text v0.3.2 golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56 google.golang.org/api v0.20.0 // indirect diff --git a/go.sum b/go.sum index c277b44880..f253cef29c 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQY github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.29.16 h1:Gbtod7Y4W/Ai7wPtesdvgGVTkFN8JxAaGouRLlcQfQs= github.com/aws/aws-sdk-go v1.29.16/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg= -github.com/caos/logging v0.0.0-20191210002624-b3260f690a6a h1:HOU/3xL/afsZ+2aCstfJlrzRkwYMTFR1TIEgps5ny8s= -github.com/caos/logging v0.0.0-20191210002624-b3260f690a6a/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0= +github.com/caos/logging v0.0.1 h1:YSGtO2/+5OWdwilBCou50akoDHAT/OhkbrolkVlR6U0= +github.com/caos/logging v0.0.1/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0= github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -140,6 +140,8 @@ github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q= +github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= @@ -234,8 +236,8 @@ golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab h1:FvshnhkKW+LO3HWHodML8kuVX golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae h1:3tcmuaB7wwSZtelmiv479UjUB+vviwABz7a133ZwOKQ= -golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775 h1:TC0v2RSO1u2kn1ZugjrFXkRZAEaqMN/RW+OTZkBzmLE= +golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 39039dde626a975fff5a0f77cfb69fc99b8d17a6 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 30 Mar 2020 13:17:49 +0200 Subject: [PATCH 19/20] fix tracing/statusFromError --- internal/api/grpc/caos_errors.go | 4 ++-- internal/tracing/span.go | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/internal/api/grpc/caos_errors.go b/internal/api/grpc/caos_errors.go index 5ed7fa95da..413a2fd128 100644 --- a/internal/api/grpc/caos_errors.go +++ b/internal/api/grpc/caos_errors.go @@ -11,14 +11,14 @@ func CaosToGRPCError(err error) error { if err == nil { return nil } - code, msg, ok := extract(err) + code, msg, ok := Extract(err) if !ok { return status.Convert(err).Err() } return status.Error(code, msg) } -func extract(err error) (c codes.Code, msg string, ok bool) { +func Extract(err error) (c codes.Code, msg string, ok bool) { switch caosErr := err.(type) { case *caos_errs.AlreadyExistsError: return codes.AlreadyExists, caosErr.GetMessage(), true diff --git a/internal/tracing/span.go b/internal/tracing/span.go index 1ad46ca0b1..aff92822c2 100644 --- a/internal/tracing/span.go +++ b/internal/tracing/span.go @@ -5,9 +5,8 @@ import ( "strconv" "go.opencensus.io/trace" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" + "github.com/caos/zitadel/internal/api/grpc" "github.com/caos/zitadel/internal/errors" ) @@ -41,10 +40,8 @@ func (s *Span) SetStatusByError(err error) { } func statusFromError(err error) trace.Status { - if statusErr, ok := status.FromError(err); ok { - return trace.Status{Code: int32(statusErr.Code()), Message: statusErr.Message()} - } - return trace.Status{Code: int32(codes.Unknown), Message: "Unknown"} + code, msg, _ := grpc.Extract(err) + return trace.Status{Code: int32(code), Message: msg} } // AddAnnotation creates an annotation. The annotation will not be added to the tracing use Annotate(msg) afterwards From c564f47b92b7000afcd9b0ee668a2b6f2a6e4376 Mon Sep 17 00:00:00 2001 From: livio-a Date: Mon, 30 Mar 2020 16:53:29 +0200 Subject: [PATCH 20/20] single marshaller --- internal/proto/struct.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/proto/struct.go b/internal/proto/struct.go index f1b5eb29f9..5c7b93b430 100644 --- a/internal/proto/struct.go +++ b/internal/proto/struct.go @@ -10,6 +10,10 @@ import ( "github.com/caos/logging" ) +var ( + marshaller = new(jsonpb.Marshaler) +) + func MustToPBStruct(object interface{}) *pb_struct.Struct { s, err := ToPBStruct(object) logging.Log("PROTO-7Aa3t").OnError(err).Panic("unable to map object to pb-struct") @@ -38,7 +42,6 @@ func MustFromPBStruct(object interface{}, s *pb_struct.Struct) { } func FromPBStruct(object interface{}, s *pb_struct.Struct) error { - marshaller := new(jsonpb.Marshaler) jsonString, err := marshaller.MarshalToString(s) if err != nil { return err