From 5c5cc463969dbca2b980ce66e5faa0638104b0c1 Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Tue, 14 Jun 2022 18:42:57 -0400 Subject: [PATCH 01/18] feature: WEOS-1504 Add logs to container * Setup the echo logger as the default logger --- controllers/rest/api.go | 18 ++++++++++++++++++ controllers/rest/interfaces.go | 5 +++++ 2 files changed, 23 insertions(+) diff --git a/controllers/rest/api.go b/controllers/rest/api.go index d07c3c12..29e801c5 100644 --- a/controllers/rest/api.go +++ b/controllers/rest/api.go @@ -47,6 +47,7 @@ type RESTAPI struct { eventStores map[string]model.EventRepository commandDispatchers map[string]model.CommandDispatcher projections map[string]projections.Projection + logs map[string]model.Log globalInitializers []GlobalInitializer operationInitializers []OperationInitializer registeredInitializers map[reflect.Value]int @@ -341,6 +342,21 @@ func (p *RESTAPI) GetWeOSConfig() *APIConfig { return p.Config } +//RegisterLog setup a log +func (p *RESTAPI) RegisterLog(name string, logger model.Log) { + if p.logs == nil { + p.logs = make(map[string]model.Log) + } + p.logs[name] = logger +} + +func (p *RESTAPI) GetLog(name string) (model.Log, error) { + if tlog, ok := p.logs[name]; ok { + return tlog, nil + } + return nil, fmt.Errorf("log '%s' not found", name) +} + const SWAGGERUIENDPOINT = "/_discover/" const SWAGGERJSONENDPOINT = "/_discover_json" @@ -368,6 +384,8 @@ func (p *RESTAPI) RegisterDefaultSwaggerJSON(pathMiddleware []echo.MiddlewareFun //Initialize and setup configurations for RESTAPI func (p *RESTAPI) Initialize(ctxt context.Context) error { + //register logger + p.RegisterLog("Default", p.e.Logger) //register standard controllers p.RegisterController("CreateController", CreateController) p.RegisterController("UpdateController", UpdateController) diff --git a/controllers/rest/interfaces.go b/controllers/rest/interfaces.go index b563ec66..a188a1ae 100644 --- a/controllers/rest/interfaces.go +++ b/controllers/rest/interfaces.go @@ -1,3 +1,4 @@ +//go:generate moq -out rest_mocks_test.go -pkg rest_test . Container package rest import ( @@ -76,4 +77,8 @@ type Container interface { GetConfig() *openapi3.Swagger //GetWeOSConfig this is the old way of getting the config GetWeOSConfig() *APIConfig + //RegisterLog set logger + RegisterLog(name string, logger model.Log) + //GetLog + GetLog(name string) (model.Log, error) } From 1d9bd48f7ea8536185096ffbc24f67cc0b88ba14 Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Wed, 15 Jun 2022 06:00:59 -0400 Subject: [PATCH 02/18] feature: WEOS-1504 Add http client to container --- controllers/rest/api.go | 24 + controllers/rest/interfaces.go | 5 + controllers/rest/rest_mocks_test.go | 1381 +++++++++++++++++++++++++++ 3 files changed, 1410 insertions(+) create mode 100644 controllers/rest/rest_mocks_test.go diff --git a/controllers/rest/api.go b/controllers/rest/api.go index 29e801c5..754c6e0a 100644 --- a/controllers/rest/api.go +++ b/controllers/rest/api.go @@ -48,6 +48,7 @@ type RESTAPI struct { commandDispatchers map[string]model.CommandDispatcher projections map[string]projections.Projection logs map[string]model.Log + httpClients map[string]*http.Client globalInitializers []GlobalInitializer operationInitializers []OperationInitializer registeredInitializers map[reflect.Value]int @@ -357,6 +358,20 @@ func (p *RESTAPI) GetLog(name string) (model.Log, error) { return nil, fmt.Errorf("log '%s' not found", name) } +func (p *RESTAPI) RegisterHTTPClient(name string, client *http.Client) { + if p.httpClients == nil { + p.httpClients = make(map[string]*http.Client) + } + p.httpClients[name] = client +} + +func (p *RESTAPI) GetHTTPClient(name string) (*http.Client, error) { + if client, ok := p.httpClients[name]; ok { + return client, nil + } + return nil, fmt.Errorf("http client '%s' not found", name) +} + const SWAGGERUIENDPOINT = "/_discover/" const SWAGGERJSONENDPOINT = "/_discover_json" @@ -386,6 +401,15 @@ func (p *RESTAPI) RegisterDefaultSwaggerJSON(pathMiddleware []echo.MiddlewareFun func (p *RESTAPI) Initialize(ctxt context.Context) error { //register logger p.RegisterLog("Default", p.e.Logger) + //register httpClient + t := http.DefaultTransport.(*http.Transport).Clone() + t.MaxIdleConns = 100 + t.MaxConnsPerHost = 100 + t.MaxIdleConnsPerHost = 100 + p.RegisterHTTPClient("Default", &http.Client{ + Transport: t, + Timeout: time.Second * 10, + }) //register standard controllers p.RegisterController("CreateController", CreateController) p.RegisterController("UpdateController", UpdateController) diff --git a/controllers/rest/interfaces.go b/controllers/rest/interfaces.go index a188a1ae..e08337af 100644 --- a/controllers/rest/interfaces.go +++ b/controllers/rest/interfaces.go @@ -9,6 +9,7 @@ import ( "github.com/wepala/weos/projections" "golang.org/x/net/context" "gorm.io/gorm" + "net/http" ) type ( @@ -81,4 +82,8 @@ type Container interface { RegisterLog(name string, logger model.Log) //GetLog GetLog(name string) (model.Log, error) + //RegisterHTTPClient setup http client to use + RegisterHTTPClient(name string, client *http.Client) + //GetHTTPClient return htpt client + GetHTTPClient(name string) (*http.Client, error) } diff --git a/controllers/rest/rest_mocks_test.go b/controllers/rest/rest_mocks_test.go new file mode 100644 index 00000000..ce19cf1a --- /dev/null +++ b/controllers/rest/rest_mocks_test.go @@ -0,0 +1,1381 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package rest_test + +import ( + "database/sql" + "github.com/getkin/kin-openapi/openapi3" + "github.com/wepala/weos/controllers/rest" + weos "github.com/wepala/weos/model" + "github.com/wepala/weos/projections" + "gorm.io/gorm" + "net/http" + "sync" +) + +// Ensure, that ContainerMock does implement rest.Container. +// If this is not the case, regenerate this file with moq. +var _ rest.Container = &ContainerMock{} + +// ContainerMock is a mock implementation of rest.Container. +// +// func TestSomethingThatUsesContainer(t *testing.T) { +// +// // make and configure a mocked rest.Container +// mockedContainer := &ContainerMock{ +// GetCommandDispatcherFunc: func(name string) (weos.CommandDispatcher, error) { +// panic("mock out the GetCommandDispatcher method") +// }, +// GetConfigFunc: func() *openapi3.Swagger { +// panic("mock out the GetConfig method") +// }, +// GetControllerFunc: func(name string) (rest.Controller, error) { +// panic("mock out the GetController method") +// }, +// GetDBConnectionFunc: func(name string) (*sql.DB, error) { +// panic("mock out the GetDBConnection method") +// }, +// GetEntityFactoriesFunc: func() map[string]weos.EntityFactory { +// panic("mock out the GetEntityFactories method") +// }, +// GetEntityFactoryFunc: func(name string) (weos.EntityFactory, error) { +// panic("mock out the GetEntityFactory method") +// }, +// GetEventStoreFunc: func(name string) (weos.EventRepository, error) { +// panic("mock out the GetEventStore method") +// }, +// GetGlobalInitializersFunc: func() []rest.GlobalInitializer { +// panic("mock out the GetGlobalInitializers method") +// }, +// GetGormDBConnectionFunc: func(name string) (*gorm.DB, error) { +// panic("mock out the GetGormDBConnection method") +// }, +// GetHTTPClientFunc: func(name string) (*http.Client, error) { +// panic("mock out the GetHTTPClient method") +// }, +// GetLogFunc: func(name string) (weos.Log, error) { +// panic("mock out the GetLog method") +// }, +// GetMiddlewareFunc: func(name string) (rest.Middleware, error) { +// panic("mock out the GetMiddleware method") +// }, +// GetOperationInitializersFunc: func() []rest.OperationInitializer { +// panic("mock out the GetOperationInitializers method") +// }, +// GetPostPathInitializersFunc: func() []rest.PathInitializer { +// panic("mock out the GetPostPathInitializers method") +// }, +// GetPrePathInitializersFunc: func() []rest.PathInitializer { +// panic("mock out the GetPrePathInitializers method") +// }, +// GetProjectionFunc: func(name string) (projections.Projection, error) { +// panic("mock out the GetProjection method") +// }, +// GetWeOSConfigFunc: func() *rest.APIConfig { +// panic("mock out the GetWeOSConfig method") +// }, +// RegisterCommandDispatcherFunc: func(name string, dispatcher weos.CommandDispatcher) { +// panic("mock out the RegisterCommandDispatcher method") +// }, +// RegisterControllerFunc: func(name string, controller rest.Controller) { +// panic("mock out the RegisterController method") +// }, +// RegisterDBConnectionFunc: func(name string, connection *sql.DB) { +// panic("mock out the RegisterDBConnection method") +// }, +// RegisterEntityFactoryFunc: func(name string, factory weos.EntityFactory) { +// panic("mock out the RegisterEntityFactory method") +// }, +// RegisterEventStoreFunc: func(name string, repository weos.EventRepository) { +// panic("mock out the RegisterEventStore method") +// }, +// RegisterGORMDBFunc: func(name string, connection *gorm.DB) { +// panic("mock out the RegisterGORMDB method") +// }, +// RegisterGlobalInitializerFunc: func(initializer rest.GlobalInitializer) { +// panic("mock out the RegisterGlobalInitializer method") +// }, +// RegisterHTTPClientFunc: func(name string, client *http.Client) { +// panic("mock out the RegisterHTTPClient method") +// }, +// RegisterLogFunc: func(name string, logger weos.Log) { +// panic("mock out the RegisterLog method") +// }, +// RegisterMiddlewareFunc: func(name string, middleware rest.Middleware) { +// panic("mock out the RegisterMiddleware method") +// }, +// RegisterOperationInitializerFunc: func(initializer rest.OperationInitializer) { +// panic("mock out the RegisterOperationInitializer method") +// }, +// RegisterPostPathInitializerFunc: func(initializer rest.PathInitializer) { +// panic("mock out the RegisterPostPathInitializer method") +// }, +// RegisterPrePathInitializerFunc: func(initializer rest.PathInitializer) { +// panic("mock out the RegisterPrePathInitializer method") +// }, +// RegisterProjectionFunc: func(name string, projection projections.Projection) { +// panic("mock out the RegisterProjection method") +// }, +// } +// +// // use mockedContainer in code that requires rest.Container +// // and then make assertions. +// +// } +type ContainerMock struct { + // GetCommandDispatcherFunc mocks the GetCommandDispatcher method. + GetCommandDispatcherFunc func(name string) (weos.CommandDispatcher, error) + + // GetConfigFunc mocks the GetConfig method. + GetConfigFunc func() *openapi3.Swagger + + // GetControllerFunc mocks the GetController method. + GetControllerFunc func(name string) (rest.Controller, error) + + // GetDBConnectionFunc mocks the GetDBConnection method. + GetDBConnectionFunc func(name string) (*sql.DB, error) + + // GetEntityFactoriesFunc mocks the GetEntityFactories method. + GetEntityFactoriesFunc func() map[string]weos.EntityFactory + + // GetEntityFactoryFunc mocks the GetEntityFactory method. + GetEntityFactoryFunc func(name string) (weos.EntityFactory, error) + + // GetEventStoreFunc mocks the GetEventStore method. + GetEventStoreFunc func(name string) (weos.EventRepository, error) + + // GetGlobalInitializersFunc mocks the GetGlobalInitializers method. + GetGlobalInitializersFunc func() []rest.GlobalInitializer + + // GetGormDBConnectionFunc mocks the GetGormDBConnection method. + GetGormDBConnectionFunc func(name string) (*gorm.DB, error) + + // GetHTTPClientFunc mocks the GetHTTPClient method. + GetHTTPClientFunc func(name string) (*http.Client, error) + + // GetLogFunc mocks the GetLog method. + GetLogFunc func(name string) (weos.Log, error) + + // GetMiddlewareFunc mocks the GetMiddleware method. + GetMiddlewareFunc func(name string) (rest.Middleware, error) + + // GetOperationInitializersFunc mocks the GetOperationInitializers method. + GetOperationInitializersFunc func() []rest.OperationInitializer + + // GetPostPathInitializersFunc mocks the GetPostPathInitializers method. + GetPostPathInitializersFunc func() []rest.PathInitializer + + // GetPrePathInitializersFunc mocks the GetPrePathInitializers method. + GetPrePathInitializersFunc func() []rest.PathInitializer + + // GetProjectionFunc mocks the GetProjection method. + GetProjectionFunc func(name string) (projections.Projection, error) + + // GetWeOSConfigFunc mocks the GetWeOSConfig method. + GetWeOSConfigFunc func() *rest.APIConfig + + // RegisterCommandDispatcherFunc mocks the RegisterCommandDispatcher method. + RegisterCommandDispatcherFunc func(name string, dispatcher weos.CommandDispatcher) + + // RegisterControllerFunc mocks the RegisterController method. + RegisterControllerFunc func(name string, controller rest.Controller) + + // RegisterDBConnectionFunc mocks the RegisterDBConnection method. + RegisterDBConnectionFunc func(name string, connection *sql.DB) + + // RegisterEntityFactoryFunc mocks the RegisterEntityFactory method. + RegisterEntityFactoryFunc func(name string, factory weos.EntityFactory) + + // RegisterEventStoreFunc mocks the RegisterEventStore method. + RegisterEventStoreFunc func(name string, repository weos.EventRepository) + + // RegisterGORMDBFunc mocks the RegisterGORMDB method. + RegisterGORMDBFunc func(name string, connection *gorm.DB) + + // RegisterGlobalInitializerFunc mocks the RegisterGlobalInitializer method. + RegisterGlobalInitializerFunc func(initializer rest.GlobalInitializer) + + // RegisterHTTPClientFunc mocks the RegisterHTTPClient method. + RegisterHTTPClientFunc func(name string, client *http.Client) + + // RegisterLogFunc mocks the RegisterLog method. + RegisterLogFunc func(name string, logger weos.Log) + + // RegisterMiddlewareFunc mocks the RegisterMiddleware method. + RegisterMiddlewareFunc func(name string, middleware rest.Middleware) + + // RegisterOperationInitializerFunc mocks the RegisterOperationInitializer method. + RegisterOperationInitializerFunc func(initializer rest.OperationInitializer) + + // RegisterPostPathInitializerFunc mocks the RegisterPostPathInitializer method. + RegisterPostPathInitializerFunc func(initializer rest.PathInitializer) + + // RegisterPrePathInitializerFunc mocks the RegisterPrePathInitializer method. + RegisterPrePathInitializerFunc func(initializer rest.PathInitializer) + + // RegisterProjectionFunc mocks the RegisterProjection method. + RegisterProjectionFunc func(name string, projection projections.Projection) + + // calls tracks calls to the methods. + calls struct { + // GetCommandDispatcher holds details about calls to the GetCommandDispatcher method. + GetCommandDispatcher []struct { + // Name is the name argument value. + Name string + } + // GetConfig holds details about calls to the GetConfig method. + GetConfig []struct { + } + // GetController holds details about calls to the GetController method. + GetController []struct { + // Name is the name argument value. + Name string + } + // GetDBConnection holds details about calls to the GetDBConnection method. + GetDBConnection []struct { + // Name is the name argument value. + Name string + } + // GetEntityFactories holds details about calls to the GetEntityFactories method. + GetEntityFactories []struct { + } + // GetEntityFactory holds details about calls to the GetEntityFactory method. + GetEntityFactory []struct { + // Name is the name argument value. + Name string + } + // GetEventStore holds details about calls to the GetEventStore method. + GetEventStore []struct { + // Name is the name argument value. + Name string + } + // GetGlobalInitializers holds details about calls to the GetGlobalInitializers method. + GetGlobalInitializers []struct { + } + // GetGormDBConnection holds details about calls to the GetGormDBConnection method. + GetGormDBConnection []struct { + // Name is the name argument value. + Name string + } + // GetHTTPClient holds details about calls to the GetHTTPClient method. + GetHTTPClient []struct { + // Name is the name argument value. + Name string + } + // GetLog holds details about calls to the GetLog method. + GetLog []struct { + // Name is the name argument value. + Name string + } + // GetMiddleware holds details about calls to the GetMiddleware method. + GetMiddleware []struct { + // Name is the name argument value. + Name string + } + // GetOperationInitializers holds details about calls to the GetOperationInitializers method. + GetOperationInitializers []struct { + } + // GetPostPathInitializers holds details about calls to the GetPostPathInitializers method. + GetPostPathInitializers []struct { + } + // GetPrePathInitializers holds details about calls to the GetPrePathInitializers method. + GetPrePathInitializers []struct { + } + // GetProjection holds details about calls to the GetProjection method. + GetProjection []struct { + // Name is the name argument value. + Name string + } + // GetWeOSConfig holds details about calls to the GetWeOSConfig method. + GetWeOSConfig []struct { + } + // RegisterCommandDispatcher holds details about calls to the RegisterCommandDispatcher method. + RegisterCommandDispatcher []struct { + // Name is the name argument value. + Name string + // Dispatcher is the dispatcher argument value. + Dispatcher weos.CommandDispatcher + } + // RegisterController holds details about calls to the RegisterController method. + RegisterController []struct { + // Name is the name argument value. + Name string + // Controller is the controller argument value. + Controller rest.Controller + } + // RegisterDBConnection holds details about calls to the RegisterDBConnection method. + RegisterDBConnection []struct { + // Name is the name argument value. + Name string + // Connection is the connection argument value. + Connection *sql.DB + } + // RegisterEntityFactory holds details about calls to the RegisterEntityFactory method. + RegisterEntityFactory []struct { + // Name is the name argument value. + Name string + // Factory is the factory argument value. + Factory weos.EntityFactory + } + // RegisterEventStore holds details about calls to the RegisterEventStore method. + RegisterEventStore []struct { + // Name is the name argument value. + Name string + // Repository is the repository argument value. + Repository weos.EventRepository + } + // RegisterGORMDB holds details about calls to the RegisterGORMDB method. + RegisterGORMDB []struct { + // Name is the name argument value. + Name string + // Connection is the connection argument value. + Connection *gorm.DB + } + // RegisterGlobalInitializer holds details about calls to the RegisterGlobalInitializer method. + RegisterGlobalInitializer []struct { + // Initializer is the initializer argument value. + Initializer rest.GlobalInitializer + } + // RegisterHTTPClient holds details about calls to the RegisterHTTPClient method. + RegisterHTTPClient []struct { + // Name is the name argument value. + Name string + // Client is the client argument value. + Client *http.Client + } + // RegisterLog holds details about calls to the RegisterLog method. + RegisterLog []struct { + // Name is the name argument value. + Name string + // Logger is the logger argument value. + Logger weos.Log + } + // RegisterMiddleware holds details about calls to the RegisterMiddleware method. + RegisterMiddleware []struct { + // Name is the name argument value. + Name string + // Middleware is the middleware argument value. + Middleware rest.Middleware + } + // RegisterOperationInitializer holds details about calls to the RegisterOperationInitializer method. + RegisterOperationInitializer []struct { + // Initializer is the initializer argument value. + Initializer rest.OperationInitializer + } + // RegisterPostPathInitializer holds details about calls to the RegisterPostPathInitializer method. + RegisterPostPathInitializer []struct { + // Initializer is the initializer argument value. + Initializer rest.PathInitializer + } + // RegisterPrePathInitializer holds details about calls to the RegisterPrePathInitializer method. + RegisterPrePathInitializer []struct { + // Initializer is the initializer argument value. + Initializer rest.PathInitializer + } + // RegisterProjection holds details about calls to the RegisterProjection method. + RegisterProjection []struct { + // Name is the name argument value. + Name string + // Projection is the projection argument value. + Projection projections.Projection + } + } + lockGetCommandDispatcher sync.RWMutex + lockGetConfig sync.RWMutex + lockGetController sync.RWMutex + lockGetDBConnection sync.RWMutex + lockGetEntityFactories sync.RWMutex + lockGetEntityFactory sync.RWMutex + lockGetEventStore sync.RWMutex + lockGetGlobalInitializers sync.RWMutex + lockGetGormDBConnection sync.RWMutex + lockGetHTTPClient sync.RWMutex + lockGetLog sync.RWMutex + lockGetMiddleware sync.RWMutex + lockGetOperationInitializers sync.RWMutex + lockGetPostPathInitializers sync.RWMutex + lockGetPrePathInitializers sync.RWMutex + lockGetProjection sync.RWMutex + lockGetWeOSConfig sync.RWMutex + lockRegisterCommandDispatcher sync.RWMutex + lockRegisterController sync.RWMutex + lockRegisterDBConnection sync.RWMutex + lockRegisterEntityFactory sync.RWMutex + lockRegisterEventStore sync.RWMutex + lockRegisterGORMDB sync.RWMutex + lockRegisterGlobalInitializer sync.RWMutex + lockRegisterHTTPClient sync.RWMutex + lockRegisterLog sync.RWMutex + lockRegisterMiddleware sync.RWMutex + lockRegisterOperationInitializer sync.RWMutex + lockRegisterPostPathInitializer sync.RWMutex + lockRegisterPrePathInitializer sync.RWMutex + lockRegisterProjection sync.RWMutex +} + +// GetCommandDispatcher calls GetCommandDispatcherFunc. +func (mock *ContainerMock) GetCommandDispatcher(name string) (weos.CommandDispatcher, error) { + if mock.GetCommandDispatcherFunc == nil { + panic("ContainerMock.GetCommandDispatcherFunc: method is nil but Container.GetCommandDispatcher was just called") + } + callInfo := struct { + Name string + }{ + Name: name, + } + mock.lockGetCommandDispatcher.Lock() + mock.calls.GetCommandDispatcher = append(mock.calls.GetCommandDispatcher, callInfo) + mock.lockGetCommandDispatcher.Unlock() + return mock.GetCommandDispatcherFunc(name) +} + +// GetCommandDispatcherCalls gets all the calls that were made to GetCommandDispatcher. +// Check the length with: +// len(mockedContainer.GetCommandDispatcherCalls()) +func (mock *ContainerMock) GetCommandDispatcherCalls() []struct { + Name string +} { + var calls []struct { + Name string + } + mock.lockGetCommandDispatcher.RLock() + calls = mock.calls.GetCommandDispatcher + mock.lockGetCommandDispatcher.RUnlock() + return calls +} + +// GetConfig calls GetConfigFunc. +func (mock *ContainerMock) GetConfig() *openapi3.Swagger { + if mock.GetConfigFunc == nil { + panic("ContainerMock.GetConfigFunc: method is nil but Container.GetConfig was just called") + } + callInfo := struct { + }{} + mock.lockGetConfig.Lock() + mock.calls.GetConfig = append(mock.calls.GetConfig, callInfo) + mock.lockGetConfig.Unlock() + return mock.GetConfigFunc() +} + +// GetConfigCalls gets all the calls that were made to GetConfig. +// Check the length with: +// len(mockedContainer.GetConfigCalls()) +func (mock *ContainerMock) GetConfigCalls() []struct { +} { + var calls []struct { + } + mock.lockGetConfig.RLock() + calls = mock.calls.GetConfig + mock.lockGetConfig.RUnlock() + return calls +} + +// GetController calls GetControllerFunc. +func (mock *ContainerMock) GetController(name string) (rest.Controller, error) { + if mock.GetControllerFunc == nil { + panic("ContainerMock.GetControllerFunc: method is nil but Container.GetController was just called") + } + callInfo := struct { + Name string + }{ + Name: name, + } + mock.lockGetController.Lock() + mock.calls.GetController = append(mock.calls.GetController, callInfo) + mock.lockGetController.Unlock() + return mock.GetControllerFunc(name) +} + +// GetControllerCalls gets all the calls that were made to GetController. +// Check the length with: +// len(mockedContainer.GetControllerCalls()) +func (mock *ContainerMock) GetControllerCalls() []struct { + Name string +} { + var calls []struct { + Name string + } + mock.lockGetController.RLock() + calls = mock.calls.GetController + mock.lockGetController.RUnlock() + return calls +} + +// GetDBConnection calls GetDBConnectionFunc. +func (mock *ContainerMock) GetDBConnection(name string) (*sql.DB, error) { + if mock.GetDBConnectionFunc == nil { + panic("ContainerMock.GetDBConnectionFunc: method is nil but Container.GetDBConnection was just called") + } + callInfo := struct { + Name string + }{ + Name: name, + } + mock.lockGetDBConnection.Lock() + mock.calls.GetDBConnection = append(mock.calls.GetDBConnection, callInfo) + mock.lockGetDBConnection.Unlock() + return mock.GetDBConnectionFunc(name) +} + +// GetDBConnectionCalls gets all the calls that were made to GetDBConnection. +// Check the length with: +// len(mockedContainer.GetDBConnectionCalls()) +func (mock *ContainerMock) GetDBConnectionCalls() []struct { + Name string +} { + var calls []struct { + Name string + } + mock.lockGetDBConnection.RLock() + calls = mock.calls.GetDBConnection + mock.lockGetDBConnection.RUnlock() + return calls +} + +// GetEntityFactories calls GetEntityFactoriesFunc. +func (mock *ContainerMock) GetEntityFactories() map[string]weos.EntityFactory { + if mock.GetEntityFactoriesFunc == nil { + panic("ContainerMock.GetEntityFactoriesFunc: method is nil but Container.GetEntityFactories was just called") + } + callInfo := struct { + }{} + mock.lockGetEntityFactories.Lock() + mock.calls.GetEntityFactories = append(mock.calls.GetEntityFactories, callInfo) + mock.lockGetEntityFactories.Unlock() + return mock.GetEntityFactoriesFunc() +} + +// GetEntityFactoriesCalls gets all the calls that were made to GetEntityFactories. +// Check the length with: +// len(mockedContainer.GetEntityFactoriesCalls()) +func (mock *ContainerMock) GetEntityFactoriesCalls() []struct { +} { + var calls []struct { + } + mock.lockGetEntityFactories.RLock() + calls = mock.calls.GetEntityFactories + mock.lockGetEntityFactories.RUnlock() + return calls +} + +// GetEntityFactory calls GetEntityFactoryFunc. +func (mock *ContainerMock) GetEntityFactory(name string) (weos.EntityFactory, error) { + if mock.GetEntityFactoryFunc == nil { + panic("ContainerMock.GetEntityFactoryFunc: method is nil but Container.GetEntityFactory was just called") + } + callInfo := struct { + Name string + }{ + Name: name, + } + mock.lockGetEntityFactory.Lock() + mock.calls.GetEntityFactory = append(mock.calls.GetEntityFactory, callInfo) + mock.lockGetEntityFactory.Unlock() + return mock.GetEntityFactoryFunc(name) +} + +// GetEntityFactoryCalls gets all the calls that were made to GetEntityFactory. +// Check the length with: +// len(mockedContainer.GetEntityFactoryCalls()) +func (mock *ContainerMock) GetEntityFactoryCalls() []struct { + Name string +} { + var calls []struct { + Name string + } + mock.lockGetEntityFactory.RLock() + calls = mock.calls.GetEntityFactory + mock.lockGetEntityFactory.RUnlock() + return calls +} + +// GetEventStore calls GetEventStoreFunc. +func (mock *ContainerMock) GetEventStore(name string) (weos.EventRepository, error) { + if mock.GetEventStoreFunc == nil { + panic("ContainerMock.GetEventStoreFunc: method is nil but Container.GetEventStore was just called") + } + callInfo := struct { + Name string + }{ + Name: name, + } + mock.lockGetEventStore.Lock() + mock.calls.GetEventStore = append(mock.calls.GetEventStore, callInfo) + mock.lockGetEventStore.Unlock() + return mock.GetEventStoreFunc(name) +} + +// GetEventStoreCalls gets all the calls that were made to GetEventStore. +// Check the length with: +// len(mockedContainer.GetEventStoreCalls()) +func (mock *ContainerMock) GetEventStoreCalls() []struct { + Name string +} { + var calls []struct { + Name string + } + mock.lockGetEventStore.RLock() + calls = mock.calls.GetEventStore + mock.lockGetEventStore.RUnlock() + return calls +} + +// GetGlobalInitializers calls GetGlobalInitializersFunc. +func (mock *ContainerMock) GetGlobalInitializers() []rest.GlobalInitializer { + if mock.GetGlobalInitializersFunc == nil { + panic("ContainerMock.GetGlobalInitializersFunc: method is nil but Container.GetGlobalInitializers was just called") + } + callInfo := struct { + }{} + mock.lockGetGlobalInitializers.Lock() + mock.calls.GetGlobalInitializers = append(mock.calls.GetGlobalInitializers, callInfo) + mock.lockGetGlobalInitializers.Unlock() + return mock.GetGlobalInitializersFunc() +} + +// GetGlobalInitializersCalls gets all the calls that were made to GetGlobalInitializers. +// Check the length with: +// len(mockedContainer.GetGlobalInitializersCalls()) +func (mock *ContainerMock) GetGlobalInitializersCalls() []struct { +} { + var calls []struct { + } + mock.lockGetGlobalInitializers.RLock() + calls = mock.calls.GetGlobalInitializers + mock.lockGetGlobalInitializers.RUnlock() + return calls +} + +// GetGormDBConnection calls GetGormDBConnectionFunc. +func (mock *ContainerMock) GetGormDBConnection(name string) (*gorm.DB, error) { + if mock.GetGormDBConnectionFunc == nil { + panic("ContainerMock.GetGormDBConnectionFunc: method is nil but Container.GetGormDBConnection was just called") + } + callInfo := struct { + Name string + }{ + Name: name, + } + mock.lockGetGormDBConnection.Lock() + mock.calls.GetGormDBConnection = append(mock.calls.GetGormDBConnection, callInfo) + mock.lockGetGormDBConnection.Unlock() + return mock.GetGormDBConnectionFunc(name) +} + +// GetGormDBConnectionCalls gets all the calls that were made to GetGormDBConnection. +// Check the length with: +// len(mockedContainer.GetGormDBConnectionCalls()) +func (mock *ContainerMock) GetGormDBConnectionCalls() []struct { + Name string +} { + var calls []struct { + Name string + } + mock.lockGetGormDBConnection.RLock() + calls = mock.calls.GetGormDBConnection + mock.lockGetGormDBConnection.RUnlock() + return calls +} + +// GetHTTPClient calls GetHTTPClientFunc. +func (mock *ContainerMock) GetHTTPClient(name string) (*http.Client, error) { + if mock.GetHTTPClientFunc == nil { + panic("ContainerMock.GetHTTPClientFunc: method is nil but Container.GetHTTPClient was just called") + } + callInfo := struct { + Name string + }{ + Name: name, + } + mock.lockGetHTTPClient.Lock() + mock.calls.GetHTTPClient = append(mock.calls.GetHTTPClient, callInfo) + mock.lockGetHTTPClient.Unlock() + return mock.GetHTTPClientFunc(name) +} + +// GetHTTPClientCalls gets all the calls that were made to GetHTTPClient. +// Check the length with: +// len(mockedContainer.GetHTTPClientCalls()) +func (mock *ContainerMock) GetHTTPClientCalls() []struct { + Name string +} { + var calls []struct { + Name string + } + mock.lockGetHTTPClient.RLock() + calls = mock.calls.GetHTTPClient + mock.lockGetHTTPClient.RUnlock() + return calls +} + +// GetLog calls GetLogFunc. +func (mock *ContainerMock) GetLog(name string) (weos.Log, error) { + if mock.GetLogFunc == nil { + panic("ContainerMock.GetLogFunc: method is nil but Container.GetLog was just called") + } + callInfo := struct { + Name string + }{ + Name: name, + } + mock.lockGetLog.Lock() + mock.calls.GetLog = append(mock.calls.GetLog, callInfo) + mock.lockGetLog.Unlock() + return mock.GetLogFunc(name) +} + +// GetLogCalls gets all the calls that were made to GetLog. +// Check the length with: +// len(mockedContainer.GetLogCalls()) +func (mock *ContainerMock) GetLogCalls() []struct { + Name string +} { + var calls []struct { + Name string + } + mock.lockGetLog.RLock() + calls = mock.calls.GetLog + mock.lockGetLog.RUnlock() + return calls +} + +// GetMiddleware calls GetMiddlewareFunc. +func (mock *ContainerMock) GetMiddleware(name string) (rest.Middleware, error) { + if mock.GetMiddlewareFunc == nil { + panic("ContainerMock.GetMiddlewareFunc: method is nil but Container.GetMiddleware was just called") + } + callInfo := struct { + Name string + }{ + Name: name, + } + mock.lockGetMiddleware.Lock() + mock.calls.GetMiddleware = append(mock.calls.GetMiddleware, callInfo) + mock.lockGetMiddleware.Unlock() + return mock.GetMiddlewareFunc(name) +} + +// GetMiddlewareCalls gets all the calls that were made to GetMiddleware. +// Check the length with: +// len(mockedContainer.GetMiddlewareCalls()) +func (mock *ContainerMock) GetMiddlewareCalls() []struct { + Name string +} { + var calls []struct { + Name string + } + mock.lockGetMiddleware.RLock() + calls = mock.calls.GetMiddleware + mock.lockGetMiddleware.RUnlock() + return calls +} + +// GetOperationInitializers calls GetOperationInitializersFunc. +func (mock *ContainerMock) GetOperationInitializers() []rest.OperationInitializer { + if mock.GetOperationInitializersFunc == nil { + panic("ContainerMock.GetOperationInitializersFunc: method is nil but Container.GetOperationInitializers was just called") + } + callInfo := struct { + }{} + mock.lockGetOperationInitializers.Lock() + mock.calls.GetOperationInitializers = append(mock.calls.GetOperationInitializers, callInfo) + mock.lockGetOperationInitializers.Unlock() + return mock.GetOperationInitializersFunc() +} + +// GetOperationInitializersCalls gets all the calls that were made to GetOperationInitializers. +// Check the length with: +// len(mockedContainer.GetOperationInitializersCalls()) +func (mock *ContainerMock) GetOperationInitializersCalls() []struct { +} { + var calls []struct { + } + mock.lockGetOperationInitializers.RLock() + calls = mock.calls.GetOperationInitializers + mock.lockGetOperationInitializers.RUnlock() + return calls +} + +// GetPostPathInitializers calls GetPostPathInitializersFunc. +func (mock *ContainerMock) GetPostPathInitializers() []rest.PathInitializer { + if mock.GetPostPathInitializersFunc == nil { + panic("ContainerMock.GetPostPathInitializersFunc: method is nil but Container.GetPostPathInitializers was just called") + } + callInfo := struct { + }{} + mock.lockGetPostPathInitializers.Lock() + mock.calls.GetPostPathInitializers = append(mock.calls.GetPostPathInitializers, callInfo) + mock.lockGetPostPathInitializers.Unlock() + return mock.GetPostPathInitializersFunc() +} + +// GetPostPathInitializersCalls gets all the calls that were made to GetPostPathInitializers. +// Check the length with: +// len(mockedContainer.GetPostPathInitializersCalls()) +func (mock *ContainerMock) GetPostPathInitializersCalls() []struct { +} { + var calls []struct { + } + mock.lockGetPostPathInitializers.RLock() + calls = mock.calls.GetPostPathInitializers + mock.lockGetPostPathInitializers.RUnlock() + return calls +} + +// GetPrePathInitializers calls GetPrePathInitializersFunc. +func (mock *ContainerMock) GetPrePathInitializers() []rest.PathInitializer { + if mock.GetPrePathInitializersFunc == nil { + panic("ContainerMock.GetPrePathInitializersFunc: method is nil but Container.GetPrePathInitializers was just called") + } + callInfo := struct { + }{} + mock.lockGetPrePathInitializers.Lock() + mock.calls.GetPrePathInitializers = append(mock.calls.GetPrePathInitializers, callInfo) + mock.lockGetPrePathInitializers.Unlock() + return mock.GetPrePathInitializersFunc() +} + +// GetPrePathInitializersCalls gets all the calls that were made to GetPrePathInitializers. +// Check the length with: +// len(mockedContainer.GetPrePathInitializersCalls()) +func (mock *ContainerMock) GetPrePathInitializersCalls() []struct { +} { + var calls []struct { + } + mock.lockGetPrePathInitializers.RLock() + calls = mock.calls.GetPrePathInitializers + mock.lockGetPrePathInitializers.RUnlock() + return calls +} + +// GetProjection calls GetProjectionFunc. +func (mock *ContainerMock) GetProjection(name string) (projections.Projection, error) { + if mock.GetProjectionFunc == nil { + panic("ContainerMock.GetProjectionFunc: method is nil but Container.GetProjection was just called") + } + callInfo := struct { + Name string + }{ + Name: name, + } + mock.lockGetProjection.Lock() + mock.calls.GetProjection = append(mock.calls.GetProjection, callInfo) + mock.lockGetProjection.Unlock() + return mock.GetProjectionFunc(name) +} + +// GetProjectionCalls gets all the calls that were made to GetProjection. +// Check the length with: +// len(mockedContainer.GetProjectionCalls()) +func (mock *ContainerMock) GetProjectionCalls() []struct { + Name string +} { + var calls []struct { + Name string + } + mock.lockGetProjection.RLock() + calls = mock.calls.GetProjection + mock.lockGetProjection.RUnlock() + return calls +} + +// GetWeOSConfig calls GetWeOSConfigFunc. +func (mock *ContainerMock) GetWeOSConfig() *rest.APIConfig { + if mock.GetWeOSConfigFunc == nil { + panic("ContainerMock.GetWeOSConfigFunc: method is nil but Container.GetWeOSConfig was just called") + } + callInfo := struct { + }{} + mock.lockGetWeOSConfig.Lock() + mock.calls.GetWeOSConfig = append(mock.calls.GetWeOSConfig, callInfo) + mock.lockGetWeOSConfig.Unlock() + return mock.GetWeOSConfigFunc() +} + +// GetWeOSConfigCalls gets all the calls that were made to GetWeOSConfig. +// Check the length with: +// len(mockedContainer.GetWeOSConfigCalls()) +func (mock *ContainerMock) GetWeOSConfigCalls() []struct { +} { + var calls []struct { + } + mock.lockGetWeOSConfig.RLock() + calls = mock.calls.GetWeOSConfig + mock.lockGetWeOSConfig.RUnlock() + return calls +} + +// RegisterCommandDispatcher calls RegisterCommandDispatcherFunc. +func (mock *ContainerMock) RegisterCommandDispatcher(name string, dispatcher weos.CommandDispatcher) { + if mock.RegisterCommandDispatcherFunc == nil { + panic("ContainerMock.RegisterCommandDispatcherFunc: method is nil but Container.RegisterCommandDispatcher was just called") + } + callInfo := struct { + Name string + Dispatcher weos.CommandDispatcher + }{ + Name: name, + Dispatcher: dispatcher, + } + mock.lockRegisterCommandDispatcher.Lock() + mock.calls.RegisterCommandDispatcher = append(mock.calls.RegisterCommandDispatcher, callInfo) + mock.lockRegisterCommandDispatcher.Unlock() + mock.RegisterCommandDispatcherFunc(name, dispatcher) +} + +// RegisterCommandDispatcherCalls gets all the calls that were made to RegisterCommandDispatcher. +// Check the length with: +// len(mockedContainer.RegisterCommandDispatcherCalls()) +func (mock *ContainerMock) RegisterCommandDispatcherCalls() []struct { + Name string + Dispatcher weos.CommandDispatcher +} { + var calls []struct { + Name string + Dispatcher weos.CommandDispatcher + } + mock.lockRegisterCommandDispatcher.RLock() + calls = mock.calls.RegisterCommandDispatcher + mock.lockRegisterCommandDispatcher.RUnlock() + return calls +} + +// RegisterController calls RegisterControllerFunc. +func (mock *ContainerMock) RegisterController(name string, controller rest.Controller) { + if mock.RegisterControllerFunc == nil { + panic("ContainerMock.RegisterControllerFunc: method is nil but Container.RegisterController was just called") + } + callInfo := struct { + Name string + Controller rest.Controller + }{ + Name: name, + Controller: controller, + } + mock.lockRegisterController.Lock() + mock.calls.RegisterController = append(mock.calls.RegisterController, callInfo) + mock.lockRegisterController.Unlock() + mock.RegisterControllerFunc(name, controller) +} + +// RegisterControllerCalls gets all the calls that were made to RegisterController. +// Check the length with: +// len(mockedContainer.RegisterControllerCalls()) +func (mock *ContainerMock) RegisterControllerCalls() []struct { + Name string + Controller rest.Controller +} { + var calls []struct { + Name string + Controller rest.Controller + } + mock.lockRegisterController.RLock() + calls = mock.calls.RegisterController + mock.lockRegisterController.RUnlock() + return calls +} + +// RegisterDBConnection calls RegisterDBConnectionFunc. +func (mock *ContainerMock) RegisterDBConnection(name string, connection *sql.DB) { + if mock.RegisterDBConnectionFunc == nil { + panic("ContainerMock.RegisterDBConnectionFunc: method is nil but Container.RegisterDBConnection was just called") + } + callInfo := struct { + Name string + Connection *sql.DB + }{ + Name: name, + Connection: connection, + } + mock.lockRegisterDBConnection.Lock() + mock.calls.RegisterDBConnection = append(mock.calls.RegisterDBConnection, callInfo) + mock.lockRegisterDBConnection.Unlock() + mock.RegisterDBConnectionFunc(name, connection) +} + +// RegisterDBConnectionCalls gets all the calls that were made to RegisterDBConnection. +// Check the length with: +// len(mockedContainer.RegisterDBConnectionCalls()) +func (mock *ContainerMock) RegisterDBConnectionCalls() []struct { + Name string + Connection *sql.DB +} { + var calls []struct { + Name string + Connection *sql.DB + } + mock.lockRegisterDBConnection.RLock() + calls = mock.calls.RegisterDBConnection + mock.lockRegisterDBConnection.RUnlock() + return calls +} + +// RegisterEntityFactory calls RegisterEntityFactoryFunc. +func (mock *ContainerMock) RegisterEntityFactory(name string, factory weos.EntityFactory) { + if mock.RegisterEntityFactoryFunc == nil { + panic("ContainerMock.RegisterEntityFactoryFunc: method is nil but Container.RegisterEntityFactory was just called") + } + callInfo := struct { + Name string + Factory weos.EntityFactory + }{ + Name: name, + Factory: factory, + } + mock.lockRegisterEntityFactory.Lock() + mock.calls.RegisterEntityFactory = append(mock.calls.RegisterEntityFactory, callInfo) + mock.lockRegisterEntityFactory.Unlock() + mock.RegisterEntityFactoryFunc(name, factory) +} + +// RegisterEntityFactoryCalls gets all the calls that were made to RegisterEntityFactory. +// Check the length with: +// len(mockedContainer.RegisterEntityFactoryCalls()) +func (mock *ContainerMock) RegisterEntityFactoryCalls() []struct { + Name string + Factory weos.EntityFactory +} { + var calls []struct { + Name string + Factory weos.EntityFactory + } + mock.lockRegisterEntityFactory.RLock() + calls = mock.calls.RegisterEntityFactory + mock.lockRegisterEntityFactory.RUnlock() + return calls +} + +// RegisterEventStore calls RegisterEventStoreFunc. +func (mock *ContainerMock) RegisterEventStore(name string, repository weos.EventRepository) { + if mock.RegisterEventStoreFunc == nil { + panic("ContainerMock.RegisterEventStoreFunc: method is nil but Container.RegisterEventStore was just called") + } + callInfo := struct { + Name string + Repository weos.EventRepository + }{ + Name: name, + Repository: repository, + } + mock.lockRegisterEventStore.Lock() + mock.calls.RegisterEventStore = append(mock.calls.RegisterEventStore, callInfo) + mock.lockRegisterEventStore.Unlock() + mock.RegisterEventStoreFunc(name, repository) +} + +// RegisterEventStoreCalls gets all the calls that were made to RegisterEventStore. +// Check the length with: +// len(mockedContainer.RegisterEventStoreCalls()) +func (mock *ContainerMock) RegisterEventStoreCalls() []struct { + Name string + Repository weos.EventRepository +} { + var calls []struct { + Name string + Repository weos.EventRepository + } + mock.lockRegisterEventStore.RLock() + calls = mock.calls.RegisterEventStore + mock.lockRegisterEventStore.RUnlock() + return calls +} + +// RegisterGORMDB calls RegisterGORMDBFunc. +func (mock *ContainerMock) RegisterGORMDB(name string, connection *gorm.DB) { + if mock.RegisterGORMDBFunc == nil { + panic("ContainerMock.RegisterGORMDBFunc: method is nil but Container.RegisterGORMDB was just called") + } + callInfo := struct { + Name string + Connection *gorm.DB + }{ + Name: name, + Connection: connection, + } + mock.lockRegisterGORMDB.Lock() + mock.calls.RegisterGORMDB = append(mock.calls.RegisterGORMDB, callInfo) + mock.lockRegisterGORMDB.Unlock() + mock.RegisterGORMDBFunc(name, connection) +} + +// RegisterGORMDBCalls gets all the calls that were made to RegisterGORMDB. +// Check the length with: +// len(mockedContainer.RegisterGORMDBCalls()) +func (mock *ContainerMock) RegisterGORMDBCalls() []struct { + Name string + Connection *gorm.DB +} { + var calls []struct { + Name string + Connection *gorm.DB + } + mock.lockRegisterGORMDB.RLock() + calls = mock.calls.RegisterGORMDB + mock.lockRegisterGORMDB.RUnlock() + return calls +} + +// RegisterGlobalInitializer calls RegisterGlobalInitializerFunc. +func (mock *ContainerMock) RegisterGlobalInitializer(initializer rest.GlobalInitializer) { + if mock.RegisterGlobalInitializerFunc == nil { + panic("ContainerMock.RegisterGlobalInitializerFunc: method is nil but Container.RegisterGlobalInitializer was just called") + } + callInfo := struct { + Initializer rest.GlobalInitializer + }{ + Initializer: initializer, + } + mock.lockRegisterGlobalInitializer.Lock() + mock.calls.RegisterGlobalInitializer = append(mock.calls.RegisterGlobalInitializer, callInfo) + mock.lockRegisterGlobalInitializer.Unlock() + mock.RegisterGlobalInitializerFunc(initializer) +} + +// RegisterGlobalInitializerCalls gets all the calls that were made to RegisterGlobalInitializer. +// Check the length with: +// len(mockedContainer.RegisterGlobalInitializerCalls()) +func (mock *ContainerMock) RegisterGlobalInitializerCalls() []struct { + Initializer rest.GlobalInitializer +} { + var calls []struct { + Initializer rest.GlobalInitializer + } + mock.lockRegisterGlobalInitializer.RLock() + calls = mock.calls.RegisterGlobalInitializer + mock.lockRegisterGlobalInitializer.RUnlock() + return calls +} + +// RegisterHTTPClient calls RegisterHTTPClientFunc. +func (mock *ContainerMock) RegisterHTTPClient(name string, client *http.Client) { + if mock.RegisterHTTPClientFunc == nil { + panic("ContainerMock.RegisterHTTPClientFunc: method is nil but Container.RegisterHTTPClient was just called") + } + callInfo := struct { + Name string + Client *http.Client + }{ + Name: name, + Client: client, + } + mock.lockRegisterHTTPClient.Lock() + mock.calls.RegisterHTTPClient = append(mock.calls.RegisterHTTPClient, callInfo) + mock.lockRegisterHTTPClient.Unlock() + mock.RegisterHTTPClientFunc(name, client) +} + +// RegisterHTTPClientCalls gets all the calls that were made to RegisterHTTPClient. +// Check the length with: +// len(mockedContainer.RegisterHTTPClientCalls()) +func (mock *ContainerMock) RegisterHTTPClientCalls() []struct { + Name string + Client *http.Client +} { + var calls []struct { + Name string + Client *http.Client + } + mock.lockRegisterHTTPClient.RLock() + calls = mock.calls.RegisterHTTPClient + mock.lockRegisterHTTPClient.RUnlock() + return calls +} + +// RegisterLog calls RegisterLogFunc. +func (mock *ContainerMock) RegisterLog(name string, logger weos.Log) { + if mock.RegisterLogFunc == nil { + panic("ContainerMock.RegisterLogFunc: method is nil but Container.RegisterLog was just called") + } + callInfo := struct { + Name string + Logger weos.Log + }{ + Name: name, + Logger: logger, + } + mock.lockRegisterLog.Lock() + mock.calls.RegisterLog = append(mock.calls.RegisterLog, callInfo) + mock.lockRegisterLog.Unlock() + mock.RegisterLogFunc(name, logger) +} + +// RegisterLogCalls gets all the calls that were made to RegisterLog. +// Check the length with: +// len(mockedContainer.RegisterLogCalls()) +func (mock *ContainerMock) RegisterLogCalls() []struct { + Name string + Logger weos.Log +} { + var calls []struct { + Name string + Logger weos.Log + } + mock.lockRegisterLog.RLock() + calls = mock.calls.RegisterLog + mock.lockRegisterLog.RUnlock() + return calls +} + +// RegisterMiddleware calls RegisterMiddlewareFunc. +func (mock *ContainerMock) RegisterMiddleware(name string, middleware rest.Middleware) { + if mock.RegisterMiddlewareFunc == nil { + panic("ContainerMock.RegisterMiddlewareFunc: method is nil but Container.RegisterMiddleware was just called") + } + callInfo := struct { + Name string + Middleware rest.Middleware + }{ + Name: name, + Middleware: middleware, + } + mock.lockRegisterMiddleware.Lock() + mock.calls.RegisterMiddleware = append(mock.calls.RegisterMiddleware, callInfo) + mock.lockRegisterMiddleware.Unlock() + mock.RegisterMiddlewareFunc(name, middleware) +} + +// RegisterMiddlewareCalls gets all the calls that were made to RegisterMiddleware. +// Check the length with: +// len(mockedContainer.RegisterMiddlewareCalls()) +func (mock *ContainerMock) RegisterMiddlewareCalls() []struct { + Name string + Middleware rest.Middleware +} { + var calls []struct { + Name string + Middleware rest.Middleware + } + mock.lockRegisterMiddleware.RLock() + calls = mock.calls.RegisterMiddleware + mock.lockRegisterMiddleware.RUnlock() + return calls +} + +// RegisterOperationInitializer calls RegisterOperationInitializerFunc. +func (mock *ContainerMock) RegisterOperationInitializer(initializer rest.OperationInitializer) { + if mock.RegisterOperationInitializerFunc == nil { + panic("ContainerMock.RegisterOperationInitializerFunc: method is nil but Container.RegisterOperationInitializer was just called") + } + callInfo := struct { + Initializer rest.OperationInitializer + }{ + Initializer: initializer, + } + mock.lockRegisterOperationInitializer.Lock() + mock.calls.RegisterOperationInitializer = append(mock.calls.RegisterOperationInitializer, callInfo) + mock.lockRegisterOperationInitializer.Unlock() + mock.RegisterOperationInitializerFunc(initializer) +} + +// RegisterOperationInitializerCalls gets all the calls that were made to RegisterOperationInitializer. +// Check the length with: +// len(mockedContainer.RegisterOperationInitializerCalls()) +func (mock *ContainerMock) RegisterOperationInitializerCalls() []struct { + Initializer rest.OperationInitializer +} { + var calls []struct { + Initializer rest.OperationInitializer + } + mock.lockRegisterOperationInitializer.RLock() + calls = mock.calls.RegisterOperationInitializer + mock.lockRegisterOperationInitializer.RUnlock() + return calls +} + +// RegisterPostPathInitializer calls RegisterPostPathInitializerFunc. +func (mock *ContainerMock) RegisterPostPathInitializer(initializer rest.PathInitializer) { + if mock.RegisterPostPathInitializerFunc == nil { + panic("ContainerMock.RegisterPostPathInitializerFunc: method is nil but Container.RegisterPostPathInitializer was just called") + } + callInfo := struct { + Initializer rest.PathInitializer + }{ + Initializer: initializer, + } + mock.lockRegisterPostPathInitializer.Lock() + mock.calls.RegisterPostPathInitializer = append(mock.calls.RegisterPostPathInitializer, callInfo) + mock.lockRegisterPostPathInitializer.Unlock() + mock.RegisterPostPathInitializerFunc(initializer) +} + +// RegisterPostPathInitializerCalls gets all the calls that were made to RegisterPostPathInitializer. +// Check the length with: +// len(mockedContainer.RegisterPostPathInitializerCalls()) +func (mock *ContainerMock) RegisterPostPathInitializerCalls() []struct { + Initializer rest.PathInitializer +} { + var calls []struct { + Initializer rest.PathInitializer + } + mock.lockRegisterPostPathInitializer.RLock() + calls = mock.calls.RegisterPostPathInitializer + mock.lockRegisterPostPathInitializer.RUnlock() + return calls +} + +// RegisterPrePathInitializer calls RegisterPrePathInitializerFunc. +func (mock *ContainerMock) RegisterPrePathInitializer(initializer rest.PathInitializer) { + if mock.RegisterPrePathInitializerFunc == nil { + panic("ContainerMock.RegisterPrePathInitializerFunc: method is nil but Container.RegisterPrePathInitializer was just called") + } + callInfo := struct { + Initializer rest.PathInitializer + }{ + Initializer: initializer, + } + mock.lockRegisterPrePathInitializer.Lock() + mock.calls.RegisterPrePathInitializer = append(mock.calls.RegisterPrePathInitializer, callInfo) + mock.lockRegisterPrePathInitializer.Unlock() + mock.RegisterPrePathInitializerFunc(initializer) +} + +// RegisterPrePathInitializerCalls gets all the calls that were made to RegisterPrePathInitializer. +// Check the length with: +// len(mockedContainer.RegisterPrePathInitializerCalls()) +func (mock *ContainerMock) RegisterPrePathInitializerCalls() []struct { + Initializer rest.PathInitializer +} { + var calls []struct { + Initializer rest.PathInitializer + } + mock.lockRegisterPrePathInitializer.RLock() + calls = mock.calls.RegisterPrePathInitializer + mock.lockRegisterPrePathInitializer.RUnlock() + return calls +} + +// RegisterProjection calls RegisterProjectionFunc. +func (mock *ContainerMock) RegisterProjection(name string, projection projections.Projection) { + if mock.RegisterProjectionFunc == nil { + panic("ContainerMock.RegisterProjectionFunc: method is nil but Container.RegisterProjection was just called") + } + callInfo := struct { + Name string + Projection projections.Projection + }{ + Name: name, + Projection: projection, + } + mock.lockRegisterProjection.Lock() + mock.calls.RegisterProjection = append(mock.calls.RegisterProjection, callInfo) + mock.lockRegisterProjection.Unlock() + mock.RegisterProjectionFunc(name, projection) +} + +// RegisterProjectionCalls gets all the calls that were made to RegisterProjection. +// Check the length with: +// len(mockedContainer.RegisterProjectionCalls()) +func (mock *ContainerMock) RegisterProjectionCalls() []struct { + Name string + Projection projections.Projection +} { + var calls []struct { + Name string + Projection projections.Projection + } + mock.lockRegisterProjection.RLock() + calls = mock.calls.RegisterProjection + mock.lockRegisterProjection.RUnlock() + return calls +} From 78631ad5c52cf05224d98eebe5bf9d2901a9afbb Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Sun, 19 Jun 2022 15:43:38 -0400 Subject: [PATCH 03/18] docs: Added spec for authorization configuration --- end2end_test.go | 4 +-- features/security-schemes.feature | 55 +++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/end2end_test.go b/end2end_test.go index b68d1f75..f23b8592 100644 --- a/end2end_test.go +++ b/end2end_test.go @@ -2143,9 +2143,9 @@ func TestBDD(t *testing.T) { TestSuiteInitializer: InitializeSuite, Options: &godog.Options{ Format: "pretty", - Tags: "~long && ~skipped", + //Tags: "~long && ~skipped", //Tags: "WEOS-1378", - //Tags: "WEOS-1327 && ~skipped", + Tags: "WEOS-1343 && ~skipped", }, }.Run() if status != 0 { diff --git a/features/security-schemes.feature b/features/security-schemes.feature index 53bca380..7906bbe6 100644 --- a/features/security-schemes.feature +++ b/features/security-schemes.feature @@ -47,6 +47,9 @@ Feature: Use OpenAPI Security Scheme to protect endpoints type: openIdConnect openIdConnectUrl: https://dev-bhjqt6zc.us.auth0.com/.well-known/openid-configuration x-skip-expiry-check: true + x-jwt-map: + user: sub + role: azp schemas: Blog: type: object @@ -115,6 +118,10 @@ Feature: Use OpenAPI Security Scheme to protect endpoints name: Authorization schema: type: string + x-auth: + allow: + users: + - auth0|1234 requestBody: description: Blog info that is submitted required: true @@ -211,6 +218,10 @@ Feature: Use OpenAPI Security Scheme to protect endpoints type: string summary: Get Blog by id operationId: Get Blog + x-auth: + allow: + roles: + - Y9IvGucEhViFd58GL0bBoNrgEk3ohW88 responses: 200: description: Blog details without any supporting collections @@ -238,6 +249,10 @@ Feature: Use OpenAPI Security Scheme to protect endpoints application/json: schema: $ref: "#/components/schemas/Blog" + x-auth: + deny: + roles: + - Y9IvGucEhViFd58GL0bBoNrgEk3ohW88 responses: 200: description: Update Blog @@ -358,7 +373,7 @@ Feature: Use OpenAPI Security Scheme to protect endpoints If the openIdConnectUrl set is not a valid openid connect url then a warning should be shown to the developer - And the specification is + Given the specification is """ openapi: 3.0.3 info: @@ -519,7 +534,7 @@ Feature: Use OpenAPI Security Scheme to protect endpoints If the developer references a security scheme that is not defined then an error should be shown so that the developer knows that security was not correctly configured. - And the specification is + Given the specification is """ openapi: 3.0.3 info: @@ -675,6 +690,42 @@ Feature: Use OpenAPI Security Scheme to protect endpoints When the "OpenAPI 3.0" specification is parsed Then an error should be returned + @WEOS-1519 + Scenario: User Denied based on id not being in the allow list + + In order to support JWT from different authentication services, the developer should be able to specify which part of + the JWT should be used for the user id, role, organization + + Given "Sojourner" is on the "Blog" create screen + And "Sojourner" authenticated and received a JWT + And "Sojourner" enters "3" in the "id" field + And "Sojourner" enters "Some Blog" in the "title" field + And "Sojourner" enters "Some Description" in the "description" field + When the "Blog" is submitted + Then a 403 response should be returned + + + @WEOS-1519 + Scenario: User Allowed based on the role being on the allow list + + In order to support JWT from different authentication services, the developer should be able to specify which part of + the JWT should be used for the user id, role, organization + + Given "Sojourner" authenticated and received a JWT + When the "GET" endpoint "/blogs/1234" is hit + Then a 200 response should be returned + + + @WEOS-1519 + Scenario: User denied based on the role being on the deny list + + Given "Sojourner" is on the "Blog" edit screen with id "1234" + And "Sojourner" authenticated and received a JWT + And "Sojourner" enters "Some New Title" in the "title" field + And "Sojourner" enters "Some Description" in the "description" field + When the "Blog" is submitted + Then a 403 response should be returned + Scenario: Request with missing required scope Scenario: Set security on a specific path \ No newline at end of file From 017c7d2ac53eb15e2ed7e924bb0b23a30ceeaff6 Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Sun, 19 Jun 2022 19:13:24 -0400 Subject: [PATCH 04/18] feature: WEOS-1518 WEOS-1519 WEOS-1344 Create Security Middleware for managing security configuration * Created Security Configuration struct for managing the api security * Add Security Configuration to the container * Created a concept of Authenticator which defines how each security scheme type should be implemented --- controllers/rest/api.go | 9 + .../rest/fixtures/blog-security-invalid.yaml | 730 ++++++++++++++++++ controllers/rest/helpers_test.go | 29 + controllers/rest/interfaces.go | 4 +- controllers/rest/rest_mocks_test.go | 250 +++++- controllers/rest/security.go | 112 +++ controllers/rest/security_test.go | 107 +++ 7 files changed, 1209 insertions(+), 32 deletions(-) create mode 100644 controllers/rest/fixtures/blog-security-invalid.yaml create mode 100644 controllers/rest/helpers_test.go create mode 100644 controllers/rest/security.go create mode 100644 controllers/rest/security_test.go diff --git a/controllers/rest/api.go b/controllers/rest/api.go index 754c6e0a..0785cebd 100644 --- a/controllers/rest/api.go +++ b/controllers/rest/api.go @@ -38,6 +38,7 @@ type RESTAPI struct { Client *http.Client projection *projections.GORMDB Config *APIConfig + securityConfiguration *SecurityConfiguration e *echo.Echo PathConfigs map[string]*PathConfig Schemas map[string]ds.Builder @@ -372,6 +373,14 @@ func (p *RESTAPI) GetHTTPClient(name string) (*http.Client, error) { return nil, fmt.Errorf("http client '%s' not found", name) } +func (p *RESTAPI) RegisterSecurityConfiguration(configuration *SecurityConfiguration) { + p.securityConfiguration = configuration +} + +func (p *RESTAPI) GetSecurityConfiguration() *SecurityConfiguration { + return p.securityConfiguration +} + const SWAGGERUIENDPOINT = "/_discover/" const SWAGGERJSONENDPOINT = "/_discover_json" diff --git a/controllers/rest/fixtures/blog-security-invalid.yaml b/controllers/rest/fixtures/blog-security-invalid.yaml new file mode 100644 index 00000000..fe10b1ec --- /dev/null +++ b/controllers/rest/fixtures/blog-security-invalid.yaml @@ -0,0 +1,730 @@ +openapi: 3.0.3 +info: + title: Blog + description: Blog example + version: 1.0.0 +servers: + - url: https://prod1.weos.sh/blog/dev + description: WeOS Dev + - url: https://prod1.weos.sh/blog/v1 +x-weos-config: + event-source: + - title: default + driver: service + endpoint: https://prod1.weos.sh/events/v1 + - title: event + driver: sqlite3 + database: test.db + database: + driver: sqlite3 + database: test.db + databases: + - title: default + driver: sqlite3 + database: test.db + rest: + middleware: + - RequestID + - Recover + - ZapLogger +components: + securitySchemes: + Auth0: + type: invalidType + openIdConnectUrl: https://dev-bhjqt6zc.us.auth0.com/.well-known/openid-configuration + x-skip-expiry-check: true + schemas: + Category: + type: object + properties: + title: + type: string + description: + type: string + nullable: true + required: + - title + x-identifier: + - title + Author: + type: object + properties: + id: + type: string + format: ksuid + firstName: + type: string + nullable: true + lastName: + type: string + nullable: true + email: + type: string + format: email + nullable: true + required: + - firstName + - lastName + x-identifier: + - id + - email + Blog: + type: object + properties: + url: + type: string + format: uri + title: + type: string + description: + type: string + nullable: true + status: + type: string + nullable: true + enum: + - "null" + - unpublished + - published + image: + type: string + format: byte + nullable: true + categories: + type: array + items: + $ref: "#/components/schemas/Category" + nullable: true + posts: + type: array + items: + $ref: "#/components/schemas/Post" + nullable: true + lastUpdated: + type: string + format: date-time + nullable: true + created: + type: string + format: date-time + nullable: true + required: + - title + - url + Post: + type: object + properties: + title: + type: string + description: + type: string + nullable: true + author: + $ref: "#/components/schemas/Author" + created: + type: string + format: date-time + nullable: true +security: + - Auth0: ["email","name"] + +paths: + /health: + summary: Health Check + get: + security: [] + x-controller: HealthCheck + x-middleware: + - Recover + - OpenIDMiddleware + responses: + 200: + description: Health Response + 500: + description: API Internal Error + /blogs: + parameters: + - in: header + name: someHeader + schema: + type: string + - in: header + name: someOtherHeader + schema: + type: string + x-context-name: soh + - in: header + name: X-Account-Id + schema: + type: string + x-context-name: AccountID + - in: query + name: q + schema: + type: string + - in: query + name: cost + schema: + type: number + - in: query + name: leverage + schema: + type: number + format: double + post: + operationId: Add Blog + summary: Create Blog + x-projection: Default + x-event-dispatcher: Default + x-command-disptacher: Default + parameters: + - in: header + name: Authorization + schema: + type: string + x-middleware: + - OpenIDMiddleware + requestBody: + description: Blog info that is submitted + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Blog" + multipart/form-data: + schema: + $ref: "#/components/schemas/Blog" + responses: + 201: + description: Add Blog to Aggregator + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + get: + operationId: Get Blogs + summary: Get List of Blogs + parameters: + - in: query + name: page + schema: + type: integer + - in: query + name: l + x-alias: limit + schema: + type: integer + - in: query + name: _filters + schema: + type: array + items: + type: object + properties: + field: + type: string + operator: + type: string + values: + type: array + items: + type: string + + required: false + description: query string + x-context: + filters: + - field: status + operator: eq + values: + - Active + - field: lastUpdated + operator: between + values: + - 2021-12-17 15:46:00 + - 2021-12-18 15:46:00 + - field: categories + operator: in + values: + - Technology + - Javascript + sorts: + - field: title + order: asc + page: 1 + limit: 10 + responses: + 200: + description: List of blogs + content: + application/json: + schema: + type: object + properties: + total: + type: integer + page: + type: integer + blogs: + type: array + x-alias: items + items: + $ref: "#/components/schemas/Blog" + put: + operationId: Import blogs + requestBody: + content: + text/csv: + schema: + type: string + format: binary + responses: + 201: + description: items created + + /blogs/{id}: + parameters: + - in: query + name: sequence_no + schema: + type: integer + - in: query + name: use_entity_id + schema: + type: boolean + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + description: blog id + - in: header + name: If-Match + schema: + type: string + required: false + - in: query + name: cost + schema: + type: number + - in: query + name: leverage + schema: + type: number + format: double + summary: Get Blog by id + operationId: Get Blog + responses: + 200: + description: Blog details without any supporting collections + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + put: + parameters: + - in: path + name: id + schema: + type: string + required: true + description: blog id + - in: header + name: If-Match + schema: + type: string + - in: query + name: cost + schema: + type: number + - in: query + name: leverage + schema: + type: number + format: double + summary: Update blog details + operationId: Update Blog + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + responses: + 200: + description: Update Blog + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + delete: + parameters: + - in: path + name: id + schema: + type: string + required: true + description: blog id + - in: header + name: If-Match + schema: + type: string + x-schema: "Blog" + summary: Delete blog + operationId: Delete Blog + responses: + 200: + description: Blog Deleted + + /posts/: + post: + operationId: Create Blog Post + summary: Create Blog Post + requestBody: + description: Post details + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Post" + responses: + 201: + description: Post + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + put: + operationId: Import Blog Posts + summary: Import Blog Posts + requestBody: + description: List of posts to import + required: true + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Post" + application/x-www-form-urlencoded: + schema: + type: array + items: + $ref: "#/components/schemas/Post" + responses: + 201: + description: Post + get: + operationId: Get Posts + summary: Get a blog's list of posts + parameters: + - in: query + name: q + schema: + type: string + required: false + description: query string + responses: + 200: + description: List of blog posts + content: + application/json: + schema: + type: object + properties: + total: + type: integer + page: + type: integer + items: + type: array + items: + $ref: "#/components/schemas/Post" + + /posts/{id}: + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Get blog post by id + responses: + 200: + description: Get blog post information + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + put: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Update post + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + responses: + 200: + description: Get blog post information + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + delete: + parameters: + - in: path + name: id + schema: + type: string + required: true + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + summary: Delete post + responses: + 200: + description: Delete post + + + /categories/: + post: + operationId: Create Blog Category + summary: Create Blog Category + requestBody: + description: Post details + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Category" + responses: + 201: + description: Post + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + get: + operationId: Get Categories + summary: Get a blog's list of categories + parameters: + - in: query + name: q + schema: + type: string + required: false + description: query string + responses: + 200: + description: List of blog categories + content: + application/json: + schema: + type: object + properties: + total: + type: integer + page: + type: integer + items: + type: array + items: + $ref: "#/components/schemas/Category" + + /categories/{title}: + get: + parameters: + - in: path + name: title + schema: + type: string + required: true + summary: Get blog category by title + responses: + 200: + description: Get blog category information + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + put: + parameters: + - in: path + name: title + schema: + type: string + required: true + summary: Update category + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + responses: + 200: + description: Get blog category information + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + delete: + parameters: + - in: path + name: title + schema: + type: string + required: true + requestBody: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + summary: Delete category + responses: + 200: + description: Delete category + + /authors/: + post: + operationId: Create Blog Author + summary: Create Blog Author + requestBody: + description: Author details + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Author" + responses: + 201: + description: Post + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + get: + operationId: Get Authors + summary: Get a blog's list of authors + parameters: + - in: query + name: q + schema: + type: string + required: false + description: query string + responses: + 200: + description: List of blog authors + content: + application/json: + schema: + type: object + properties: + total: + type: integer + page: + type: integer + items: + type: array + items: + $ref: "#/components/schemas/Author" + + /authors/{id}: + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + - in: header + name: email + schema: + type: string + format: email + required: true + summary: Get Author by email and id + responses: + 200: + description: Get author information + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + put: + parameters: + - in: path + name: id + schema: + type: string + required: true + - in: header + name: email + schema: + type: string + format: email + required: true + summary: Update Author details + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + responses: + 200: + description: Author details + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + delete: + parameters: + - in: path + name: id + schema: + type: string + required: true + - in: header + name: email + schema: + type: string + format: email + required: true + requestBody: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + summary: Delete author + responses: + 200: + description: Delete author diff --git a/controllers/rest/helpers_test.go b/controllers/rest/helpers_test.go new file mode 100644 index 00000000..7ce22d79 --- /dev/null +++ b/controllers/rest/helpers_test.go @@ -0,0 +1,29 @@ +package rest_test + +import ( + "github.com/getkin/kin-openapi/openapi3" + "io/ioutil" + "os" + "regexp" + "strings" + "testing" +) + +func LoadConfig(t *testing.T, file string) (*openapi3.Swagger, error) { + //load config + content, err := ioutil.ReadFile(file) + if err != nil { + t.Fatalf("error loading api specification '%s'", err) + } + //change the $ref to another marker so that it doesn't get considered an environment variable WECON-1 + tempFile := strings.ReplaceAll(string(content), "$ref", "__ref__") + //replace environment variables in file + tempFile = os.ExpandEnv(string(tempFile)) + tempFile = strings.ReplaceAll(string(tempFile), "__ref__", "$ref") + //update path so that the open api way of specifying url parameters is change to the echo style of url parameters + re := regexp.MustCompile(`\{([a-zA-Z0-9\-_]+?)\}`) + tempFile = re.ReplaceAllString(tempFile, `:$1`) + content = []byte(tempFile) + loader := openapi3.NewSwaggerLoader() + return loader.LoadSwaggerFromData(content) +} diff --git a/controllers/rest/interfaces.go b/controllers/rest/interfaces.go index e08337af..52e54291 100644 --- a/controllers/rest/interfaces.go +++ b/controllers/rest/interfaces.go @@ -1,4 +1,4 @@ -//go:generate moq -out rest_mocks_test.go -pkg rest_test . Container +//go:generate moq -out rest_mocks_test.go -pkg rest_test . Container Authenticator package rest import ( @@ -86,4 +86,6 @@ type Container interface { RegisterHTTPClient(name string, client *http.Client) //GetHTTPClient return htpt client GetHTTPClient(name string) (*http.Client, error) + RegisterSecurityConfiguration(configuration *SecurityConfiguration) + GetSecurityConfiguration() *SecurityConfiguration } diff --git a/controllers/rest/rest_mocks_test.go b/controllers/rest/rest_mocks_test.go index ce19cf1a..3eec4d1c 100644 --- a/controllers/rest/rest_mocks_test.go +++ b/controllers/rest/rest_mocks_test.go @@ -6,6 +6,7 @@ package rest_test import ( "database/sql" "github.com/getkin/kin-openapi/openapi3" + "github.com/labstack/echo/v4" "github.com/wepala/weos/controllers/rest" weos "github.com/wepala/weos/model" "github.com/wepala/weos/projections" @@ -72,6 +73,9 @@ var _ rest.Container = &ContainerMock{} // GetProjectionFunc: func(name string) (projections.Projection, error) { // panic("mock out the GetProjection method") // }, +// GetSecurityConfigurationFunc: func() *rest.SecurityConfiguration { +// panic("mock out the GetSecurityConfiguration method") +// }, // GetWeOSConfigFunc: func() *rest.APIConfig { // panic("mock out the GetWeOSConfig method") // }, @@ -117,6 +121,9 @@ var _ rest.Container = &ContainerMock{} // RegisterProjectionFunc: func(name string, projection projections.Projection) { // panic("mock out the RegisterProjection method") // }, +// RegisterSecurityConfigurationFunc: func(configuration *rest.SecurityConfiguration) { +// panic("mock out the RegisterSecurityConfiguration method") +// }, // } // // // use mockedContainer in code that requires rest.Container @@ -172,6 +179,9 @@ type ContainerMock struct { // GetProjectionFunc mocks the GetProjection method. GetProjectionFunc func(name string) (projections.Projection, error) + // GetSecurityConfigurationFunc mocks the GetSecurityConfiguration method. + GetSecurityConfigurationFunc func() *rest.SecurityConfiguration + // GetWeOSConfigFunc mocks the GetWeOSConfig method. GetWeOSConfigFunc func() *rest.APIConfig @@ -217,6 +227,9 @@ type ContainerMock struct { // RegisterProjectionFunc mocks the RegisterProjection method. RegisterProjectionFunc func(name string, projection projections.Projection) + // RegisterSecurityConfigurationFunc mocks the RegisterSecurityConfiguration method. + RegisterSecurityConfigurationFunc func(configuration *rest.SecurityConfiguration) + // calls tracks calls to the methods. calls struct { // GetCommandDispatcher holds details about calls to the GetCommandDispatcher method. @@ -287,6 +300,9 @@ type ContainerMock struct { // Name is the name argument value. Name string } + // GetSecurityConfiguration holds details about calls to the GetSecurityConfiguration method. + GetSecurityConfiguration []struct { + } // GetWeOSConfig holds details about calls to the GetWeOSConfig method. GetWeOSConfig []struct { } @@ -380,38 +396,45 @@ type ContainerMock struct { // Projection is the projection argument value. Projection projections.Projection } + // RegisterSecurityConfiguration holds details about calls to the RegisterSecurityConfiguration method. + RegisterSecurityConfiguration []struct { + // Configuration is the configuration argument value. + Configuration *rest.SecurityConfiguration + } } - lockGetCommandDispatcher sync.RWMutex - lockGetConfig sync.RWMutex - lockGetController sync.RWMutex - lockGetDBConnection sync.RWMutex - lockGetEntityFactories sync.RWMutex - lockGetEntityFactory sync.RWMutex - lockGetEventStore sync.RWMutex - lockGetGlobalInitializers sync.RWMutex - lockGetGormDBConnection sync.RWMutex - lockGetHTTPClient sync.RWMutex - lockGetLog sync.RWMutex - lockGetMiddleware sync.RWMutex - lockGetOperationInitializers sync.RWMutex - lockGetPostPathInitializers sync.RWMutex - lockGetPrePathInitializers sync.RWMutex - lockGetProjection sync.RWMutex - lockGetWeOSConfig sync.RWMutex - lockRegisterCommandDispatcher sync.RWMutex - lockRegisterController sync.RWMutex - lockRegisterDBConnection sync.RWMutex - lockRegisterEntityFactory sync.RWMutex - lockRegisterEventStore sync.RWMutex - lockRegisterGORMDB sync.RWMutex - lockRegisterGlobalInitializer sync.RWMutex - lockRegisterHTTPClient sync.RWMutex - lockRegisterLog sync.RWMutex - lockRegisterMiddleware sync.RWMutex - lockRegisterOperationInitializer sync.RWMutex - lockRegisterPostPathInitializer sync.RWMutex - lockRegisterPrePathInitializer sync.RWMutex - lockRegisterProjection sync.RWMutex + lockGetCommandDispatcher sync.RWMutex + lockGetConfig sync.RWMutex + lockGetController sync.RWMutex + lockGetDBConnection sync.RWMutex + lockGetEntityFactories sync.RWMutex + lockGetEntityFactory sync.RWMutex + lockGetEventStore sync.RWMutex + lockGetGlobalInitializers sync.RWMutex + lockGetGormDBConnection sync.RWMutex + lockGetHTTPClient sync.RWMutex + lockGetLog sync.RWMutex + lockGetMiddleware sync.RWMutex + lockGetOperationInitializers sync.RWMutex + lockGetPostPathInitializers sync.RWMutex + lockGetPrePathInitializers sync.RWMutex + lockGetProjection sync.RWMutex + lockGetSecurityConfiguration sync.RWMutex + lockGetWeOSConfig sync.RWMutex + lockRegisterCommandDispatcher sync.RWMutex + lockRegisterController sync.RWMutex + lockRegisterDBConnection sync.RWMutex + lockRegisterEntityFactory sync.RWMutex + lockRegisterEventStore sync.RWMutex + lockRegisterGORMDB sync.RWMutex + lockRegisterGlobalInitializer sync.RWMutex + lockRegisterHTTPClient sync.RWMutex + lockRegisterLog sync.RWMutex + lockRegisterMiddleware sync.RWMutex + lockRegisterOperationInitializer sync.RWMutex + lockRegisterPostPathInitializer sync.RWMutex + lockRegisterPrePathInitializer sync.RWMutex + lockRegisterProjection sync.RWMutex + lockRegisterSecurityConfiguration sync.RWMutex } // GetCommandDispatcher calls GetCommandDispatcherFunc. @@ -880,6 +903,32 @@ func (mock *ContainerMock) GetProjectionCalls() []struct { return calls } +// GetSecurityConfiguration calls GetSecurityConfigurationFunc. +func (mock *ContainerMock) GetSecurityConfiguration() *rest.SecurityConfiguration { + if mock.GetSecurityConfigurationFunc == nil { + panic("ContainerMock.GetSecurityConfigurationFunc: method is nil but Container.GetSecurityConfiguration was just called") + } + callInfo := struct { + }{} + mock.lockGetSecurityConfiguration.Lock() + mock.calls.GetSecurityConfiguration = append(mock.calls.GetSecurityConfiguration, callInfo) + mock.lockGetSecurityConfiguration.Unlock() + return mock.GetSecurityConfigurationFunc() +} + +// GetSecurityConfigurationCalls gets all the calls that were made to GetSecurityConfiguration. +// Check the length with: +// len(mockedContainer.GetSecurityConfigurationCalls()) +func (mock *ContainerMock) GetSecurityConfigurationCalls() []struct { +} { + var calls []struct { + } + mock.lockGetSecurityConfiguration.RLock() + calls = mock.calls.GetSecurityConfiguration + mock.lockGetSecurityConfiguration.RUnlock() + return calls +} + // GetWeOSConfig calls GetWeOSConfigFunc. func (mock *ContainerMock) GetWeOSConfig() *rest.APIConfig { if mock.GetWeOSConfigFunc == nil { @@ -1379,3 +1428,142 @@ func (mock *ContainerMock) RegisterProjectionCalls() []struct { mock.lockRegisterProjection.RUnlock() return calls } + +// RegisterSecurityConfiguration calls RegisterSecurityConfigurationFunc. +func (mock *ContainerMock) RegisterSecurityConfiguration(configuration *rest.SecurityConfiguration) { + if mock.RegisterSecurityConfigurationFunc == nil { + panic("ContainerMock.RegisterSecurityConfigurationFunc: method is nil but Container.RegisterSecurityConfiguration was just called") + } + callInfo := struct { + Configuration *rest.SecurityConfiguration + }{ + Configuration: configuration, + } + mock.lockRegisterSecurityConfiguration.Lock() + mock.calls.RegisterSecurityConfiguration = append(mock.calls.RegisterSecurityConfiguration, callInfo) + mock.lockRegisterSecurityConfiguration.Unlock() + mock.RegisterSecurityConfigurationFunc(configuration) +} + +// RegisterSecurityConfigurationCalls gets all the calls that were made to RegisterSecurityConfiguration. +// Check the length with: +// len(mockedContainer.RegisterSecurityConfigurationCalls()) +func (mock *ContainerMock) RegisterSecurityConfigurationCalls() []struct { + Configuration *rest.SecurityConfiguration +} { + var calls []struct { + Configuration *rest.SecurityConfiguration + } + mock.lockRegisterSecurityConfiguration.RLock() + calls = mock.calls.RegisterSecurityConfiguration + mock.lockRegisterSecurityConfiguration.RUnlock() + return calls +} + +// Ensure, that AuthenticatorMock does implement rest.Authenticator. +// If this is not the case, regenerate this file with moq. +var _ rest.Authenticator = &AuthenticatorMock{} + +// AuthenticatorMock is a mock implementation of rest.Authenticator. +// +// func TestSomethingThatUsesAuthenticator(t *testing.T) { +// +// // make and configure a mocked rest.Authenticator +// mockedAuthenticator := &AuthenticatorMock{ +// AuthenticateFunc: func(ctxt echo.Context) (bool, error) { +// panic("mock out the Authenticate method") +// }, +// FromSchemaFunc: func(scheme *openapi3.SecurityScheme) (rest.Authenticator, error) { +// panic("mock out the FromSchema method") +// }, +// } +// +// // use mockedAuthenticator in code that requires rest.Authenticator +// // and then make assertions. +// +// } +type AuthenticatorMock struct { + // AuthenticateFunc mocks the Authenticate method. + AuthenticateFunc func(ctxt echo.Context) (bool, error) + + // FromSchemaFunc mocks the FromSchema method. + FromSchemaFunc func(scheme *openapi3.SecurityScheme) (rest.Authenticator, error) + + // calls tracks calls to the methods. + calls struct { + // Authenticate holds details about calls to the Authenticate method. + Authenticate []struct { + // Ctxt is the ctxt argument value. + Ctxt echo.Context + } + // FromSchema holds details about calls to the FromSchema method. + FromSchema []struct { + // Scheme is the scheme argument value. + Scheme *openapi3.SecurityScheme + } + } + lockAuthenticate sync.RWMutex + lockFromSchema sync.RWMutex +} + +// Authenticate calls AuthenticateFunc. +func (mock *AuthenticatorMock) Authenticate(ctxt echo.Context) (bool, error) { + if mock.AuthenticateFunc == nil { + panic("AuthenticatorMock.AuthenticateFunc: method is nil but Authenticator.Authenticate was just called") + } + callInfo := struct { + Ctxt echo.Context + }{ + Ctxt: ctxt, + } + mock.lockAuthenticate.Lock() + mock.calls.Authenticate = append(mock.calls.Authenticate, callInfo) + mock.lockAuthenticate.Unlock() + return mock.AuthenticateFunc(ctxt) +} + +// AuthenticateCalls gets all the calls that were made to Authenticate. +// Check the length with: +// len(mockedAuthenticator.AuthenticateCalls()) +func (mock *AuthenticatorMock) AuthenticateCalls() []struct { + Ctxt echo.Context +} { + var calls []struct { + Ctxt echo.Context + } + mock.lockAuthenticate.RLock() + calls = mock.calls.Authenticate + mock.lockAuthenticate.RUnlock() + return calls +} + +// FromSchema calls FromSchemaFunc. +func (mock *AuthenticatorMock) FromSchema(scheme *openapi3.SecurityScheme) (rest.Authenticator, error) { + if mock.FromSchemaFunc == nil { + panic("AuthenticatorMock.FromSchemaFunc: method is nil but Authenticator.FromSchema was just called") + } + callInfo := struct { + Scheme *openapi3.SecurityScheme + }{ + Scheme: scheme, + } + mock.lockFromSchema.Lock() + mock.calls.FromSchema = append(mock.calls.FromSchema, callInfo) + mock.lockFromSchema.Unlock() + return mock.FromSchemaFunc(scheme) +} + +// FromSchemaCalls gets all the calls that were made to FromSchema. +// Check the length with: +// len(mockedAuthenticator.FromSchemaCalls()) +func (mock *AuthenticatorMock) FromSchemaCalls() []struct { + Scheme *openapi3.SecurityScheme +} { + var calls []struct { + Scheme *openapi3.SecurityScheme + } + mock.lockFromSchema.RLock() + calls = mock.calls.FromSchema + mock.lockFromSchema.RUnlock() + return calls +} diff --git a/controllers/rest/security.go b/controllers/rest/security.go new file mode 100644 index 00000000..ebb67381 --- /dev/null +++ b/controllers/rest/security.go @@ -0,0 +1,112 @@ +package rest + +import ( + "encoding/json" + "fmt" + "github.com/getkin/kin-openapi/openapi3" + "github.com/labstack/echo/v4" + "github.com/labstack/gommon/log" + "github.com/wepala/weos/model" + "github.com/wepala/weos/projections" + "net/http" +) + +//Authenticator interface that must be implemented so that a request can be authenticated +type Authenticator interface { + Authenticate(ctxt echo.Context) (bool, error) + FromSchema(scheme *openapi3.SecurityScheme) (Authenticator, error) +} + +//SecurityConfiguration mange the security configuration for the API +type SecurityConfiguration struct { + schemas []*openapi3.SchemaRef + defaultConfig []map[string][]string + Authenticators map[string]Authenticator +} + +func (s *SecurityConfiguration) FromSchema(schemas map[string]*openapi3.SecuritySchemeRef) (*SecurityConfiguration, error) { + var err error + //configure the authenticators based on the schemas + s.Authenticators = make(map[string]Authenticator) + for name, schema := range schemas { + if schema.Value != nil { + switch schema.Value.Type { + case "openIdConnect": + s.Authenticators[name], err = new(OpenIDConnect).FromSchema(schema.Value) + default: + err = fmt.Errorf("unsupported security scheme '%s'", name) + return s, err + } + } + } + return s, err +} + +func (s *SecurityConfiguration) SetDefaultSecurity(config []map[string][]string) { + s.defaultConfig = config +} + +func (s *SecurityConfiguration) Middleware(api Container, projection projections.Projection, commandDispatcher model.CommandDispatcher, eventSource model.EventRepository, entityFactory model.EntityFactory, path *openapi3.PathItem, operation *openapi3.Operation) echo.MiddlewareFunc { + //check that the schemes exist + var authenticators []Authenticator + logger, _ := api.GetLog("Default") + if logger == nil { + logger = log.New("log") + } + securitySchemes := api.GetConfig().Security + if operation.Security != nil { + if len(*operation.Security) > 0 { + securitySchemes = append(securitySchemes, *operation.Security...) + } else { //if an empty array is set for the security scheme then no security scheme should be set + securitySchemes = make(openapi3.SecurityRequirements, 0) + } + } + + for _, scheme := range securitySchemes { + for name, _ := range scheme { + allAuthenticators := api.GetSecurityConfiguration().Authenticators + if authenticator, ok := allAuthenticators[name]; ok { + authenticators = append(authenticators, authenticator) + } else { + logger.Errorf("security scheme '%s' was not configured in components > security schemes", name) + } + } + + } + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctxt echo.Context) error { + //loop through the authenticators and go to the next middleware when one authenticates otherwise return 403 + for _, authenticator := range authenticators { + var success bool + var err error + if success, err = authenticator.Authenticate(ctxt); success { + return next(ctxt) + } else { + ctxt.Logger().Debugf("error authenticating '%s'", err) + } + } + return ctxt.NoContent(http.StatusForbidden) + } + } +} + +//Authenticators + +type OpenIDConnect struct { + connectURL string +} + +func (o OpenIDConnect) Authenticate(ctxt echo.Context) (bool, error) { + //TODO implement me + panic("implement me") +} + +func (o OpenIDConnect) FromSchema(scheme *openapi3.SecurityScheme) (Authenticator, error) { + var err error + if tinterface, ok := scheme.Extensions[OpenIDConnectUrlExtension]; ok { + if rawURL, ok := tinterface.(json.RawMessage); ok { + err = json.Unmarshal(rawURL, &o.connectURL) + } + } + return o, err +} diff --git a/controllers/rest/security_test.go b/controllers/rest/security_test.go new file mode 100644 index 00000000..0edf2054 --- /dev/null +++ b/controllers/rest/security_test.go @@ -0,0 +1,107 @@ +package rest_test + +import ( + "github.com/getkin/kin-openapi/openapi3" + "github.com/labstack/echo/v4" + "github.com/wepala/weos/controllers/rest" + "github.com/wepala/weos/model" + "net/http" + "net/http/httptest" + "testing" +) + +func TestSecurityConfiguration_FromSchema(t *testing.T) { + t.Run("set open id authenticator", func(t *testing.T) { + swagger, err := LoadConfig(t, "fixtures/blog-security.yaml") + if err != nil { + t.Fatalf("unexpected error loading config '%s'", err) + } + config, err := new(rest.SecurityConfiguration).FromSchema(swagger.Components.SecuritySchemes) + if err != nil { + t.Fatalf("unexpected error setting up security configuration '%s'", err) + } + if len(config.Authenticators) != 1 { + t.Errorf("expected %d authenticators to be setup, got %d", 1, len(config.Authenticators)) + } + }) + t.Run("set unrecognized authenticator", func(t *testing.T) { + swagger, err := LoadConfig(t, "fixtures/blog-security-invalid.yaml") + if err != nil { + t.Fatalf("unexpected error loading config '%s'", err) + } + config, err := new(rest.SecurityConfiguration).FromSchema(swagger.Components.SecuritySchemes) + if err == nil { + t.Error("unexpected error for invalid securityScheme type") + } + if len(config.Authenticators) > 0 { + t.Errorf("expected %d authenticators to be setup, got %d", 0, len(config.Authenticators)) + } + }) +} + +func TestSecurityConfiguration_Middleware(t *testing.T) { + t.Run("valid authenticator", func(t *testing.T) { + swagger, err := LoadConfig(t, "fixtures/blog-security.yaml") + if err != nil { + t.Fatalf("unexpected error loading config '%s'", err) + } + config, err := new(rest.SecurityConfiguration).FromSchema(swagger.Components.SecuritySchemes) + if err != nil { + t.Fatalf("unexpected error setting up security configuration '%s'", err) + } + //set mock authenticator so we can check that is was set and called + mockAuthenticator := &AuthenticatorMock{AuthenticateFunc: func(ctxt echo.Context) (bool, error) { + return true, nil + }} + config.Authenticators["Auth0"] = mockAuthenticator + //find path with no security scheme set + path := swagger.Paths.Find("/blogs") + container := &ContainerMock{ + GetSecurityConfigurationFunc: func() *rest.SecurityConfiguration { + return config + }, + GetLogFunc: func(name string) (model.Log, error) { + return &LogMock{ + DebugfFunc: func(format string, args ...interface{}) { + + }, + ErrorfFunc: func(format string, args ...interface{}) { + + }, + }, nil + }, + GetConfigFunc: func() *openapi3.Swagger { + return swagger + }, + } + mw := config.Middleware(container, &ProjectionMock{}, &CommandDispatcherMock{}, &EventRepositoryMock{}, &EntityFactoryMock{}, path, path.Post) + nextMiddlewareHit := false + handler := mw(func(context echo.Context) error { + nextMiddlewareHit = true + return nil + }) + + e := echo.New() + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/blogs", nil) + e.POST("/blogs", handler) + e.ServeHTTP(resp, req) + + if len(mockAuthenticator.AuthenticateCalls()) != 1 { + t.Errorf("expected the mock authenticator to be called %d time, called %d times", 1, len(mockAuthenticator.AuthenticateCalls())) + } + + if !nextMiddlewareHit { + t.Errorf("expected the next middleware to be hit") + } + }) +} + +//func TestSecurityConfiguration_SetDefaultSecurity(t *testing.T) { +// swagger, err := LoadConfig(t, "fixtures/blog-security.yaml") +// if err != nil { +// t.Fatalf("unexpected error loading config '%s'", err) +// } +// config, err := new(rest.SecurityConfiguration).FromSchema(swagger.Components.SecuritySchemes) +// config.SetDefaultSecurity(swagger.Security.With()) +//} From 426ce0cd9cd651944a7e8ca5c07addf44fa0bddf Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Mon, 20 Jun 2022 13:36:12 -0400 Subject: [PATCH 05/18] feature: WEOS-1344 Create Security Middleware for managing security configuration * Changed Authenticators to Validators * Switched to using weauth configuration --- controllers/rest/fixtures/blog-security.yaml | 17 ++++- controllers/rest/rest_mocks_test.go | 32 ++++----- controllers/rest/security.go | 58 +++++---------- controllers/rest/security_authenticators.go | 70 +++++++++++++++++++ .../rest/security_authenticators_test.go | 57 +++++++++++++++ controllers/rest/security_test.go | 10 +-- go.mod | 2 +- go.sum | 2 + 8 files changed, 183 insertions(+), 65 deletions(-) create mode 100644 controllers/rest/security_authenticators.go create mode 100644 controllers/rest/security_authenticators_test.go diff --git a/controllers/rest/fixtures/blog-security.yaml b/controllers/rest/fixtures/blog-security.yaml index 17eb7685..856fd592 100644 --- a/controllers/rest/fixtures/blog-security.yaml +++ b/controllers/rest/fixtures/blog-security.yaml @@ -31,8 +31,21 @@ components: securitySchemes: Auth0: type: openIdConnect - openIdConnectUrl: https://dev-bhjqt6zc.us.auth0.com/.well-known/openid-configuration + openIdConnectUrl: https://weauth-dev.weos.sh/.well-known/openid-configuration x-skip-expiry-check: true + Auth02: + type: oauth2 + flows: + authorizationCode: + authorizationUrl: https://dev-bhjqt6zc.us.auth0.com/authorize + tokenUrl: https://dev-bhjqt6zc.us.auth0.com/oauth/token + scopes: + openid: Open Id + profile: Read profile data + offline_access: Access information without valid token + name: User name + email: User email address + schemas: Category: type: object @@ -319,6 +332,8 @@ paths: format: double summary: Get Blog by id operationId: Get Blog + security: + - Auth02: ["email"] responses: 200: description: Blog details without any supporting collections diff --git a/controllers/rest/rest_mocks_test.go b/controllers/rest/rest_mocks_test.go index 3eec4d1c..e91b21c5 100644 --- a/controllers/rest/rest_mocks_test.go +++ b/controllers/rest/rest_mocks_test.go @@ -1460,38 +1460,38 @@ func (mock *ContainerMock) RegisterSecurityConfigurationCalls() []struct { return calls } -// Ensure, that AuthenticatorMock does implement rest.Authenticator. +// Ensure, that AuthenticatorMock does implement rest.Validator. // If this is not the case, regenerate this file with moq. -var _ rest.Authenticator = &AuthenticatorMock{} +var _ rest.Validator = &AuthenticatorMock{} -// AuthenticatorMock is a mock implementation of rest.Authenticator. +// AuthenticatorMock is a mock implementation of rest.Validator. // // func TestSomethingThatUsesAuthenticator(t *testing.T) { // -// // make and configure a mocked rest.Authenticator +// // make and configure a mocked rest.Validator // mockedAuthenticator := &AuthenticatorMock{ // AuthenticateFunc: func(ctxt echo.Context) (bool, error) { -// panic("mock out the Authenticate method") +// panic("mock out the Validate method") // }, -// FromSchemaFunc: func(scheme *openapi3.SecurityScheme) (rest.Authenticator, error) { +// FromSchemaFunc: func(scheme *openapi3.SecurityScheme) (rest.Validator, error) { // panic("mock out the FromSchema method") // }, // } // -// // use mockedAuthenticator in code that requires rest.Authenticator +// // use mockedAuthenticator in code that requires rest.Validator // // and then make assertions. // // } type AuthenticatorMock struct { - // AuthenticateFunc mocks the Authenticate method. + // AuthenticateFunc mocks the Validate method. AuthenticateFunc func(ctxt echo.Context) (bool, error) // FromSchemaFunc mocks the FromSchema method. - FromSchemaFunc func(scheme *openapi3.SecurityScheme) (rest.Authenticator, error) + FromSchemaFunc func(scheme *openapi3.SecurityScheme) (rest.Validator, error) // calls tracks calls to the methods. calls struct { - // Authenticate holds details about calls to the Authenticate method. + // Validate holds details about calls to the Validate method. Authenticate []struct { // Ctxt is the ctxt argument value. Ctxt echo.Context @@ -1506,10 +1506,10 @@ type AuthenticatorMock struct { lockFromSchema sync.RWMutex } -// Authenticate calls AuthenticateFunc. -func (mock *AuthenticatorMock) Authenticate(ctxt echo.Context) (bool, error) { +// Validate calls AuthenticateFunc. +func (mock *AuthenticatorMock) Validate(ctxt echo.Context) (bool, error) { if mock.AuthenticateFunc == nil { - panic("AuthenticatorMock.AuthenticateFunc: method is nil but Authenticator.Authenticate was just called") + panic("AuthenticatorMock.AuthenticateFunc: method is nil but Validator.Validate was just called") } callInfo := struct { Ctxt echo.Context @@ -1522,7 +1522,7 @@ func (mock *AuthenticatorMock) Authenticate(ctxt echo.Context) (bool, error) { return mock.AuthenticateFunc(ctxt) } -// AuthenticateCalls gets all the calls that were made to Authenticate. +// AuthenticateCalls gets all the calls that were made to Validate. // Check the length with: // len(mockedAuthenticator.AuthenticateCalls()) func (mock *AuthenticatorMock) AuthenticateCalls() []struct { @@ -1538,9 +1538,9 @@ func (mock *AuthenticatorMock) AuthenticateCalls() []struct { } // FromSchema calls FromSchemaFunc. -func (mock *AuthenticatorMock) FromSchema(scheme *openapi3.SecurityScheme) (rest.Authenticator, error) { +func (mock *AuthenticatorMock) FromSchema(scheme *openapi3.SecurityScheme) (rest.Validator, error) { if mock.FromSchemaFunc == nil { - panic("AuthenticatorMock.FromSchemaFunc: method is nil but Authenticator.FromSchema was just called") + panic("AuthenticatorMock.FromSchemaFunc: method is nil but Validator.FromSchema was just called") } callInfo := struct { Scheme *openapi3.SecurityScheme diff --git a/controllers/rest/security.go b/controllers/rest/security.go index ebb67381..3e5e23ac 100644 --- a/controllers/rest/security.go +++ b/controllers/rest/security.go @@ -1,7 +1,6 @@ package rest import ( - "encoding/json" "fmt" "github.com/getkin/kin-openapi/openapi3" "github.com/labstack/echo/v4" @@ -11,28 +10,28 @@ import ( "net/http" ) -//Authenticator interface that must be implemented so that a request can be authenticated -type Authenticator interface { - Authenticate(ctxt echo.Context) (bool, error) - FromSchema(scheme *openapi3.SecurityScheme) (Authenticator, error) +//Validator interface that must be implemented so that a request can be authenticated +type Validator interface { + Validate(ctxt echo.Context) (bool, error) + FromSchema(scheme *openapi3.SecurityScheme) (Validator, error) } //SecurityConfiguration mange the security configuration for the API type SecurityConfiguration struct { - schemas []*openapi3.SchemaRef - defaultConfig []map[string][]string - Authenticators map[string]Authenticator + schemas []*openapi3.SchemaRef + defaultConfig []map[string][]string + Validators map[string]Validator } func (s *SecurityConfiguration) FromSchema(schemas map[string]*openapi3.SecuritySchemeRef) (*SecurityConfiguration, error) { var err error //configure the authenticators based on the schemas - s.Authenticators = make(map[string]Authenticator) + s.Validators = make(map[string]Validator) for name, schema := range schemas { if schema.Value != nil { switch schema.Value.Type { case "openIdConnect": - s.Authenticators[name], err = new(OpenIDConnect).FromSchema(schema.Value) + s.Validators[name], err = new(OpenIDConnect).FromSchema(schema.Value) default: err = fmt.Errorf("unsupported security scheme '%s'", name) return s, err @@ -42,13 +41,9 @@ func (s *SecurityConfiguration) FromSchema(schemas map[string]*openapi3.Security return s, err } -func (s *SecurityConfiguration) SetDefaultSecurity(config []map[string][]string) { - s.defaultConfig = config -} - func (s *SecurityConfiguration) Middleware(api Container, projection projections.Projection, commandDispatcher model.CommandDispatcher, eventSource model.EventRepository, entityFactory model.EntityFactory, path *openapi3.PathItem, operation *openapi3.Operation) echo.MiddlewareFunc { //check that the schemes exist - var authenticators []Authenticator + var validators []Validator logger, _ := api.GetLog("Default") if logger == nil { logger = log.New("log") @@ -64,9 +59,9 @@ func (s *SecurityConfiguration) Middleware(api Container, projection projections for _, scheme := range securitySchemes { for name, _ := range scheme { - allAuthenticators := api.GetSecurityConfiguration().Authenticators - if authenticator, ok := allAuthenticators[name]; ok { - authenticators = append(authenticators, authenticator) + allValidators := api.GetSecurityConfiguration().Validators + if validator, ok := allValidators[name]; ok { + validators = append(validators, validator) } else { logger.Errorf("security scheme '%s' was not configured in components > security schemes", name) } @@ -75,11 +70,11 @@ func (s *SecurityConfiguration) Middleware(api Container, projection projections } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(ctxt echo.Context) error { - //loop through the authenticators and go to the next middleware when one authenticates otherwise return 403 - for _, authenticator := range authenticators { + //loop through the validators and go to the next middleware when one authenticates otherwise return 403 + for _, validator := range validators { var success bool var err error - if success, err = authenticator.Authenticate(ctxt); success { + if success, err = validator.Validate(ctxt); success { return next(ctxt) } else { ctxt.Logger().Debugf("error authenticating '%s'", err) @@ -89,24 +84,3 @@ func (s *SecurityConfiguration) Middleware(api Container, projection projections } } } - -//Authenticators - -type OpenIDConnect struct { - connectURL string -} - -func (o OpenIDConnect) Authenticate(ctxt echo.Context) (bool, error) { - //TODO implement me - panic("implement me") -} - -func (o OpenIDConnect) FromSchema(scheme *openapi3.SecurityScheme) (Authenticator, error) { - var err error - if tinterface, ok := scheme.Extensions[OpenIDConnectUrlExtension]; ok { - if rawURL, ok := tinterface.(json.RawMessage); ok { - err = json.Unmarshal(rawURL, &o.connectURL) - } - } - return o, err -} diff --git a/controllers/rest/security_authenticators.go b/controllers/rest/security_authenticators.go new file mode 100644 index 00000000..3b3c7c43 --- /dev/null +++ b/controllers/rest/security_authenticators.go @@ -0,0 +1,70 @@ +package rest + +import ( + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "github.com/getkin/kin-openapi/openapi3" + "github.com/golang-jwt/jwt/v4" + "github.com/labstack/echo/v4" + "strings" +) + +//OpenIDConnect authorizer for OpenID +type OpenIDConnect struct { + connectURL string +} + +func (o OpenIDConnect) Validate(ctxt echo.Context) (bool, error) { + //TODO implement me + panic("implement me") +} + +func (o OpenIDConnect) FromSchema(scheme *openapi3.SecurityScheme) (Validator, error) { + var err error + if tinterface, ok := scheme.Extensions[OpenIDConnectUrlExtension]; ok { + if rawURL, ok := tinterface.(json.RawMessage); ok { + err = json.Unmarshal(rawURL, &o.connectURL) + } + } + return o, err +} + +type OAuth2 struct { + connectURL string + Flows *openapi3.OAuthFlows + clientSecret string +} + +func (o OAuth2) Validate(ctxt echo.Context) (bool, error) { + authorizationHeader := ctxt.Request().Header.Get("Authorization") + tokenString := strings.Replace(authorizationHeader, "Bearer ", "", -1) + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + //TODO figure out good way to load certificate here + cert := `` + + block, _ := pem.Decode([]byte(cert)) + if block == nil { + return nil, fmt.Errorf("unable to decode cert") + } + + pub, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse public key '%s'", err) + } + + return pub.PublicKey, nil + }) + return token.Valid, err +} + +func (o OAuth2) FromSchema(scheme *openapi3.SecurityScheme) (Validator, error) { + var err error + o.Flows = scheme.Flows + return o, err +} diff --git a/controllers/rest/security_authenticators_test.go b/controllers/rest/security_authenticators_test.go new file mode 100644 index 00000000..f9fe9903 --- /dev/null +++ b/controllers/rest/security_authenticators_test.go @@ -0,0 +1,57 @@ +package rest_test + +import ( + "github.com/labstack/echo/v4" + "github.com/wepala/weos/controllers/rest" + "net/http/httptest" + "os" + "testing" +) + +func TestOAuth2_FromSchema(t *testing.T) { + t.Run("initialize oauth2 authenticator", func(t *testing.T) { + swagger, err := LoadConfig(t, "fixtures/blog-security.yaml") + if err != nil { + t.Fatalf("unexpected error loading config '%s'", err) + } + authenticator, err := new(rest.OAuth2).FromSchema(swagger.Components.SecuritySchemes["Auth02"].Value) + if tauthenticator, ok := authenticator.(rest.OAuth2); ok { + if tauthenticator.Flows.AuthorizationCode.AuthorizationURL != swagger.Components.SecuritySchemes["Auth02"].Value.Flows.AuthorizationCode.AuthorizationURL { + t.Errorf("expected authorization url to be '%s', got '%s'", swagger.Components.SecuritySchemes["Auth02"].Value.Flows.AuthorizationCode.AuthorizationURL, tauthenticator.Flows.AuthorizationCode.AuthorizationURL) + } + } else { + t.Fatalf("expected OAuth2 authenticator") + } + }) +} + +func TestOAuth2_Authenticate(t *testing.T) { + t.Run("authenticate valid token", func(t *testing.T) { + t.Skipf("need to figure out way to load certificates see https://wepala.atlassian.net/browse/WEOS-1520") + swagger, err := LoadConfig(t, "fixtures/blog-security.yaml") + if err != nil { + t.Fatalf("unexpected error loading config '%s'", err) + } + + rawRequest := httptest.NewRequest("POST", "/blogs/1234", nil) + token, ok := os.LookupEnv("OAUTH_TEST_KEY") + if !ok { + t.Fatal("test requires token set in 'OAUTH_TEST_KEY' environment variable") + } + rawRequest.Header.Add("Authorization", "Bearer "+token) + rw := httptest.NewRecorder() + + e := echo.New() + ctxt := e.NewContext(rawRequest, rw) + + authenticator, _ := new(rest.OAuth2).FromSchema(swagger.Components.SecuritySchemes["Auth02"].Value) + result, err := authenticator.Validate(ctxt) + if err != nil { + t.Fatalf("error authenticating '%s'", err) + } + + if !result { + t.Error("authentication failed") + } + }) +} diff --git a/controllers/rest/security_test.go b/controllers/rest/security_test.go index 0edf2054..8ef9ecaf 100644 --- a/controllers/rest/security_test.go +++ b/controllers/rest/security_test.go @@ -20,8 +20,8 @@ func TestSecurityConfiguration_FromSchema(t *testing.T) { if err != nil { t.Fatalf("unexpected error setting up security configuration '%s'", err) } - if len(config.Authenticators) != 1 { - t.Errorf("expected %d authenticators to be setup, got %d", 1, len(config.Authenticators)) + if len(config.Validators) != 1 { + t.Errorf("expected %d authenticators to be setup, got %d", 1, len(config.Validators)) } }) t.Run("set unrecognized authenticator", func(t *testing.T) { @@ -33,8 +33,8 @@ func TestSecurityConfiguration_FromSchema(t *testing.T) { if err == nil { t.Error("unexpected error for invalid securityScheme type") } - if len(config.Authenticators) > 0 { - t.Errorf("expected %d authenticators to be setup, got %d", 0, len(config.Authenticators)) + if len(config.Validators) > 0 { + t.Errorf("expected %d authenticators to be setup, got %d", 0, len(config.Validators)) } }) } @@ -53,7 +53,7 @@ func TestSecurityConfiguration_Middleware(t *testing.T) { mockAuthenticator := &AuthenticatorMock{AuthenticateFunc: func(ctxt echo.Context) (bool, error) { return true, nil }} - config.Authenticators["Auth0"] = mockAuthenticator + config.Validators["Auth0"] = mockAuthenticator //find path with no security scheme set path := swagger.Paths.Find("/blogs") container := &ContainerMock{ diff --git a/go.mod b/go.mod index 3aeed3de..3ebdc899 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/coreos/go-oidc/v3 v3.1.0 github.com/cucumber/godog v0.12.2 github.com/getkin/kin-openapi v0.15.0 + github.com/golang-jwt/jwt/v4 v4.4.1 github.com/google/uuid v1.3.0 github.com/kr/text v0.2.0 // indirect github.com/labstack/echo/v4 v4.5.0 @@ -20,7 +21,6 @@ require ( github.com/testcontainers/testcontainers-go v0.12.0 go.uber.org/zap v1.13.0 golang.org/x/net v0.0.0-20211108170745-6635138e15ea - golang.org/x/text v0.3.7 gorm.io/datatypes v1.0.5 gorm.io/driver/clickhouse v0.2.2 gorm.io/driver/mysql v1.2.2 diff --git a/go.sum b/go.sum index e3cec5b7..e9141dbb 100644 --- a/go.sum +++ b/go.sum @@ -318,6 +318,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.4.1 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ= +github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= From 5d71d71eac343e4f5fae12a440de9b82eb325939 Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Mon, 20 Jun 2022 18:20:51 -0400 Subject: [PATCH 06/18] feature: WEOS-1518 Map JWT parts to user id, role, WEOS-1344 Added support for multiple security schemes * Removed manually setting up OpenIDMiddleware * Updated security initializer to setup the security config on the container and use the middleware on the security config * Updated validator interface to return user id and role * Added new helper function to get open id config --- controllers/rest/api.go | 1 - controllers/rest/fixtures/blog-security.yaml | 23 +-- controllers/rest/fixtures/jwt/demo.jwt | 1 + controllers/rest/global_initializers.go | 45 +++--- controllers/rest/global_initializers_test.go | 28 +--- controllers/rest/interfaces.go | 2 +- controllers/rest/openapi_extensions.go | 3 + controllers/rest/rest_mocks_test.go | 110 ++++++------- controllers/rest/security.go | 21 ++- controllers/rest/security_authenticators.go | 70 -------- .../rest/security_authenticators_test.go | 57 ------- controllers/rest/security_test.go | 12 +- controllers/rest/security_validators.go | 152 ++++++++++++++++++ controllers/rest/security_validators_test.go | 127 +++++++++++++++ controllers/rest/utils.go | 20 +++ features/security-schemes.feature | 6 +- 16 files changed, 413 insertions(+), 265 deletions(-) create mode 100644 controllers/rest/fixtures/jwt/demo.jwt delete mode 100644 controllers/rest/security_authenticators.go delete mode 100644 controllers/rest/security_authenticators_test.go create mode 100644 controllers/rest/security_validators.go create mode 100644 controllers/rest/security_validators_test.go diff --git a/controllers/rest/api.go b/controllers/rest/api.go index 0785cebd..f3340f3f 100644 --- a/controllers/rest/api.go +++ b/controllers/rest/api.go @@ -432,7 +432,6 @@ func (p *RESTAPI) Initialize(ctxt context.Context) error { //register standard middleware p.RegisterMiddleware("Context", Context) - p.RegisterMiddleware("OpenIDMiddleware", OpenIDMiddleware) p.RegisterMiddleware("CreateMiddleware", CreateMiddleware) p.RegisterMiddleware("CreateBatchMiddleware", CreateBatchMiddleware) p.RegisterMiddleware("UpdateMiddleware", UpdateMiddleware) diff --git a/controllers/rest/fixtures/blog-security.yaml b/controllers/rest/fixtures/blog-security.yaml index 856fd592..be27c2b6 100644 --- a/controllers/rest/fixtures/blog-security.yaml +++ b/controllers/rest/fixtures/blog-security.yaml @@ -29,22 +29,16 @@ x-weos-config: - ZapLogger components: securitySchemes: - Auth0: + WeAuth: type: openIdConnect openIdConnectUrl: https://weauth-dev.weos.sh/.well-known/openid-configuration + x-jwt-map: + user: sub + role: tag + Auth0: + type: openIdConnect + openIdConnectUrl: https://dev-bhjqt6zc.us.auth0.com/.well-known/openid-configuration x-skip-expiry-check: true - Auth02: - type: oauth2 - flows: - authorizationCode: - authorizationUrl: https://dev-bhjqt6zc.us.auth0.com/authorize - tokenUrl: https://dev-bhjqt6zc.us.auth0.com/oauth/token - scopes: - openid: Open Id - profile: Read profile data - offline_access: Access information without valid token - name: User name - email: User email address schemas: Category: @@ -149,7 +143,6 @@ paths: x-controller: HealthCheck x-middleware: - Recover - - OpenIDMiddleware responses: 200: description: Health Response @@ -195,8 +188,6 @@ paths: name: Authorization schema: type: string - x-middleware: - - OpenIDMiddleware requestBody: description: Blog info that is submitted required: true diff --git a/controllers/rest/fixtures/jwt/demo.jwt b/controllers/rest/fixtures/jwt/demo.jwt new file mode 100644 index 00000000..9f0a2fbc --- /dev/null +++ b/controllers/rest/fixtures/jwt/demo.jwt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsImtpZCI6ImRlbW8iLCJ0eXAiOiJKV1QifQ.eyJvd25lciI6ImRlbW8iLCJuYW1lIjoiZGVtbyIsImNyZWF0ZWRUaW1lIjoiMjAyMi0wNi0yMFQwODo0MzozNC0wNDowMCIsInVwZGF0ZWRUaW1lIjoiIiwiaWQiOiI5M2I0NmRjZC1iYWYxLTQxZDItOTljYy00NDlkMDJiZmUxOTUiLCJ0eXBlIjoibm9ybWFsLXVzZXIiLCJwYXNzd29yZCI6IiIsInBhc3N3b3JkU2FsdCI6IiIsImRpc3BsYXlOYW1lIjoiRGVtbyIsImZpcnN0TmFtZSI6IiIsImxhc3ROYW1lIjoiIiwiYXZhdGFyIjoiaHR0cHM6Ly9jYXNiaW4ub3JnL2ltZy9jYXNiaW4uc3ZnIiwicGVybWFuZW50QXZhdGFyIjoiIiwiZW1haWwiOiJkZW1vQHdlcGFsYS5jb20iLCJlbWFpbFZlcmlmaWVkIjpmYWxzZSwicGhvbmUiOiI0NjE0ODA0MzU1MSIsImxvY2F0aW9uIjoiIiwiYWRkcmVzcyI6W10sImFmZmlsaWF0aW9uIjoiRXhhbXBsZSBJbmMuIiwidGl0bGUiOiIiLCJpZENhcmRUeXBlIjoiIiwiaWRDYXJkIjoiIiwiaG9tZXBhZ2UiOiIiLCJiaW8iOiIiLCJyZWdpb24iOiIiLCJsYW5ndWFnZSI6IiIsImdlbmRlciI6IiIsImJpcnRoZGF5IjoiIiwiZWR1Y2F0aW9uIjoiIiwic2NvcmUiOjAsImthcm1hIjowLCJyYW5raW5nIjoyLCJpc0RlZmF1bHRBdmF0YXIiOmZhbHNlLCJpc09ubGluZSI6ZmFsc2UsImlzQWRtaW4iOmZhbHNlLCJpc0dsb2JhbEFkbWluIjpmYWxzZSwiaXNGb3JiaWRkZW4iOmZhbHNlLCJpc0RlbGV0ZWQiOmZhbHNlLCJzaWdudXBBcHBsaWNhdGlvbiI6ImRlbW8iLCJoYXNoIjoiIiwicHJlSGFzaCI6IiIsImNyZWF0ZWRJcCI6IiIsImxhc3RTaWduaW5UaW1lIjoiIiwibGFzdFNpZ25pbklwIjoiIiwiZ2l0aHViIjoiIiwiZ29vZ2xlIjoiIiwicXEiOiIiLCJ3ZWNoYXQiOiIiLCJ1bmlvbklkIjoiIiwiZmFjZWJvb2siOiIiLCJkaW5ndGFsayI6IiIsIndlaWJvIjoiIiwiZ2l0ZWUiOiIiLCJsaW5rZWRpbiI6IiIsIndlY29tIjoiIiwibGFyayI6IiIsImdpdGxhYiI6IiIsImFkZnMiOiIiLCJiYWlkdSI6IiIsImFsaXBheSI6IiIsImNhc2Rvb3IiOiIiLCJpbmZvZmxvdyI6IiIsImFwcGxlIjoiIiwiYXp1cmVhZCI6IiIsInNsYWNrIjoiIiwic3RlYW0iOiIiLCJiaWxpYmlsaSI6IiIsIm9rdGEiOiIiLCJkb3V5aW4iOiIiLCJjdXN0b20iOiIiLCJsZGFwIjoiIiwicHJvcGVydGllcyI6e30sInRhZyI6InN0YWZmIiwic2NvcGUiOiJyZWFkIiwiaXNzIjoiaHR0cHM6Ly93ZWF1dGgtZGV2Lndlb3Muc2giLCJzdWIiOiI5M2I0NmRjZC1iYWYxLTQxZDItOTljYy00NDlkMDJiZmUxOTUiLCJhdWQiOlsiYTZiMGJiMjM3OWFhZWYyYTk4ZDQiXSwiZXhwIjoxNzE2MjE3MTEyLCJuYmYiOjE2NTU3MzcxMTIsImlhdCI6MTY1NTczNzExMn0.i2YPYF1JIQmwY7M31AjcLJP1gHLtfL0mdXwFkRuyLqhYHSMjE7iDSR2CO0o-Luvx9jt5BJoto9myMVadgjhWweWzhuMPy7I5ZPQA-m4_dpN_tlztKXoPLFEa8aUeiR50jQmWA3s2qAXc6b94D265giaIXgAXvp6EzM3jXxTAz1otgI6-6J_I9ZeG0q1YZugqgSNlY06Wvuv14A6DO1UuFIhrB_EHW-b0GsulfRXUUY6qmNqmC43PGXdQfa_RGSVP59D5o3SutgUm_jQcH1qNsH2H8GVyXKGsaWvK35Cdn8-_q4giG4RgSE0dth0U3cvqllYWQOxzSd3KDiWtg939MDyFaT0V_FGxzm21l6-oj3Z6MvFwrtLrWNopc8I8U2iWd0FYTa9cc0GpZPTOOOuFipmSmJg-bUHXBiQC_FcZXSBp3S2Ze4sFdSdykyq9Yfj4HdGzaR-pkeuNtOY9yDegL0KxP1L2jzoFxmXR9YKhi4dQv382yNq-U6MmRzgOU-1gkbIVW8SpF1ktIkAYihw-TifTQIU8jihsMuIKLybC8LAvPmI08OVLQqZe3WffO8k4Q8w8yHmJ0O-jLcp4sjiQCYivLVmRKTxzqQ45v5RQ8dT_uLKkdPH2ribhxDWed0qYHdaiYm-rl5Xa2Qo1-GZ2TK61qejixCQU1JVC4Vp5e3g \ No newline at end of file diff --git a/controllers/rest/global_initializers.go b/controllers/rest/global_initializers.go index 967f0440..9c61d73a 100644 --- a/controllers/rest/global_initializers.go +++ b/controllers/rest/global_initializers.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "github.com/getkin/kin-openapi/openapi3" + "github.com/labstack/gommon/log" weosContext "github.com/wepala/weos/context" "github.com/wepala/weos/model" "github.com/wepala/weos/projections" @@ -15,35 +16,31 @@ import ( //Security adds authorization middleware to the initialize context func Security(ctxt context.Context, tapi Container, swagger *openapi3.Swagger) (context.Context, error) { - api := tapi.(*RESTAPI) - middlewares := GetOperationMiddlewares(ctxt) - found := false - - for _, security := range swagger.Security { - for key, _ := range security { - if swagger.Components.SecuritySchemes != nil && swagger.Components.SecuritySchemes[key] != nil { - //checks if the security scheme has type openIdConnect - if swagger.Components.SecuritySchemes[key].Value.Type == "openIdConnect" { - found = true - break + if swagger.Components.SecuritySchemes != nil { + middlewares := GetOperationMiddlewares(ctxt) + logger, err := tapi.GetLog("Default") + if err != nil { + logger = log.New("weos") + } + config, err := new(SecurityConfiguration).FromSchema(swagger.Components.SecuritySchemes) + if err != nil { + logger.Debugf("error loading security schemes '%s'", err) + return ctxt, err + } + //set config to container + tapi.RegisterSecurityConfiguration(config) + //check that all the security references are valid + for _, security := range swagger.Security { + for k, _ := range security { + if _, ok := config.Validators[k]; !ok { + return ctxt, fmt.Errorf("unable to find security configuration '%s'", k) } - } } - - } - if found { - if middleware, _ := api.GetMiddleware("OpenIDMiddleware"); middleware != nil { - middlewares = append(middlewares, middleware) - } + middlewares = append(middlewares, config.Middleware) ctxt = context.WithValue(ctxt, weosContext.MIDDLEWARES, middlewares) - } else { - if swagger.Components.SecuritySchemes != nil && swagger.Security != nil { - api.EchoInstance().Logger.Errorf("unexpected error: security defined does not match any security schemes") - return ctxt, fmt.Errorf("unexpected error: security defined does not match any security schemes") - } - } + return ctxt, nil } diff --git a/controllers/rest/global_initializers_test.go b/controllers/rest/global_initializers_test.go index 79cbb846..c23ea135 100644 --- a/controllers/rest/global_initializers_test.go +++ b/controllers/rest/global_initializers_test.go @@ -1,12 +1,8 @@ package rest_test import ( - "github.com/getkin/kin-openapi/openapi3" - "github.com/labstack/echo/v4" - weoscontext "github.com/wepala/weos/context" "github.com/wepala/weos/controllers/rest" "github.com/wepala/weos/model" - "github.com/wepala/weos/projections" "golang.org/x/net/context" "testing" ) @@ -16,20 +12,9 @@ func TestGlobalMiddlewareInitializer(t *testing.T) { if err != nil { t.Fatalf("unexpected error loading api '%s'", err) } - schemas := rest.CreateSchema(context.TODO(), api.EchoInstance(), api.Swagger) - baseCtxt := context.WithValue(context.TODO(), weoscontext.SCHEMA_BUILDERS, schemas) - middlewareCalled := false - api.RegisterMiddleware("OpenIDMiddleware", func(api rest.Container, projection projections.Projection, commandDispatcher model.CommandDispatcher, eventSource model.EventRepository, entityFactory model.EntityFactory, path *openapi3.PathItem, operation *openapi3.Operation) echo.MiddlewareFunc { - return func(handlerFunc echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - middlewareCalled = true - return nil - } - } - }) t.Run("auth middleware was added to context", func(t *testing.T) { - ctxt, err := rest.Security(baseCtxt, api, api.Swagger) + ctxt, err := rest.Security(context.TODO(), api, api.Swagger) if err != nil { t.Fatalf("unexpected error loading api '%s'", err) } @@ -37,17 +22,6 @@ func TestGlobalMiddlewareInitializer(t *testing.T) { if len(middlewares) != 1 { t.Fatalf("expected the middlewares in context to be %d, got %d", 1, len(middlewares)) } - for _, middleware := range middlewares { - err = middleware(api, nil, nil, nil, nil, nil, nil)(func(c echo.Context) error { - return nil - })(echo.New().AcquireContext()) - if err != nil { - t.Errorf("unexpected error running middleware '%s'", err) - } - } - if !middlewareCalled { - t.Errorf("expected middleware to be in context and called") - } }) } diff --git a/controllers/rest/interfaces.go b/controllers/rest/interfaces.go index 52e54291..fddf6879 100644 --- a/controllers/rest/interfaces.go +++ b/controllers/rest/interfaces.go @@ -1,4 +1,4 @@ -//go:generate moq -out rest_mocks_test.go -pkg rest_test . Container Authenticator +//go:generate moq -out rest_mocks_test.go -pkg rest_test . Container Validator package rest import ( diff --git a/controllers/rest/openapi_extensions.go b/controllers/rest/openapi_extensions.go index 3eaf1ac2..5fadcc26 100644 --- a/controllers/rest/openapi_extensions.go +++ b/controllers/rest/openapi_extensions.go @@ -41,6 +41,9 @@ const UniqueExtension = "x-unique" //OpenIDConnectUrlExtension set the open id connect url const OpenIDConnectUrlExtension = "openIdConnectUrl" +//JWTMapExtension map claims to user and role +const JWTMapExtension = "x-jwt-map" + //SkipExpiryCheckExtension set the expiry check const SkipExpiryCheckExtension = "x-skip-expiry-check" diff --git a/controllers/rest/rest_mocks_test.go b/controllers/rest/rest_mocks_test.go index e91b21c5..a85df3ef 100644 --- a/controllers/rest/rest_mocks_test.go +++ b/controllers/rest/rest_mocks_test.go @@ -1460,87 +1460,56 @@ func (mock *ContainerMock) RegisterSecurityConfigurationCalls() []struct { return calls } -// Ensure, that AuthenticatorMock does implement rest.Validator. +// Ensure, that ValidatorMock does implement rest.Validator. // If this is not the case, regenerate this file with moq. -var _ rest.Validator = &AuthenticatorMock{} +var _ rest.Validator = &ValidatorMock{} -// AuthenticatorMock is a mock implementation of rest.Validator. +// ValidatorMock is a mock implementation of rest.Validator. // -// func TestSomethingThatUsesAuthenticator(t *testing.T) { +// func TestSomethingThatUsesValidator(t *testing.T) { // // // make and configure a mocked rest.Validator -// mockedAuthenticator := &AuthenticatorMock{ -// AuthenticateFunc: func(ctxt echo.Context) (bool, error) { -// panic("mock out the Validate method") -// }, +// mockedValidator := &ValidatorMock{ // FromSchemaFunc: func(scheme *openapi3.SecurityScheme) (rest.Validator, error) { // panic("mock out the FromSchema method") // }, +// ValidateFunc: func(ctxt echo.Context) (bool, interface{}, string, string, error) { +// panic("mock out the Validate method") +// }, // } // -// // use mockedAuthenticator in code that requires rest.Validator +// // use mockedValidator in code that requires rest.Validator // // and then make assertions. // // } -type AuthenticatorMock struct { - // AuthenticateFunc mocks the Validate method. - AuthenticateFunc func(ctxt echo.Context) (bool, error) - +type ValidatorMock struct { // FromSchemaFunc mocks the FromSchema method. FromSchemaFunc func(scheme *openapi3.SecurityScheme) (rest.Validator, error) + // ValidateFunc mocks the Validate method. + ValidateFunc func(ctxt echo.Context) (bool, interface{}, string, string, error) + // calls tracks calls to the methods. calls struct { - // Validate holds details about calls to the Validate method. - Authenticate []struct { - // Ctxt is the ctxt argument value. - Ctxt echo.Context - } // FromSchema holds details about calls to the FromSchema method. FromSchema []struct { // Scheme is the scheme argument value. Scheme *openapi3.SecurityScheme } + // Validate holds details about calls to the Validate method. + Validate []struct { + // Ctxt is the ctxt argument value. + Ctxt echo.Context + } } - lockAuthenticate sync.RWMutex - lockFromSchema sync.RWMutex -} - -// Validate calls AuthenticateFunc. -func (mock *AuthenticatorMock) Validate(ctxt echo.Context) (bool, error) { - if mock.AuthenticateFunc == nil { - panic("AuthenticatorMock.AuthenticateFunc: method is nil but Validator.Validate was just called") - } - callInfo := struct { - Ctxt echo.Context - }{ - Ctxt: ctxt, - } - mock.lockAuthenticate.Lock() - mock.calls.Authenticate = append(mock.calls.Authenticate, callInfo) - mock.lockAuthenticate.Unlock() - return mock.AuthenticateFunc(ctxt) -} - -// AuthenticateCalls gets all the calls that were made to Validate. -// Check the length with: -// len(mockedAuthenticator.AuthenticateCalls()) -func (mock *AuthenticatorMock) AuthenticateCalls() []struct { - Ctxt echo.Context -} { - var calls []struct { - Ctxt echo.Context - } - mock.lockAuthenticate.RLock() - calls = mock.calls.Authenticate - mock.lockAuthenticate.RUnlock() - return calls + lockFromSchema sync.RWMutex + lockValidate sync.RWMutex } // FromSchema calls FromSchemaFunc. -func (mock *AuthenticatorMock) FromSchema(scheme *openapi3.SecurityScheme) (rest.Validator, error) { +func (mock *ValidatorMock) FromSchema(scheme *openapi3.SecurityScheme) (rest.Validator, error) { if mock.FromSchemaFunc == nil { - panic("AuthenticatorMock.FromSchemaFunc: method is nil but Validator.FromSchema was just called") + panic("ValidatorMock.FromSchemaFunc: method is nil but Validator.FromSchema was just called") } callInfo := struct { Scheme *openapi3.SecurityScheme @@ -1555,8 +1524,8 @@ func (mock *AuthenticatorMock) FromSchema(scheme *openapi3.SecurityScheme) (rest // FromSchemaCalls gets all the calls that were made to FromSchema. // Check the length with: -// len(mockedAuthenticator.FromSchemaCalls()) -func (mock *AuthenticatorMock) FromSchemaCalls() []struct { +// len(mockedValidator.FromSchemaCalls()) +func (mock *ValidatorMock) FromSchemaCalls() []struct { Scheme *openapi3.SecurityScheme } { var calls []struct { @@ -1567,3 +1536,34 @@ func (mock *AuthenticatorMock) FromSchemaCalls() []struct { mock.lockFromSchema.RUnlock() return calls } + +// Validate calls ValidateFunc. +func (mock *ValidatorMock) Validate(ctxt echo.Context) (bool, interface{}, string, string, error) { + if mock.ValidateFunc == nil { + panic("ValidatorMock.ValidateFunc: method is nil but Validator.Validate was just called") + } + callInfo := struct { + Ctxt echo.Context + }{ + Ctxt: ctxt, + } + mock.lockValidate.Lock() + mock.calls.Validate = append(mock.calls.Validate, callInfo) + mock.lockValidate.Unlock() + return mock.ValidateFunc(ctxt) +} + +// ValidateCalls gets all the calls that were made to Validate. +// Check the length with: +// len(mockedValidator.ValidateCalls()) +func (mock *ValidatorMock) ValidateCalls() []struct { + Ctxt echo.Context +} { + var calls []struct { + Ctxt echo.Context + } + mock.lockValidate.RLock() + calls = mock.calls.Validate + mock.lockValidate.RUnlock() + return calls +} diff --git a/controllers/rest/security.go b/controllers/rest/security.go index 3e5e23ac..c9c00524 100644 --- a/controllers/rest/security.go +++ b/controllers/rest/security.go @@ -1,10 +1,12 @@ package rest import ( + "context" "fmt" "github.com/getkin/kin-openapi/openapi3" "github.com/labstack/echo/v4" "github.com/labstack/gommon/log" + context2 "github.com/wepala/weos/context" "github.com/wepala/weos/model" "github.com/wepala/weos/projections" "net/http" @@ -12,7 +14,8 @@ import ( //Validator interface that must be implemented so that a request can be authenticated type Validator interface { - Validate(ctxt echo.Context) (bool, error) + //Validate validate and return token, user, role + Validate(ctxt echo.Context) (bool, interface{}, string, string, error) FromSchema(scheme *openapi3.SecurityScheme) (Validator, error) } @@ -74,13 +77,21 @@ func (s *SecurityConfiguration) Middleware(api Container, projection projections for _, validator := range validators { var success bool var err error - if success, err = validator.Validate(ctxt); success { + var userID string + if success, _, userID, _, err = validator.Validate(ctxt); success { + newContext := context.WithValue(ctxt.Request().Context(), context2.USER_ID, userID) + request := ctxt.Request().WithContext(newContext) + ctxt.SetRequest(request) return next(ctxt) - } else { - ctxt.Logger().Debugf("error authenticating '%s'", err) } + ctxt.Logger().Debugf("error authenticating '%s'", err) } - return ctxt.NoContent(http.StatusForbidden) + //if there were validators configured then return un authorized status code + if len(validators) > 0 { + return ctxt.NoContent(http.StatusUnauthorized) + } + + return next(ctxt) } } } diff --git a/controllers/rest/security_authenticators.go b/controllers/rest/security_authenticators.go deleted file mode 100644 index 3b3c7c43..00000000 --- a/controllers/rest/security_authenticators.go +++ /dev/null @@ -1,70 +0,0 @@ -package rest - -import ( - "crypto/x509" - "encoding/json" - "encoding/pem" - "fmt" - "github.com/getkin/kin-openapi/openapi3" - "github.com/golang-jwt/jwt/v4" - "github.com/labstack/echo/v4" - "strings" -) - -//OpenIDConnect authorizer for OpenID -type OpenIDConnect struct { - connectURL string -} - -func (o OpenIDConnect) Validate(ctxt echo.Context) (bool, error) { - //TODO implement me - panic("implement me") -} - -func (o OpenIDConnect) FromSchema(scheme *openapi3.SecurityScheme) (Validator, error) { - var err error - if tinterface, ok := scheme.Extensions[OpenIDConnectUrlExtension]; ok { - if rawURL, ok := tinterface.(json.RawMessage); ok { - err = json.Unmarshal(rawURL, &o.connectURL) - } - } - return o, err -} - -type OAuth2 struct { - connectURL string - Flows *openapi3.OAuthFlows - clientSecret string -} - -func (o OAuth2) Validate(ctxt echo.Context) (bool, error) { - authorizationHeader := ctxt.Request().Header.Get("Authorization") - tokenString := strings.Replace(authorizationHeader, "Bearer ", "", -1) - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - - //TODO figure out good way to load certificate here - cert := `` - - block, _ := pem.Decode([]byte(cert)) - if block == nil { - return nil, fmt.Errorf("unable to decode cert") - } - - pub, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, fmt.Errorf("failed to parse public key '%s'", err) - } - - return pub.PublicKey, nil - }) - return token.Valid, err -} - -func (o OAuth2) FromSchema(scheme *openapi3.SecurityScheme) (Validator, error) { - var err error - o.Flows = scheme.Flows - return o, err -} diff --git a/controllers/rest/security_authenticators_test.go b/controllers/rest/security_authenticators_test.go deleted file mode 100644 index f9fe9903..00000000 --- a/controllers/rest/security_authenticators_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package rest_test - -import ( - "github.com/labstack/echo/v4" - "github.com/wepala/weos/controllers/rest" - "net/http/httptest" - "os" - "testing" -) - -func TestOAuth2_FromSchema(t *testing.T) { - t.Run("initialize oauth2 authenticator", func(t *testing.T) { - swagger, err := LoadConfig(t, "fixtures/blog-security.yaml") - if err != nil { - t.Fatalf("unexpected error loading config '%s'", err) - } - authenticator, err := new(rest.OAuth2).FromSchema(swagger.Components.SecuritySchemes["Auth02"].Value) - if tauthenticator, ok := authenticator.(rest.OAuth2); ok { - if tauthenticator.Flows.AuthorizationCode.AuthorizationURL != swagger.Components.SecuritySchemes["Auth02"].Value.Flows.AuthorizationCode.AuthorizationURL { - t.Errorf("expected authorization url to be '%s', got '%s'", swagger.Components.SecuritySchemes["Auth02"].Value.Flows.AuthorizationCode.AuthorizationURL, tauthenticator.Flows.AuthorizationCode.AuthorizationURL) - } - } else { - t.Fatalf("expected OAuth2 authenticator") - } - }) -} - -func TestOAuth2_Authenticate(t *testing.T) { - t.Run("authenticate valid token", func(t *testing.T) { - t.Skipf("need to figure out way to load certificates see https://wepala.atlassian.net/browse/WEOS-1520") - swagger, err := LoadConfig(t, "fixtures/blog-security.yaml") - if err != nil { - t.Fatalf("unexpected error loading config '%s'", err) - } - - rawRequest := httptest.NewRequest("POST", "/blogs/1234", nil) - token, ok := os.LookupEnv("OAUTH_TEST_KEY") - if !ok { - t.Fatal("test requires token set in 'OAUTH_TEST_KEY' environment variable") - } - rawRequest.Header.Add("Authorization", "Bearer "+token) - rw := httptest.NewRecorder() - - e := echo.New() - ctxt := e.NewContext(rawRequest, rw) - - authenticator, _ := new(rest.OAuth2).FromSchema(swagger.Components.SecuritySchemes["Auth02"].Value) - result, err := authenticator.Validate(ctxt) - if err != nil { - t.Fatalf("error authenticating '%s'", err) - } - - if !result { - t.Error("authentication failed") - } - }) -} diff --git a/controllers/rest/security_test.go b/controllers/rest/security_test.go index 8ef9ecaf..8816b61c 100644 --- a/controllers/rest/security_test.go +++ b/controllers/rest/security_test.go @@ -20,8 +20,8 @@ func TestSecurityConfiguration_FromSchema(t *testing.T) { if err != nil { t.Fatalf("unexpected error setting up security configuration '%s'", err) } - if len(config.Validators) != 1 { - t.Errorf("expected %d authenticators to be setup, got %d", 1, len(config.Validators)) + if len(config.Validators) != 2 { + t.Errorf("expected %d authenticators to be setup, got %d", 2, len(config.Validators)) } }) t.Run("set unrecognized authenticator", func(t *testing.T) { @@ -50,8 +50,8 @@ func TestSecurityConfiguration_Middleware(t *testing.T) { t.Fatalf("unexpected error setting up security configuration '%s'", err) } //set mock authenticator so we can check that is was set and called - mockAuthenticator := &AuthenticatorMock{AuthenticateFunc: func(ctxt echo.Context) (bool, error) { - return true, nil + mockAuthenticator := &ValidatorMock{ValidateFunc: func(ctxt echo.Context) (bool, interface{}, string, string, error) { + return true, nil, "", "", nil }} config.Validators["Auth0"] = mockAuthenticator //find path with no security scheme set @@ -87,8 +87,8 @@ func TestSecurityConfiguration_Middleware(t *testing.T) { e.POST("/blogs", handler) e.ServeHTTP(resp, req) - if len(mockAuthenticator.AuthenticateCalls()) != 1 { - t.Errorf("expected the mock authenticator to be called %d time, called %d times", 1, len(mockAuthenticator.AuthenticateCalls())) + if len(mockAuthenticator.ValidateCalls()) != 1 { + t.Errorf("expected the mock authenticator to be called %d time, called %d times", 1, len(mockAuthenticator.ValidateCalls())) } if !nextMiddlewareHit { diff --git a/controllers/rest/security_validators.go b/controllers/rest/security_validators.go new file mode 100644 index 00000000..2f9139d7 --- /dev/null +++ b/controllers/rest/security_validators.go @@ -0,0 +1,152 @@ +package rest + +import ( + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "github.com/coreos/go-oidc/v3/oidc" + "github.com/getkin/kin-openapi/openapi3" + "github.com/golang-jwt/jwt/v4" + "github.com/labstack/echo/v4" + "golang.org/x/net/context" + "strings" + "time" +) + +//OpenIDConnect authorizer for OpenID +type OpenIDConnect struct { + connectURL string + skipExpiryCheck bool + clientID string + userIDClaim string + roleClaim string +} + +func (o OpenIDConnect) Validate(ctxt echo.Context) (bool, interface{}, string, string, error) { + //get the Jwk url from open id connect url and validate url + openIDConfig, err := GetOpenIDConfig(o.connectURL) + if err != nil { + return false, nil, "", "", err + } else { + if jwks_uri, ok := openIDConfig["jwks_uri"]; ok { + //create key set and verifier + keySet := oidc.NewRemoteKeySet(context.Background(), jwks_uri.(string)) + var algs []string + if talgs, ok := openIDConfig["id_token_signing_alg_values_supported"]; ok { + for _, alg := range talgs.([]interface{}) { + algs = append(algs, alg.(string)) + } + + } + if talgs, ok := openIDConfig["request_object_signing_alg_values_supported"]; ok { + for _, alg := range talgs.([]interface{}) { + algs = append(algs, alg.(string)) + } + } + tokenVerifier := oidc.NewVerifier(o.connectURL, keySet, &oidc.Config{ + ClientID: o.clientID, + SupportedSigningAlgs: algs, + SkipClientIDCheck: o.clientID == "", + SkipExpiryCheck: o.skipExpiryCheck, + SkipIssuerCheck: true, + Now: time.Now, + }) + authorizationHeader := ctxt.Request().Header.Get("Authorization") + tokenString := strings.Replace(authorizationHeader, "Bearer ", "", -1) + token, err := tokenVerifier.Verify(ctxt.Request().Context(), tokenString) + + var userID string + var role string + + if token != nil { + tclaims := make(map[string]interface{}) + tclaims[o.userIDClaim] = token.Subject + tclaims[o.roleClaim] = "" + err = token.Claims(&tclaims) + if err == nil { + role = tclaims[o.roleClaim].(string) + userID = tclaims[o.userIDClaim].(string) + } + } + + return token != nil && err == nil, token, userID, role, err + } else { + return false, nil, "", "", fmt.Errorf("expected jwks_url to be set") + } + } +} + +func (o OpenIDConnect) FromSchema(scheme *openapi3.SecurityScheme) (Validator, error) { + var err error + if tinterface, ok := scheme.Extensions[OpenIDConnectUrlExtension]; ok { + if rawURL, ok := tinterface.(json.RawMessage); ok { + err = json.Unmarshal(rawURL, &o.connectURL) + } + } + + if tinterface, ok := scheme.Extensions[SkipExpiryCheckExtension]; ok { + if expiryCheck, ok := tinterface.(json.RawMessage); ok { + err = json.Unmarshal(expiryCheck, &o.skipExpiryCheck) + } + } + + if jwtMapRaw, ok := scheme.Extensions[JWTMapExtension]; ok { + var jwtMap struct { + User string `json:"user"` + Role string `json:"role"` + } + err = json.Unmarshal(jwtMapRaw.(json.RawMessage), &jwtMap) + if err != nil { + return o, err + } + o.userIDClaim = jwtMap.User + o.roleClaim = jwtMap.Role + } else { + o.userIDClaim = "sub" + } + + _, err = GetOpenIDConfig(o.connectURL) + if err != nil { + return o, fmt.Errorf("invalid open id connect url: '%s'", o.connectURL) + } + return o, err +} + +type OAuth2 struct { + connectURL string + Flows *openapi3.OAuthFlows + clientSecret string +} + +func (o OAuth2) Validate(ctxt echo.Context) (bool, interface{}, string, string, error) { + authorizationHeader := ctxt.Request().Header.Get("Authorization") + tokenString := strings.Replace(authorizationHeader, "Bearer ", "", -1) + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + //TODO figure out good way to load certificate here + cert := `` + + block, _ := pem.Decode([]byte(cert)) + if block == nil { + return nil, fmt.Errorf("unable to decode cert") + } + + pub, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse public key '%s'", err) + } + + return pub.PublicKey, nil + }) + return token.Valid, nil, "", "", err +} + +func (o OAuth2) FromSchema(scheme *openapi3.SecurityScheme) (Validator, error) { + var err error + o.Flows = scheme.Flows + return o, err +} diff --git a/controllers/rest/security_validators_test.go b/controllers/rest/security_validators_test.go new file mode 100644 index 00000000..bc045f19 --- /dev/null +++ b/controllers/rest/security_validators_test.go @@ -0,0 +1,127 @@ +package rest_test + +import ( + "github.com/getkin/kin-openapi/openapi3" + "github.com/labstack/echo/v4" + "github.com/wepala/weos/controllers/rest" + "io/ioutil" + "net/http/httptest" + "os" + "testing" +) + +func TestOpenIDConnect_Validate(t *testing.T) { + t.Run("validate token", func(t *testing.T) { + var err error + var swagger *openapi3.Swagger + var rawJWT []byte + swagger, err = LoadConfig(t, "fixtures/blog-security.yaml") + if err != nil { + t.Fatalf("unexpected error loading config '%s'", err) + } + + rawRequest := httptest.NewRequest("PUT", "/blogs/1234", nil) + rawJWT, err = ioutil.ReadFile("./fixtures/jwt/demo.jwt") + if err != nil { + t.Fatalf("unable to read jwt fixture '%s'", err) + } + rawRequest.Header.Add("Authorization", "Bearer "+string(rawJWT)) + rw := httptest.NewRecorder() + + e := echo.New() + ctxt := e.NewContext(rawRequest, rw) + + authenticator, _ := new(rest.OpenIDConnect).FromSchema(swagger.Components.SecuritySchemes["WeAuth"].Value) + result, _, userID, role, err := authenticator.Validate(ctxt) + if err != nil { + t.Fatalf("error authenticating '%s'", err) + } + + if !result { + t.Error("authentication failed") + } + + if role != "staff" { + t.Errorf("expected the role to be '%s', got '%s'", "staff", role) + } + + if userID != "93b46dcd-baf1-41d2-99cc-449d02bfe195" { + t.Errorf("expected the userID to be '%s', got '%s'", "93b46dcd-baf1-41d2-99cc-449d02bfe195", userID) + } + }) + t.Run("invalid token", func(t *testing.T) { + var err error + var swagger *openapi3.Swagger + var rawJWT []byte + swagger, err = LoadConfig(t, "fixtures/blog-security.yaml") + if err != nil { + t.Fatalf("unexpected error loading config '%s'", err) + } + + rawRequest := httptest.NewRequest("PUT", "/blogs/1234", nil) + rawJWT, err = ioutil.ReadFile("./fixtures/jwt/demo.jwt") + if err != nil { + t.Fatalf("unable to read jwt fixture '%s'", err) + } + rawRequest.Header.Add("Authorization", "Bearer "+string(rawJWT)) + rw := httptest.NewRecorder() + + e := echo.New() + ctxt := e.NewContext(rawRequest, rw) + + authenticator, _ := new(rest.OpenIDConnect).FromSchema(swagger.Components.SecuritySchemes["Auth0"].Value) + result, _, _, _, err := authenticator.Validate(ctxt) + if result { + t.Error("expected validation to fail") + } + }) +} + +func TestOAuth2_FromSchema(t *testing.T) { + t.Skipf("need to figure out way to load certificates see https://wepala.atlassian.net/browse/WEOS-1520") + t.Run("initialize oauth2 authenticator", func(t *testing.T) { + swagger, err := LoadConfig(t, "fixtures/blog-security.yaml") + if err != nil { + t.Fatalf("unexpected error loading config '%s'", err) + } + authenticator, err := new(rest.OAuth2).FromSchema(swagger.Components.SecuritySchemes["Auth02"].Value) + if tauthenticator, ok := authenticator.(rest.OAuth2); ok { + if tauthenticator.Flows.AuthorizationCode.AuthorizationURL != swagger.Components.SecuritySchemes["Auth02"].Value.Flows.AuthorizationCode.AuthorizationURL { + t.Errorf("expected authorization url to be '%s', got '%s'", swagger.Components.SecuritySchemes["Auth02"].Value.Flows.AuthorizationCode.AuthorizationURL, tauthenticator.Flows.AuthorizationCode.AuthorizationURL) + } + } else { + t.Fatalf("expected OAuth2 authenticator") + } + }) +} + +func TestOAuth2_Authenticate(t *testing.T) { + t.Run("authenticate valid token", func(t *testing.T) { + t.Skipf("need to figure out way to load certificates see https://wepala.atlassian.net/browse/WEOS-1520") + swagger, err := LoadConfig(t, "fixtures/blog-security.yaml") + if err != nil { + t.Fatalf("unexpected error loading config '%s'", err) + } + + rawRequest := httptest.NewRequest("POST", "/blogs/1234", nil) + token, ok := os.LookupEnv("OAUTH_TEST_KEY") + if !ok { + t.Fatal("test requires token set in 'OAUTH_TEST_KEY' environment variable") + } + rawRequest.Header.Add("Authorization", "Bearer "+token) + rw := httptest.NewRecorder() + + e := echo.New() + ctxt := e.NewContext(rawRequest, rw) + + authenticator, _ := new(rest.OAuth2).FromSchema(swagger.Components.SecuritySchemes["Auth02"].Value) + result, _, _, _, err := authenticator.Validate(ctxt) + if err != nil { + t.Fatalf("error authenticating '%s'", err) + } + + if !result { + t.Error("authentication failed") + } + }) +} diff --git a/controllers/rest/utils.go b/controllers/rest/utils.go index da28df5a..d76dae4d 100644 --- a/controllers/rest/utils.go +++ b/controllers/rest/utils.go @@ -266,6 +266,7 @@ func SplitFilter(filter string) *FilterProperties { return property } +//Deprecated: 06/20/2022 Use GetOpenIDConfig to get the map of the entire config //GetJwkUrl fetches the jwk url from the open id connect url func GetJwkUrl(openIdUrl string) (string, error) { //fetches the response from the connect id url @@ -291,6 +292,25 @@ func GetJwkUrl(openIdUrl string) (string, error) { return info["jwks_uri"].(string), nil } +//GetOpenIDConfig returns map of openID content +func GetOpenIDConfig(openIdUrl string) (map[string]interface{}, error) { + //fetches the response from the url + resp, err := http.Get(openIdUrl) + if err != nil || resp == nil || resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected error fetching open id connect url") + } + defer resp.Body.Close() + // reads the body + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("unable to read response body: %v", err) + } + // unmarshal the body to a struct we can use to find the jwk uri + var info map[string]interface{} + err = json.Unmarshal(body, &info) + return info, err +} + //JSONMarshal this marshals data without using html.escape func JSONMarshal(t interface{}) ([]byte, error) { buffer := &bytes.Buffer{} diff --git a/features/security-schemes.feature b/features/security-schemes.feature index 7906bbe6..3c12e2f6 100644 --- a/features/security-schemes.feature +++ b/features/security-schemes.feature @@ -690,7 +690,7 @@ Feature: Use OpenAPI Security Scheme to protect endpoints When the "OpenAPI 3.0" specification is parsed Then an error should be returned - @WEOS-1519 + @WEOS-1519 @skipped Scenario: User Denied based on id not being in the allow list In order to support JWT from different authentication services, the developer should be able to specify which part of @@ -705,7 +705,7 @@ Feature: Use OpenAPI Security Scheme to protect endpoints Then a 403 response should be returned - @WEOS-1519 + @WEOS-1519 @skipped Scenario: User Allowed based on the role being on the allow list In order to support JWT from different authentication services, the developer should be able to specify which part of @@ -716,7 +716,7 @@ Feature: Use OpenAPI Security Scheme to protect endpoints Then a 200 response should be returned - @WEOS-1519 + @WEOS-1519 @skipped Scenario: User denied based on the role being on the deny list Given "Sojourner" is on the "Blog" edit screen with id "1234" From cdb6053a1f117f88a5bc8c163070d1d5f334533a Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Mon, 20 Jun 2022 20:35:26 -0400 Subject: [PATCH 07/18] feature: WEOS-1519 Setup permission configuration * Upgraded GORM (the casbin adapter forced that upgrade) * Setup Casbin * Created Authorization initializer that setups a default casbin enforcer using the default GORM connection * Added PermissionEnforcer to the Container --- controllers/rest/api.go | 16 ++ controllers/rest/fixtures/blog-security.yaml | 4 + controllers/rest/interfaces.go | 5 + controllers/rest/openapi_extensions.go | 3 + controllers/rest/operation_initializers.go | 57 +++++ .../rest/operation_initializers_test.go | 37 +++ controllers/rest/rest_mocks_test.go | 93 +++++++ go.mod | 14 +- go.sum | 240 ++++++++++++++++-- projections/dialects/gorm.go | 49 +++- projections/dialects/mysql.go | 115 ++++----- projections/dialects/postgresql.go | 196 ++++++++------ projections/projections_test.go | 8 +- 13 files changed, 659 insertions(+), 178 deletions(-) diff --git a/controllers/rest/api.go b/controllers/rest/api.go index f3340f3f..99fcd2a9 100644 --- a/controllers/rest/api.go +++ b/controllers/rest/api.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "github.com/casbin/casbin/v2" "net/http" "os" "reflect" @@ -60,6 +61,7 @@ type RESTAPI struct { entityFactories map[string]model.EntityFactory dbConnections map[string]*sql.DB gormConnections map[string]*gorm.DB + enforcers map[string]*casbin.Enforcer } type schema struct { @@ -222,6 +224,20 @@ func (p *RESTAPI) RegisterGORMDB(name string, connection *gorm.DB) { p.gormConnections[name] = connection } +func (p *RESTAPI) RegisterPermissionEnforcer(name string, enforcer *casbin.Enforcer) { + if p.enforcers == nil { + p.enforcers = make(map[string]*casbin.Enforcer) + } + p.enforcers[name] = enforcer +} + +func (p *RESTAPI) GetPermissionEnforcer(name string) (*casbin.Enforcer, error) { + if tenforcer, ok := p.enforcers[name]; ok { + return tenforcer, nil + } + return nil, fmt.Errorf("permission enforcer '%s' not found", name) +} + //GetMiddleware get middleware by name func (p *RESTAPI) GetMiddleware(name string) (Middleware, error) { if tmiddleware, ok := p.middlewares[name]; ok { diff --git a/controllers/rest/fixtures/blog-security.yaml b/controllers/rest/fixtures/blog-security.yaml index be27c2b6..590f3113 100644 --- a/controllers/rest/fixtures/blog-security.yaml +++ b/controllers/rest/fixtures/blog-security.yaml @@ -188,6 +188,10 @@ paths: name: Authorization schema: type: string + x-auth: + allow: + users: + - auth0|60d0c84316f69600691c1614 requestBody: description: Blog info that is submitted required: true diff --git a/controllers/rest/interfaces.go b/controllers/rest/interfaces.go index fddf6879..bed4f391 100644 --- a/controllers/rest/interfaces.go +++ b/controllers/rest/interfaces.go @@ -3,6 +3,7 @@ package rest import ( "database/sql" + "github.com/casbin/casbin/v2" "github.com/getkin/kin-openapi/openapi3" "github.com/labstack/echo/v4" "github.com/wepala/weos/model" @@ -88,4 +89,8 @@ type Container interface { GetHTTPClient(name string) (*http.Client, error) RegisterSecurityConfiguration(configuration *SecurityConfiguration) GetSecurityConfiguration() *SecurityConfiguration + //RegisterPermissionEnforcer save permission enforcer + RegisterPermissionEnforcer(name string, enforcer *casbin.Enforcer) + //GetPermissionEnforcer get Casbin enforcer + GetPermissionEnforcer(name string) (*casbin.Enforcer, error) } diff --git a/controllers/rest/openapi_extensions.go b/controllers/rest/openapi_extensions.go index 5fadcc26..b9068716 100644 --- a/controllers/rest/openapi_extensions.go +++ b/controllers/rest/openapi_extensions.go @@ -47,6 +47,9 @@ const JWTMapExtension = "x-jwt-map" //SkipExpiryCheckExtension set the expiry check const SkipExpiryCheckExtension = "x-skip-expiry-check" +//AuthorizationConfigExtension setup authorization +const AuthorizationConfigExtension = "x-auth" + //FolderExtension set staticFolder folder const FolderExtension = "x-folder" diff --git a/controllers/rest/operation_initializers.go b/controllers/rest/operation_initializers.go index 503638f5..0b7c41af 100644 --- a/controllers/rest/operation_initializers.go +++ b/controllers/rest/operation_initializers.go @@ -3,6 +3,9 @@ package rest import ( "encoding/json" "fmt" + "github.com/casbin/casbin/v2" + casbinmodel "github.com/casbin/casbin/v2/model" + gormadapter "github.com/casbin/gorm-adapter/v3" "github.com/getkin/kin-openapi/openapi3" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" @@ -40,6 +43,60 @@ func ContentTypeResponseInitializer(ctxt context.Context, api Container, path st return ctxt, nil } +//AuthorizationInitializer setup authorization +func AuthorizationInitializer(ctxt context.Context, tapi Container, path string, method string, swagger *openapi3.Swagger, pathItem *openapi3.PathItem, operation *openapi3.Operation) (context.Context, error) { + if authRaw, ok := operation.Extensions[AuthorizationConfigExtension]; ok { + var enforcer *casbin.Enforcer + var err error + //check if the default enforcer is setup + if enforcer, err = tapi.GetPermissionEnforcer("Default"); err != nil { + var adapter interface{} + if gormDB, err := tapi.GetGormDBConnection("Default"); err == nil { + adapter, _ = gormadapter.NewAdapterByDB(gormDB) + } else { + adapter = "./policy.csv" + } + + //default REST permission model + text := `[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act) +` + m, _ := casbinmodel.NewModelFromString(text) + enforcer, err = casbin.NewEnforcer(m, adapter) + if err != nil { + return ctxt, err + } + tapi.RegisterPermissionEnforcer("Default", enforcer) + } + //add rule to the enforcer based on the operation + var authConfig map[string]interface{} + if err = json.Unmarshal(authRaw.(json.RawMessage), &authConfig); err != nil { + if allowRules, ok := authConfig["allow"]; ok { + if u, ok := allowRules.(map[string]interface{})["users"]; ok { + for _, user := range u.([]string) { + var success bool + success, err = enforcer.AddPolicy(user, path, method) + if !success { + + } + } + } + } + } + return ctxt, err + } + return ctxt, nil +} + //EntityFactoryInitializer setups the EntityFactory for a specific route func EntityFactoryInitializer(ctxt context.Context, api Container, path string, method string, swagger *openapi3.Swagger, pathItem *openapi3.PathItem, operation *openapi3.Operation) (context.Context, error) { jsonSchema := operation.ExtensionProps.Extensions[SchemaExtension] diff --git a/controllers/rest/operation_initializers_test.go b/controllers/rest/operation_initializers_test.go index 6b85115a..8163be55 100644 --- a/controllers/rest/operation_initializers_test.go +++ b/controllers/rest/operation_initializers_test.go @@ -1,6 +1,8 @@ package rest_test import ( + "fmt" + "github.com/casbin/casbin/v2" "github.com/getkin/kin-openapi/openapi3" "github.com/labstack/echo/v4" weoscontext "github.com/wepala/weos/context" @@ -8,6 +10,7 @@ import ( "github.com/wepala/weos/model" "github.com/wepala/weos/projections" "golang.org/x/net/context" + "gorm.io/gorm" "net/http" "net/http/httptest" "testing" @@ -397,3 +400,37 @@ func TestGettersForOperationFunctions(t *testing.T) { } }) } + +func TestAuthorizationInitializer(t *testing.T) { + api, err := rest.New("./fixtures/blog-security.yaml") + if err != nil { + t.Fatalf("unexpected error loading api '%s'", err) + } + err = api.Initialize(context.Background()) + if err != nil { + t.Fatalf("unexpected error initializing api '%s'", err) + } + swagger := api.Swagger + + t.Run("setup default enforcer", func(t *testing.T) { + container := &ContainerMock{ + GetPermissionEnforcerFunc: func(name string) (*casbin.Enforcer, error) { + return nil, fmt.Errorf("enforcer named '%s' not found", name) + }, + RegisterPermissionEnforcerFunc: func(name string, enforcer *casbin.Enforcer) { + + }, + GetGormDBConnectionFunc: func(name string) (*gorm.DB, error) { + return api.GetGormDBConnection(name) + }, + } + path := swagger.Paths.Find("/blogs") + _, err := rest.AuthorizationInitializer(context.TODO(), container, "/blogs", "POST", swagger, path, path.Post) + if err != nil { + t.Fatalf("unexpected error setting up authorization '%s'", err) + } + if len(container.RegisterPermissionEnforcerCalls()) != 1 { + t.Fatalf("expected default enforcer to be set") + } + }) +} diff --git a/controllers/rest/rest_mocks_test.go b/controllers/rest/rest_mocks_test.go index a85df3ef..57f3fac9 100644 --- a/controllers/rest/rest_mocks_test.go +++ b/controllers/rest/rest_mocks_test.go @@ -5,6 +5,7 @@ package rest_test import ( "database/sql" + "github.com/casbin/casbin/v2" "github.com/getkin/kin-openapi/openapi3" "github.com/labstack/echo/v4" "github.com/wepala/weos/controllers/rest" @@ -64,6 +65,9 @@ var _ rest.Container = &ContainerMock{} // GetOperationInitializersFunc: func() []rest.OperationInitializer { // panic("mock out the GetOperationInitializers method") // }, +// GetPermissionEnforcerFunc: func(name string) (*casbin.Enforcer, error) { +// panic("mock out the GetPermissionEnforcer method") +// }, // GetPostPathInitializersFunc: func() []rest.PathInitializer { // panic("mock out the GetPostPathInitializers method") // }, @@ -112,6 +116,9 @@ var _ rest.Container = &ContainerMock{} // RegisterOperationInitializerFunc: func(initializer rest.OperationInitializer) { // panic("mock out the RegisterOperationInitializer method") // }, +// RegisterPermissionEnforcerFunc: func(name string, enforcer *casbin.Enforcer) { +// panic("mock out the RegisterPermissionEnforcer method") +// }, // RegisterPostPathInitializerFunc: func(initializer rest.PathInitializer) { // panic("mock out the RegisterPostPathInitializer method") // }, @@ -170,6 +177,9 @@ type ContainerMock struct { // GetOperationInitializersFunc mocks the GetOperationInitializers method. GetOperationInitializersFunc func() []rest.OperationInitializer + // GetPermissionEnforcerFunc mocks the GetPermissionEnforcer method. + GetPermissionEnforcerFunc func(name string) (*casbin.Enforcer, error) + // GetPostPathInitializersFunc mocks the GetPostPathInitializers method. GetPostPathInitializersFunc func() []rest.PathInitializer @@ -218,6 +228,9 @@ type ContainerMock struct { // RegisterOperationInitializerFunc mocks the RegisterOperationInitializer method. RegisterOperationInitializerFunc func(initializer rest.OperationInitializer) + // RegisterPermissionEnforcerFunc mocks the RegisterPermissionEnforcer method. + RegisterPermissionEnforcerFunc func(name string, enforcer *casbin.Enforcer) + // RegisterPostPathInitializerFunc mocks the RegisterPostPathInitializer method. RegisterPostPathInitializerFunc func(initializer rest.PathInitializer) @@ -289,6 +302,11 @@ type ContainerMock struct { // GetOperationInitializers holds details about calls to the GetOperationInitializers method. GetOperationInitializers []struct { } + // GetPermissionEnforcer holds details about calls to the GetPermissionEnforcer method. + GetPermissionEnforcer []struct { + // Name is the name argument value. + Name string + } // GetPostPathInitializers holds details about calls to the GetPostPathInitializers method. GetPostPathInitializers []struct { } @@ -379,6 +397,13 @@ type ContainerMock struct { // Initializer is the initializer argument value. Initializer rest.OperationInitializer } + // RegisterPermissionEnforcer holds details about calls to the RegisterPermissionEnforcer method. + RegisterPermissionEnforcer []struct { + // Name is the name argument value. + Name string + // Enforcer is the enforcer argument value. + Enforcer *casbin.Enforcer + } // RegisterPostPathInitializer holds details about calls to the RegisterPostPathInitializer method. RegisterPostPathInitializer []struct { // Initializer is the initializer argument value. @@ -415,6 +440,7 @@ type ContainerMock struct { lockGetLog sync.RWMutex lockGetMiddleware sync.RWMutex lockGetOperationInitializers sync.RWMutex + lockGetPermissionEnforcer sync.RWMutex lockGetPostPathInitializers sync.RWMutex lockGetPrePathInitializers sync.RWMutex lockGetProjection sync.RWMutex @@ -431,6 +457,7 @@ type ContainerMock struct { lockRegisterLog sync.RWMutex lockRegisterMiddleware sync.RWMutex lockRegisterOperationInitializer sync.RWMutex + lockRegisterPermissionEnforcer sync.RWMutex lockRegisterPostPathInitializer sync.RWMutex lockRegisterPrePathInitializer sync.RWMutex lockRegisterProjection sync.RWMutex @@ -820,6 +847,37 @@ func (mock *ContainerMock) GetOperationInitializersCalls() []struct { return calls } +// GetPermissionEnforcer calls GetPermissionEnforcerFunc. +func (mock *ContainerMock) GetPermissionEnforcer(name string) (*casbin.Enforcer, error) { + if mock.GetPermissionEnforcerFunc == nil { + panic("ContainerMock.GetPermissionEnforcerFunc: method is nil but Container.GetPermissionEnforcer was just called") + } + callInfo := struct { + Name string + }{ + Name: name, + } + mock.lockGetPermissionEnforcer.Lock() + mock.calls.GetPermissionEnforcer = append(mock.calls.GetPermissionEnforcer, callInfo) + mock.lockGetPermissionEnforcer.Unlock() + return mock.GetPermissionEnforcerFunc(name) +} + +// GetPermissionEnforcerCalls gets all the calls that were made to GetPermissionEnforcer. +// Check the length with: +// len(mockedContainer.GetPermissionEnforcerCalls()) +func (mock *ContainerMock) GetPermissionEnforcerCalls() []struct { + Name string +} { + var calls []struct { + Name string + } + mock.lockGetPermissionEnforcer.RLock() + calls = mock.calls.GetPermissionEnforcer + mock.lockGetPermissionEnforcer.RUnlock() + return calls +} + // GetPostPathInitializers calls GetPostPathInitializersFunc. func (mock *ContainerMock) GetPostPathInitializers() []rest.PathInitializer { if mock.GetPostPathInitializersFunc == nil { @@ -1332,6 +1390,41 @@ func (mock *ContainerMock) RegisterOperationInitializerCalls() []struct { return calls } +// RegisterPermissionEnforcer calls RegisterPermissionEnforcerFunc. +func (mock *ContainerMock) RegisterPermissionEnforcer(name string, enforcer *casbin.Enforcer) { + if mock.RegisterPermissionEnforcerFunc == nil { + panic("ContainerMock.RegisterPermissionEnforcerFunc: method is nil but Container.RegisterPermissionEnforcer was just called") + } + callInfo := struct { + Name string + Enforcer *casbin.Enforcer + }{ + Name: name, + Enforcer: enforcer, + } + mock.lockRegisterPermissionEnforcer.Lock() + mock.calls.RegisterPermissionEnforcer = append(mock.calls.RegisterPermissionEnforcer, callInfo) + mock.lockRegisterPermissionEnforcer.Unlock() + mock.RegisterPermissionEnforcerFunc(name, enforcer) +} + +// RegisterPermissionEnforcerCalls gets all the calls that were made to RegisterPermissionEnforcer. +// Check the length with: +// len(mockedContainer.RegisterPermissionEnforcerCalls()) +func (mock *ContainerMock) RegisterPermissionEnforcerCalls() []struct { + Name string + Enforcer *casbin.Enforcer +} { + var calls []struct { + Name string + Enforcer *casbin.Enforcer + } + mock.lockRegisterPermissionEnforcer.RLock() + calls = mock.calls.RegisterPermissionEnforcer + mock.lockRegisterPermissionEnforcer.RUnlock() + return calls +} + // RegisterPostPathInitializer calls RegisterPostPathInitializerFunc. func (mock *ContainerMock) RegisterPostPathInitializer(initializer rest.PathInitializer) { if mock.RegisterPostPathInitializerFunc == nil { diff --git a/go.mod b/go.mod index 3ebdc899..3b792135 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/wepala/weos go 1.16 require ( + github.com/casbin/casbin/v2 v2.47.3 + github.com/casbin/gorm-adapter/v3 v3.7.2 github.com/coreos/go-oidc/v3 v3.1.0 github.com/cucumber/godog v0.12.2 github.com/getkin/kin-openapi v0.15.0 @@ -12,7 +14,7 @@ require ( github.com/labstack/echo/v4 v4.5.0 github.com/labstack/gommon v0.3.0 github.com/lib/pq v1.10.2 - github.com/mattn/go-sqlite3 v1.14.9 + github.com/mattn/go-sqlite3 v1.14.12 github.com/ompluscator/dynamic-struct v1.3.0 github.com/ory/dockertest/v3 v3.6.0 github.com/proullon/ramsql v0.0.0-20181213202341-817cee58a244 @@ -20,12 +22,12 @@ require ( github.com/segmentio/ksuid v1.0.3 github.com/testcontainers/testcontainers-go v0.12.0 go.uber.org/zap v1.13.0 - golang.org/x/net v0.0.0-20211108170745-6635138e15ea + golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 gorm.io/datatypes v1.0.5 gorm.io/driver/clickhouse v0.2.2 - gorm.io/driver/mysql v1.2.2 - gorm.io/driver/postgres v1.2.3 + gorm.io/driver/mysql v1.3.3 + gorm.io/driver/postgres v1.3.4 gorm.io/driver/sqlite v1.2.6 - gorm.io/driver/sqlserver v1.2.1 - gorm.io/gorm v1.22.4 + gorm.io/driver/sqlserver v1.3.2 + gorm.io/gorm v1.23.4 ) diff --git a/go.sum b/go.sum index e9141dbb..a2c3a7e7 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,9 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= @@ -43,6 +46,8 @@ github.com/ClickHouse/clickhouse-go v1.4.5 h1:FfhyEnv6/BaWldyjgT2k4gDDmeNwJ9C4Nb github.com/ClickHouse/clickhouse-go v1.4.5/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= github.com/Flaque/filet v0.0.0-20201012163910-45f684403088 h1:PnnQln5IGbhLeJOi6hVs+lCeF+B1dRfFKPGXUAez0Ww= github.com/Flaque/filet v0.0.0-20201012163910-45f684403088/go.mod h1:TK+jB3mBs+8ZMWhU5BqZKnZWJ1MrLo8etNVg51ueTBo= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= @@ -97,6 +102,11 @@ github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7 github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/casbin/casbin/v2 v2.37.4/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= +github.com/casbin/casbin/v2 v2.47.3 h1:enLgZwGYQUwj7iZyFbCvBajgGGaGWz/w+Qal+PZ+72I= +github.com/casbin/casbin/v2 v2.47.3/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= +github.com/casbin/gorm-adapter/v3 v3.7.2 h1:sC2ktcVH70tkcIdM8XuDLpUFKsONlGwSEBKt1ORKYAE= +github.com/casbin/gorm-adapter/v3 v3.7.2/go.mod h1:7mwHmC2phiw6N4gDWlzi+c4DUX7zaVmQC/hINsRgBDg= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= @@ -227,13 +237,14 @@ github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjI github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denisenkom/go-mssqldb v0.11.0 h1:9rHa233rhdOyrz2GcP9NM+gi2psgJZ4GWDpL/7ND8HI= -github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/denisenkom/go-mssqldb v0.12.0 h1:VtrkII767ttSPNRfFekePK3sctr+joXgO58stqQbtUA= +github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= @@ -273,6 +284,10 @@ github.com/getkin/kin-openapi v0.15.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/glebarez/go-sqlite v1.16.0 h1:h28rHued+hGof3fNLksBcLwz/a71fiGZ/eIJHK0SsLI= +github.com/glebarez/go-sqlite v1.16.0/go.mod h1:i8/JtqoqzBAFkrUTxbQFkQ05odCOds3j7NlDaXjqiPY= +github.com/glebarez/sqlite v1.4.3 h1:ZABNo+2YIau8F8sZ7Qh/1h/ZnlSUMHFGD4zJKPval7A= +github.com/glebarez/sqlite v1.4.3/go.mod h1:FcJlwP9scnxlQ5zxyl0+bn/qFjYcqG4eRvKYhs39QAQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -297,6 +312,7 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -320,8 +336,11 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.4.1 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ= github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188 h1:+eHOFJl1BaXrQxKX+T06f78590z4qA2ZzBTqahsKSE4= +github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -334,6 +353,8 @@ github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -360,6 +381,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -444,8 +466,8 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.10.1 h1:DzdIHIjG1AxGwoEEqS+mGsURyjt4enSmqzACXvVzOT8= -github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.11.0 h1:HiHArx4yFbwl91X3qqIHtUFoiIfLNJXCQRsnzkiwwaQ= +github.com/jackc/pgconn v1.11.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= @@ -470,24 +492,26 @@ github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01C github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.9.0 h1:/SH1RxEtltvJgsDqp3TbiTFApD3mey3iygpuEGeuBXk= -github.com/jackc/pgtype v1.9.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38= +github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.14.0 h1:TgdrmgnM7VY72EuSQzBbBd4JA1RLqJolrw9nQVZABVc= -github.com/jackc/pgx/v4 v4.14.0/go.mod h1:jT3ibf/A0ZVCp89rtCIN0zCJxcE74ypROmHEZYsG/j8= +github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w= +github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= @@ -500,6 +524,7 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -548,13 +573,15 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA= github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= +github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -583,6 +610,7 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= @@ -638,6 +666,7 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -680,6 +709,8 @@ github.com/proullon/ramsql v0.0.0-20181213202341-817cee58a244 h1:fdX2U+a2Rmc4BjR github.com/proullon/ramsql v0.0.0-20181213202341-817cee58a244/go.mod h1:jG8oAQG0ZPHPyxg5QlMERS31airDC+ZuqiAe8DUvFVo= github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -739,8 +770,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -805,7 +837,6 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -816,12 +847,14 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -892,8 +925,10 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20211108170745-6635138e15ea h1:FosBMXtOc8Tp9Hbo4ltl1WJSrTVewZU8MPnTPY2HdH8= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211108170745-6635138e15ea/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -969,6 +1004,7 @@ golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -978,9 +1014,14 @@ golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211109184856-51b60fd695b3 h1:T6tyxxvHMj2L1R2kZg0uNMpS8ZhB9lRa9XRGTCSA65w= golang.org/x/sys v0.0.0-20211109184856-51b60fd695b3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1041,6 +1082,7 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1153,25 +1195,33 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/datatypes v1.0.5 h1:3vHCfg4Bz8SDx83zE+ASskF+g/j0kWrcKrY9jFUyAl0= gorm.io/datatypes v1.0.5/go.mod h1:acG/OHGwod+1KrbwPL1t+aavb7jOBOETeyl5M8K5VQs= gorm.io/driver/clickhouse v0.2.2 h1:s1qyq9cx7hfPNaM0bRXsgu+RfaRduuWLKf4R65WfY3Q= gorm.io/driver/clickhouse v0.2.2/go.mod h1:w405Z1It29M82kcwXkdFJmcRi/q8intR0kxSaS8IkTI= -gorm.io/driver/mysql v1.2.2 h1:2qoqhOun1maoJOfLtnzJwq+bZlHkEF34rGntgySqp48= +gorm.io/driver/mysql v1.0.3/go.mod h1:twGxftLBlFgNVNakL7F+P/x9oYqoymG3YYT8cAfI9oI= gorm.io/driver/mysql v1.2.2/go.mod h1:qsiz+XcAyMrS6QY+X3M9R6b/lKM1imKmcuK9kac5LTo= -gorm.io/driver/postgres v1.2.3 h1:f4t0TmNMy9gh3TU2PX+EppoA6YsgFnyq8Ojtddb42To= -gorm.io/driver/postgres v1.2.3/go.mod h1:pJV6RgYQPG47aM1f0QeOzFH9HxQc8JcmAgjRCgS0wjs= +gorm.io/driver/mysql v1.3.3 h1:jXG9ANrwBc4+bMvBcSl8zCfPBaVoPyBEBshA8dA93X8= +gorm.io/driver/mysql v1.3.3/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U= +gorm.io/driver/postgres v1.3.4 h1:evZ7plF+Bp+Lr1mO5NdPvd6M/N98XtwHixGB+y7fdEQ= +gorm.io/driver/postgres v1.3.4/go.mod h1:y0vEuInFKJtijuSGu9e5bs5hzzSzPK+LancpKpvbRBw= gorm.io/driver/sqlite v1.2.6 h1:SStaH/b+280M7C8vXeZLz/zo9cLQmIGwwj3cSj7p6l4= gorm.io/driver/sqlite v1.2.6/go.mod h1:gyoX0vHiiwi0g49tv+x2E7l8ksauLK0U/gShcdUsjWY= -gorm.io/driver/sqlserver v1.2.1 h1:KhGOjvPX7JZ5hPyQICTJfMuTz88zgJ2lk9bWiHVNHd8= -gorm.io/driver/sqlserver v1.2.1/go.mod h1:nixq0OB3iLXZDiPv6JSOjWuPgpyaRpOIIevYtA4Ulb4= +gorm.io/driver/sqlserver v1.3.2 h1:yYt8f/xdAKLY7lCCyXxIUEgZ/WsURos3dHrx8MKFGAk= +gorm.io/driver/sqlserver v1.3.2/go.mod h1:w25Vrx2BG+CJNUu/xKbFhaKlGxT/nzRkhWCCoptX8tQ= +gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.20.11/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.22.0/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= -gorm.io/gorm v1.22.2/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= gorm.io/gorm v1.22.3/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= -gorm.io/gorm v1.22.4 h1:8aPcyEJhY0MAt8aY6Dc524Pn+pO29K+ydu+e/cXSpQM= gorm.io/gorm v1.22.4/go.mod h1:1aeVC+pe9ZmvKZban/gW4QPra7PRoTEssyc922qCAkk= +gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.4 h1:1BKWM67O6CflSLcwGQR7ccfmC4ebOxQrTfOQGRE9wjg= +gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/plugin/dbresolver v1.1.0 h1:cegr4DeprR6SkLIQlKhJLYxH8muFbJ4SmnojXvoeb00= +gorm.io/plugin/dbresolver v1.1.0/go.mod h1:tpImigFAEejCALOttyhWqsy4vfa2Uh/vAUVnL5IRF7Y= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= @@ -1197,6 +1247,144 @@ k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.20/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.22/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.24/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.35.25/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.35.26/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60= +modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw= +modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI= +modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag= +modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw= +modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ= +modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c= +modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo= +modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg= +modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I= +modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs= +modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8= +modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE= +modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk= +modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w= +modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE= +modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8= +modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc= +modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU= +modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE= +modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk= +modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI= +modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE= +modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg= +modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74= +modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU= +modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU= +modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc= +modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM= +modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ= +modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84= +modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ= +modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY= +modernc.org/ccgo/v3 v3.12.84/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w= +modernc.org/ccgo/v3 v3.12.86/go.mod h1:dN7S26DLTgVSni1PVA3KxxHTcykyDurf3OgUzNqTSrU= +modernc.org/ccgo/v3 v3.12.90/go.mod h1:obhSc3CdivCRpYZmrvO88TXlW0NvoSVvdh/ccRjJYko= +modernc.org/ccgo/v3 v3.12.92/go.mod h1:5yDdN7ti9KWPi5bRVWPl8UNhpEAtCjuEE7ayQnzzqHA= +modernc.org/ccgo/v3 v3.13.1/go.mod h1:aBYVOUfIlcSnrsRVU8VRS35y2DIfpgkmVkYZ0tpIXi4= +modernc.org/ccgo/v3 v3.15.9/go.mod h1:md59wBwDT2LznX/OTCPoVS6KIsdRgY8xqQwBV+hkTH0= +modernc.org/ccgo/v3 v3.15.10/go.mod h1:wQKxoFn0ynxMuCLfFD09c8XPUCc8obfchoVR9Cn0fI8= +modernc.org/ccgo/v3 v3.15.12/go.mod h1:VFePOWoCd8uDGRJpq/zfJ29D0EVzMSyID8LCMWYbX6I= +modernc.org/ccgo/v3 v3.15.14/go.mod h1:144Sz2iBCKogb9OKwsu7hQEub3EVgOlyI8wMUPGKUXQ= +modernc.org/ccgo/v3 v3.15.15/go.mod h1:z5qltXjU4PJl0pE5nhYQCvA9DhPHiWsl5GWl89+NSYE= +modernc.org/ccgo/v3 v3.15.16/go.mod h1:XbKRMeMWMdq712Tr5ECgATYMrzJ+g9zAZEj2ktzBe24= +modernc.org/ccgo/v3 v3.15.17/go.mod h1:bofnFkpRFf5gLY+mBZIyTW6FEcp26xi2lgOFk2Rlvs0= +modernc.org/ccgo/v3 v3.15.18/go.mod h1:/2lv3WjHyanEr2sAPdGKRC38n6f0werut9BRXUjjX+A= +modernc.org/ccgo/v3 v3.15.19/go.mod h1:TDJj+DxR26pkDteH2E5WQDj/xlmtsX7JdzkJkaZhOVU= +modernc.org/ccgo/v3 v3.16.2/go.mod h1:w55kPTAqvRMAYS3Lwij6qhqIuBEYS3Z8QtDkjD8cnik= +modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q= +modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg= +modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M= +modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU= +modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE= +modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso= +modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8= +modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8= +modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I= +modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk= +modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY= +modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE= +modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg= +modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM= +modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg= +modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo= +modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8= +modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ= +modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA= +modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM= +modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg= +modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE= +modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM= +modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU= +modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw= +modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M= +modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18= +modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8= +modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= +modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0= +modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI= +modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE= +modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY= +modernc.org/libc v1.11.88/go.mod h1:h3oIVe8dxmTcchcFuCcJ4nAWaoiwzKCdv82MM0oiIdQ= +modernc.org/libc v1.11.98/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c= +modernc.org/libc v1.11.101/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI= +modernc.org/libc v1.12.0/go.mod h1:2MH3DaF/gCU8i/UBiVE1VFRos4o523M7zipmwH8SIgQ= +modernc.org/libc v1.14.1/go.mod h1:npFeGWjmZTjFeWALQLrvklVmAxv4m80jnG3+xI8FdJk= +modernc.org/libc v1.14.2/go.mod h1:MX1GBLnRLNdvmK9azU9LCxZ5lMyhrbEMK8rG3X/Fe34= +modernc.org/libc v1.14.3/go.mod h1:GPIvQVOVPizzlqyRX3l756/3ppsAgg1QgPxjr5Q4agQ= +modernc.org/libc v1.14.6/go.mod h1:2PJHINagVxO4QW/5OQdRrvMYo+bm5ClpUFfyXCYl9ak= +modernc.org/libc v1.14.7/go.mod h1:f8xfWXW8LW41qb4X5+huVQo5dcfPlq7Cbny2TDheMv0= +modernc.org/libc v1.14.8/go.mod h1:9+JCLb1MWSY23smyOpIPbd5ED+rSS/ieiDWUpdyO3mo= +modernc.org/libc v1.14.10/go.mod h1:y1MtIWhwpJFpLYm6grAThtuXJKEsY6xkdZmXbRngIdo= +modernc.org/libc v1.14.11/go.mod h1:l5/Mz/GrZwOqzwRHA3abgSCnSeJzzTl+Ify0bAwKbAw= +modernc.org/libc v1.14.12/go.mod h1:fJdoe23MHu2ruPQkFPPqCpToDi5cckzsbmkI6Ez0LqQ= +modernc.org/libc v1.15.0/go.mod h1:H1OKCu+NYa9+uQG8WsP7DndMBP61I4PWH8ivWhbdoWQ= +modernc.org/libc v1.15.1 h1:q4wjNNEdw9eceGHEUxklZyPVTkOPHjywI7qKQBj1f/A= +modernc.org/libc v1.15.1/go.mod h1:CoZ2riUhSNTAP4bADwpxkLCyJK9SbbMvle0YRzkRT/I= +modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= +modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM= +modernc.org/memory v1.0.6/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.0.7 h1:UE3cxTRFa5tfUibAV7Jqq8P7zRY0OlJg+yWVIIaluEE= +modernc.org/memory v1.0.7/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.16.0 h1:DdvOGaWN0y+X7t2L7RUD63gcwbVjYZjcBZnA68g44EI= +modernc.org/sqlite v1.16.0/go.mod h1:Jwe13ItpESZ+78K5WS6+AjXsUg+JvirsjN3iIDO4C8k= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/tcl v1.11.2/go.mod h1:BRzgpajcGdS2qTxniOx9c/dcxjlbA7p12eJNmiriQYo= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.3.2/go.mod h1:PEU2oK2OEA1CfzDTd+8E908qEXhC9s0MfyKp5LZsd+k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/projections/dialects/gorm.go b/projections/dialects/gorm.go index cc1c8015..4eda8dd9 100644 --- a/projections/dialects/gorm.go +++ b/projections/dialects/gorm.go @@ -24,6 +24,11 @@ type Migrator struct { migrator.Migrator } +// BuildIndexOptionsInterface build index options interface +type BuildIndexOptionsInterface interface { + BuildIndexOptions([]schema.IndexOption, *gorm.Statement) []interface{} +} + // AutoMigrate func (m Migrator) AutoMigrate(values ...interface{}) error { for _, value := range m.ReorderModels(values, true) { @@ -34,14 +39,14 @@ func (m Migrator) AutoMigrate(values ...interface{}) error { } } else { if err := m.RunWithValue(value, func(stmt *gorm.Statement) (errr error) { - columnTypes, _ := m.DB.Migrator().ColumnTypes(value) - for _, field := range stmt.Schema.FieldsByDBName { + for _, dbName := range stmt.Schema.DBNames { + field := stmt.Schema.FieldsByDBName[dbName] var foundColumn gorm.ColumnType for _, columnType := range columnTypes { - if columnType.Name() == field.DBName { + if columnType.Name() == dbName { foundColumn = columnType break } @@ -49,7 +54,7 @@ func (m Migrator) AutoMigrate(values ...interface{}) error { if foundColumn == nil { // not found, add column - if err := tx.Migrator().AddColumn(value, field.DBName); err != nil { + if err := tx.Migrator().AddColumn(value, dbName); err != nil { return err } } else if err := m.DB.Migrator().MigrateColumn(value, field, foundColumn); err != nil { @@ -182,7 +187,7 @@ func (m Migrator) CreateTable(values ...interface{}) error { } createTableSQL += "," - values = append(values, clause.Expr{SQL: idx.Name}, tx.Migrator().(migrator.BuildIndexOptionsInterface).BuildIndexOptions(idx.Fields, stmt)) + values = append(values, clause.Expr{SQL: idx.Name}, tx.Migrator().(BuildIndexOptionsInterface).BuildIndexOptions(idx.Fields, stmt)) } } @@ -389,6 +394,30 @@ func (m Migrator) MigrateColumn(value interface{}, field *schema.Field, columnTy } } + // check unique + if unique, ok := columnType.Unique(); ok && unique != field.Unique { + // not primary key + if !field.PrimaryKey { + alterColumn = true + } + } + + // check default value + if v, ok := columnType.DefaultValue(); ok && v != field.DefaultValue { + // not primary key + if !field.PrimaryKey { + alterColumn = true + } + } + + // check comment + if comment, ok := columnType.Comment(); ok && comment != field.Comment { + // not primary key + if !field.PrimaryKey { + alterColumn = true + } + } + if alterColumn && !field.IgnoreMigration { return m.DB.Migrator().AlterColumn(value, field.Name) } @@ -399,13 +428,15 @@ func (m Migrator) MigrateColumn(value interface{}, field *schema.Field, columnTy // ColumnTypes return columnTypes []gorm.ColumnType and execErr error func (m Migrator) ColumnTypes(value interface{}) ([]gorm.ColumnType, error) { columnTypes := make([]gorm.ColumnType, 0) - execErr := m.RunWithValue(value, func(stmt *gorm.Statement) error { + execErr := m.RunWithValue(value, func(stmt *gorm.Statement) (err error) { rows, err := m.DB.Session(&gorm.Session{}).Table(stmt.Table).Limit(1).Rows() if err != nil { return err } - defer rows.Close() + defer func() { + err = rows.Close() + }() var rawColumnTypes []*sql.ColumnType rawColumnTypes, err = rows.ColumnTypes() @@ -414,10 +445,10 @@ func (m Migrator) ColumnTypes(value interface{}) ([]gorm.ColumnType, error) { } for _, c := range rawColumnTypes { - columnTypes = append(columnTypes, c) + columnTypes = append(columnTypes, migrator.ColumnType{SQLColumnType: c}) } - return nil + return }) return columnTypes, execErr diff --git a/projections/dialects/mysql.go b/projections/dialects/mysql.go index fbedc1e5..0ea9bcc1 100644 --- a/projections/dialects/mysql.go +++ b/projections/dialects/mysql.go @@ -3,6 +3,7 @@ package dialects import ( "database/sql" "fmt" + "strings" "gorm.io/driver/mysql" "gorm.io/gorm" @@ -19,64 +20,14 @@ func NewMySQL(config mysql.Config) gorm.Dialector { return &MySQL{&mysql.Dialector{Config: &config}} } -type MySQLColumn struct { - name string - nullable sql.NullString - datatype string - maxLen sql.NullInt64 - precision sql.NullInt64 - scale sql.NullInt64 - datetimePrecision sql.NullInt64 -} - -func (c MySQLColumn) Name() string { - return c.name -} - -func (c MySQLColumn) DatabaseTypeName() string { - return c.datatype -} - -func (c MySQLColumn) Length() (int64, bool) { - if c.maxLen.Valid { - return c.maxLen.Int64, c.maxLen.Valid - } - - return 0, false -} - -func (c MySQLColumn) Nullable() (bool, bool) { - if c.nullable.Valid { - return c.nullable.String == "YES", true - } - - return false, false -} - -// DecimalSize return precision int64, scale int64, ok bool -func (c MySQLColumn) DecimalSize() (int64, int64, bool) { - if c.precision.Valid { - if c.scale.Valid { - return c.precision.Int64, c.scale.Int64, true - } - - return c.precision.Int64, 0, true - } - - if c.datetimePrecision.Valid { - return c.datetimePrecision.Int64, 0, true - } - - return 0, 0, false -} - type MySQLMigrator struct { Migrator + DisableDatetimePrecision bool } func (dialector MySQL) Migrator(db *gorm.DB) gorm.Migrator { return MySQLMigrator{ - Migrator{ + Migrator: Migrator{ Migrator: migrator.Migrator{ Config: migrator.Config{ DB: db, @@ -205,13 +156,24 @@ func (m MySQLMigrator) ColumnTypes(value interface{}) ([]gorm.ColumnType, error) err := m.RunWithValue(value, func(stmt *gorm.Statement) error { var ( currentDatabase = m.DB.Migrator().CurrentDatabase() - columnTypeSQL = "SELECT column_name, is_nullable, data_type, character_maximum_length, numeric_precision, numeric_scale " + columnTypeSQL = "SELECT column_name, column_default, is_nullable = 'YES', data_type, character_maximum_length, column_type, column_key, extra, column_comment, numeric_precision, numeric_scale " + rows, err = m.DB.Session(&gorm.Session{}).Table(stmt.Table).Limit(1).Rows() ) - if !m.Dialector.(*MySQL).DisableDatetimePrecision { + if err != nil { + return err + } + + rawColumnTypes, err := rows.ColumnTypes() + + if err := rows.Close(); err != nil { + return err + } + + if !m.DisableDatetimePrecision { columnTypeSQL += ", datetime_precision " } - columnTypeSQL += "FROM information_schema.columns WHERE table_schema = ? AND table_name = ?" + columnTypeSQL += "FROM information_schema.columns WHERE table_schema = ? AND table_name = ? ORDER BY ORDINAL_POSITION" columns, rowErr := m.DB.Raw(columnTypeSQL, currentDatabase, stmt.Table).Rows() if rowErr != nil { @@ -221,17 +183,50 @@ func (m MySQLMigrator) ColumnTypes(value interface{}) ([]gorm.ColumnType, error) defer columns.Close() for columns.Next() { - var column MySQLColumn - var values = []interface{}{&column.name, &column.nullable, &column.datatype, - &column.maxLen, &column.precision, &column.scale} + var ( + column migrator.ColumnType + datetimePrecision sql.NullInt64 + extraValue sql.NullString + columnKey sql.NullString + values = []interface{}{ + &column.NameValue, &column.DefaultValueValue, &column.NullableValue, &column.DataTypeValue, &column.LengthValue, &column.ColumnTypeValue, &columnKey, &extraValue, &column.CommentValue, &column.DecimalSizeValue, &column.ScaleValue, + } + ) - if !m.Dialector.(*MySQL).DisableDatetimePrecision { - values = append(values, &column.datetimePrecision) + if !m.DisableDatetimePrecision { + values = append(values, &datetimePrecision) } if scanErr := columns.Scan(values...); scanErr != nil { return scanErr } + + column.PrimaryKeyValue = sql.NullBool{Bool: false, Valid: true} + column.UniqueValue = sql.NullBool{Bool: false, Valid: true} + switch columnKey.String { + case "PRI": + column.PrimaryKeyValue = sql.NullBool{Bool: true, Valid: true} + case "UNI": + column.UniqueValue = sql.NullBool{Bool: true, Valid: true} + } + + if strings.Contains(extraValue.String, "auto_increment") { + column.AutoIncrementValue = sql.NullBool{Bool: true, Valid: true} + } + + column.DefaultValueValue.String = strings.Trim(column.DefaultValueValue.String, "'") + + if datetimePrecision.Valid { + column.DecimalSizeValue = datetimePrecision + } + + for _, c := range rawColumnTypes { + if c.Name() == column.NameValue.String { + column.SQLColumnType = c + break + } + } + columnTypes = append(columnTypes, column) } diff --git a/projections/dialects/postgresql.go b/projections/dialects/postgresql.go index 44a661b0..7c02e593 100644 --- a/projections/dialects/postgresql.go +++ b/projections/dialects/postgresql.go @@ -3,6 +3,7 @@ package dialects import ( "database/sql" "fmt" + "regexp" "strings" "gorm.io/driver/postgres" @@ -36,61 +37,6 @@ func (dialector Postgres) Migrator(db *gorm.DB) gorm.Migrator { } } -type Column struct { - name string - nullable sql.NullString - datatype string - maxlen sql.NullInt64 - precision sql.NullInt64 - radix sql.NullInt64 - scale sql.NullInt64 - datetimeprecision sql.NullInt64 - typlen sql.NullInt64 -} - -func (c Column) Name() string { - return c.name -} - -func (c Column) DatabaseTypeName() string { - return c.datatype -} - -func (c Column) Length() (length int64, ok bool) { - ok = c.typlen.Valid - if ok && c.typlen.Int64 > 0 { - length = c.typlen.Int64 - } else { - ok = c.maxlen.Valid - if ok { - length = c.maxlen.Int64 - } else { - length = 0 - } - } - return -} - -func (c Column) Nullable() (nullable bool, ok bool) { - if c.nullable.Valid { - nullable, ok = c.nullable.String == "YES", true - } else { - nullable, ok = false, false - } - return -} - -func (c Column) DecimalSize() (precision int64, scale int64, ok bool) { - if ok = c.precision.Valid && c.scale.Valid && c.radix.Valid && c.radix.Int64 == 10; ok { - precision, scale = c.precision.Int64, c.scale.Int64 - } else if ok = c.datetimeprecision.Valid; ok { - precision, scale = c.datetimeprecision.Int64, 0 - } else { - precision, scale, ok = 0, 0, false - } - return -} - type PostgresMigrator struct { Migrator } @@ -331,40 +277,142 @@ func (m PostgresMigrator) HasConstraint(value interface{}, name string) bool { return count > 0 } +func (m PostgresMigrator) GetRows(currentSchema interface{}, table interface{}) (*sql.Rows, error) { + name := table.(string) + if currentSchema != nil { + if _, ok := currentSchema.(string); ok { + name = fmt.Sprintf("%v.%v", currentSchema, table) + } + } + return m.DB.Session(&gorm.Session{}).Table(name).Limit(1).Rows() +} + func (m PostgresMigrator) ColumnTypes(value interface{}) (columnTypes []gorm.ColumnType, err error) { columnTypes = make([]gorm.ColumnType, 0) err = m.RunWithValue(value, func(stmt *gorm.Statement) error { - currentDatabase := m.DB.Migrator().CurrentDatabase() - currentSchema, table := m.CurrentSchema(stmt, stmt.Table) - columns, err := m.DB.Raw( - "SELECT column_name, is_nullable, udt_name, character_maximum_length, "+ - "numeric_precision, numeric_precision_radix, numeric_scale, datetime_precision, 8 * typlen "+ - "FROM information_schema.columns AS cols JOIN pg_type AS pgt ON cols.udt_name = pgt.typname "+ - "WHERE table_catalog = ? AND table_schema = ? AND table_name = ?", - currentDatabase, currentSchema, table).Rows() + var ( + currentDatabase = m.DB.Migrator().CurrentDatabase() + currentSchema, table = m.CurrentSchema(stmt, stmt.Table) + columns, err = m.DB.Raw( + "SELECT c.column_name, c.is_nullable = 'YES', c.udt_name, c.character_maximum_length, c.numeric_precision, c.numeric_precision_radix, c.numeric_scale, c.datetime_precision, 8 * typlen, c.column_default, pd.description FROM information_schema.columns AS c JOIN pg_type AS pgt ON c.udt_name = pgt.typname LEFT JOIN pg_catalog.pg_description as pd ON pd.objsubid = c.ordinal_position AND pd.objoid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = c.table_name AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = c.table_schema)) where table_catalog = ? AND table_schema = ? AND table_name = ?", + currentDatabase, currentSchema, table).Rows() + ) + if err != nil { return err } - defer columns.Close() for columns.Next() { - var column Column + var ( + column = &migrator.ColumnType{ + PrimaryKeyValue: sql.NullBool{Valid: true}, + UniqueValue: sql.NullBool{Valid: true}, + } + datetimePrecision sql.NullInt64 + radixValue sql.NullInt64 + typeLenValue sql.NullInt64 + ) + err = columns.Scan( - &column.name, - &column.nullable, - &column.datatype, - &column.maxlen, - &column.precision, - &column.radix, - &column.scale, - &column.datetimeprecision, - &column.typlen, + &column.NameValue, &column.NullableValue, &column.DataTypeValue, &column.LengthValue, &column.DecimalSizeValue, + &radixValue, &column.ScaleValue, &datetimePrecision, &typeLenValue, &column.DefaultValueValue, &column.CommentValue, ) if err != nil { return err } + + if typeLenValue.Valid && typeLenValue.Int64 > 0 { + column.LengthValue = typeLenValue + } + + if strings.HasPrefix(column.DefaultValueValue.String, "nextval('") && strings.HasSuffix(column.DefaultValueValue.String, "seq'::regclass)") { + column.AutoIncrementValue = sql.NullBool{Bool: true, Valid: true} + column.DefaultValueValue = sql.NullString{} + } + + if column.DefaultValueValue.Valid { + column.DefaultValueValue.String = regexp.MustCompile("'(.*)'::[\\w]+$").ReplaceAllString(column.DefaultValueValue.String, "$1") + } + + if datetimePrecision.Valid { + column.DecimalSizeValue = datetimePrecision + } + columnTypes = append(columnTypes, column) } + columns.Close() + + // assign sql column type + { + rows, rowsErr := m.GetRows(currentSchema, table) + if rowsErr != nil { + return rowsErr + } + rawColumnTypes, err := rows.ColumnTypes() + if err != nil { + return err + } + for _, columnType := range columnTypes { + for _, c := range rawColumnTypes { + if c.Name() == columnType.Name() { + columnType.(*migrator.ColumnType).SQLColumnType = c + break + } + } + } + rows.Close() + } + + // check primary, unique field + { + columnTypeRows, err := m.DB.Raw("SELECT c.column_name, constraint_type FROM information_schema.table_constraints tc JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_name) JOIN information_schema.columns AS c ON c.table_schema = tc.constraint_schema AND tc.table_name = c.table_name AND ccu.column_name = c.column_name WHERE constraint_type IN ('PRIMARY KEY', 'UNIQUE') AND c.table_catalog = ? AND c.table_schema = ? AND c.table_name = ?", currentDatabase, currentSchema, table).Rows() + if err != nil { + return err + } + + for columnTypeRows.Next() { + var name, columnType string + columnTypeRows.Scan(&name, &columnType) + for _, c := range columnTypes { + mc := c.(*migrator.ColumnType) + if mc.NameValue.String == name { + switch columnType { + case "PRIMARY KEY": + mc.PrimaryKeyValue = sql.NullBool{Bool: true, Valid: true} + case "UNIQUE": + mc.UniqueValue = sql.NullBool{Bool: true, Valid: true} + } + break + } + } + } + columnTypeRows.Close() + } + + // check column type + { + dataTypeRows, err := m.DB.Raw(`SELECT a.attname as column_name, format_type(a.atttypid, a.atttypmod) AS data_type + FROM pg_attribute a JOIN pg_class b ON a.attrelid = b.relfilenode AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = ?) + WHERE a.attnum > 0 -- hide internal columns + AND NOT a.attisdropped -- hide deleted columns + AND b.relname = ?`, currentSchema, table).Rows() + if err != nil { + return err + } + + for dataTypeRows.Next() { + var name, dataType string + dataTypeRows.Scan(&name, &dataType) + for _, c := range columnTypes { + mc := c.(*migrator.ColumnType) + if mc.NameValue.String == name { + mc.ColumnTypeValue = sql.NullString{String: dataType, Valid: true} + break + } + } + } + dataTypeRows.Close() + } return err }) diff --git a/projections/projections_test.go b/projections/projections_test.go index 34f6aad5..a8bcdd13 100644 --- a/projections/projections_test.go +++ b/projections/projections_test.go @@ -1940,9 +1940,11 @@ components: } if c.Name() == "title" { found1 = true - nullable, _ := c.Nullable() - if nullable { - t.Errorf("expected the title field to be NOT nullable") + if gormDB.Dialector.Name() != "sqlite" { // for the nullable check to work the sql driver needs to support it + nullable, _ := c.Nullable() + if nullable { + t.Errorf("expected the title field to be NOT nullable") + } } } if c.Name() == "description" { From 6555162943172b563de966817293bc5add129521 Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Tue, 21 Jun 2022 16:32:02 -0400 Subject: [PATCH 08/18] feature: WEOS-1519 Setup permission configuration * Added scope checking * Added role to context * Added permission checking using the casbin enforcer --- context/context.go | 1 + controllers/rest/api.go | 1 + controllers/rest/fixtures/blog-security.yaml | 8 +- controllers/rest/operation_initializers.go | 19 ++- controllers/rest/security.go | 63 ++++++++- controllers/rest/security_test.go | 137 ++++++++++++++++++- end2end_test.go | 4 +- features/security-schemes.feature | 14 +- 8 files changed, 227 insertions(+), 20 deletions(-) diff --git a/context/context.go b/context/context.go index 6da66e49..fda451a5 100644 --- a/context/context.go +++ b/context/context.go @@ -16,6 +16,7 @@ const HeaderXLogLevel = "X-LOG-LEVEL" const ACCOUNT_ID ContextKey = "ACCOUNT_ID" const OPERATION_ID = "OPERATION_ID" const USER_ID ContextKey = "USER_ID" +const ROLE ContextKey = "ROLE" const LOG_LEVEL ContextKey = "LOG_LEVEL" const REQUEST_ID ContextKey = "REQUEST_ID" const WEOS_ID ContextKey = "WEOS_ID" diff --git a/controllers/rest/api.go b/controllers/rest/api.go index 99fcd2a9..98811ec7 100644 --- a/controllers/rest/api.go +++ b/controllers/rest/api.go @@ -469,6 +469,7 @@ func (p *RESTAPI) Initialize(ctxt context.Context) error { p.RegisterOperationInitializer(ContentTypeResponseInitializer) p.RegisterOperationInitializer(EntityFactoryInitializer) p.RegisterOperationInitializer(UserDefinedInitializer) + p.RegisterOperationInitializer(AuthorizationInitializer) p.RegisterOperationInitializer(StandardInitializer) p.RegisterOperationInitializer(RouteInitializer) //register standard post path initializers diff --git a/controllers/rest/fixtures/blog-security.yaml b/controllers/rest/fixtures/blog-security.yaml index 590f3113..1655077d 100644 --- a/controllers/rest/fixtures/blog-security.yaml +++ b/controllers/rest/fixtures/blog-security.yaml @@ -133,7 +133,7 @@ components: format: date-time nullable: true security: - - Auth0: ["email","name"] + - Auth0: ["email"] paths: /health: @@ -328,7 +328,7 @@ paths: summary: Get Blog by id operationId: Get Blog security: - - Auth02: ["email"] + - Auth0: ["name"] responses: 200: description: Blog details without any supporting collections @@ -702,6 +702,10 @@ paths: format: email required: true summary: Update Author details + x-auth: + allow: + users: + - auth0|1234 requestBody: required: true content: diff --git a/controllers/rest/operation_initializers.go b/controllers/rest/operation_initializers.go index 0b7c41af..f73a2246 100644 --- a/controllers/rest/operation_initializers.go +++ b/controllers/rest/operation_initializers.go @@ -79,14 +79,25 @@ m = r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act) } //add rule to the enforcer based on the operation var authConfig map[string]interface{} - if err = json.Unmarshal(authRaw.(json.RawMessage), &authConfig); err != nil { + if err = json.Unmarshal(authRaw.(json.RawMessage), &authConfig); err == nil { if allowRules, ok := authConfig["allow"]; ok { + //setup users if u, ok := allowRules.(map[string]interface{})["users"]; ok { - for _, user := range u.([]string) { + for _, user := range u.([]interface{}) { var success bool - success, err = enforcer.AddPolicy(user, path, method) + success, err = enforcer.AddPolicy(user.(string), path, method) if !success { - + //TODO show warning to developer or something + } + } + } + //setup roles + if u, ok := allowRules.(map[string]interface{})["roles"]; ok { + for _, user := range u.([]interface{}) { + var success bool + success, err = enforcer.AddPolicy(user.(string), path, method) + if !success { + //TODO show warning to developer or something } } } diff --git a/controllers/rest/security.go b/controllers/rest/security.go index c9c00524..075963e9 100644 --- a/controllers/rest/security.go +++ b/controllers/rest/security.go @@ -3,13 +3,16 @@ package rest import ( "context" "fmt" + "github.com/coreos/go-oidc/v3/oidc" "github.com/getkin/kin-openapi/openapi3" + "github.com/golang-jwt/jwt/v4" "github.com/labstack/echo/v4" "github.com/labstack/gommon/log" context2 "github.com/wepala/weos/context" "github.com/wepala/weos/model" "github.com/wepala/weos/projections" "net/http" + "strings" ) //Validator interface that must be implemented so that a request can be authenticated @@ -73,15 +76,67 @@ func (s *SecurityConfiguration) Middleware(api Container, projection projections } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(ctxt echo.Context) error { + var success bool + var err error + var userID string + var ttoken interface{} //parsed token + var role string //loop through the validators and go to the next middleware when one authenticates otherwise return 403 for _, validator := range validators { - var success bool - var err error - var userID string - if success, _, userID, _, err = validator.Validate(ctxt); success { + if success, ttoken, userID, role, err = validator.Validate(ctxt); success { newContext := context.WithValue(ctxt.Request().Context(), context2.USER_ID, userID) + newContext = context.WithValue(newContext, context2.ROLE, role) request := ctxt.Request().WithContext(newContext) ctxt.SetRequest(request) + //check the scopes of the logged-in user against what is required and if the user doesn't have the required scope deny access + for _, securityScheme := range securitySchemes { + for _, scopes := range securityScheme { + for _, scope := range scopes { + //account for the different token types that could be returned + switch t := ttoken.(type) { + case *oidc.IDToken: + claims := make(map[string]interface{}) + err = t.Claims(&claims) + if err != nil { + ctxt.Logger().Debugf("invalid claims '%s'", err) + } + if _, ok := claims["scope"]; !ok { + ctxt.Logger().Debug("token from issuer '%s' does not have scopes", t.Issuer) + return ctxt.NoContent(http.StatusForbidden) + } + //if the required scope is not in the user scope + if !strings.Contains(claims["scope"].(string), scope) { + ctxt.Logger().Debug("token from issuer '%s' does not have required scope '%s'", t.Issuer, scope) + return ctxt.NoContent(http.StatusForbidden) + } + case *jwt.Token: + if claims, ok := t.Claims.(jwt.MapClaims); ok { + if _, ok := claims["scope"]; !ok { + ctxt.Logger().Debug("token from issuer '%s' does not have scopes", claims["iss"]) + return ctxt.NoContent(http.StatusForbidden) + } + //if the required scope is not in the user scope + if !strings.Contains(claims["scope"].(string), scope) { + ctxt.Logger().Debug("token from issuer '%s' does not have required scope '%s'", claims["iss"], scope) + return ctxt.NoContent(http.StatusForbidden) + } + } + } + } + } + } + //check permissions to ensure the user can access this endpoint + if enforcer, err := api.GetPermissionEnforcer("Default"); err == nil { + success, err = enforcer.Enforce(userID, ctxt.Request().URL.Path, ctxt.Request().Method) + //fmt.Printf("explanations %v", explanations) + if err != nil { + ctxt.Logger().Errorf("error looking up permissions '%s'", err) + } + if success { + return next(ctxt) + } + return ctxt.NoContent(http.StatusForbidden) + } return next(ctxt) } ctxt.Logger().Debugf("error authenticating '%s'", err) diff --git a/controllers/rest/security_test.go b/controllers/rest/security_test.go index 8816b61c..45937156 100644 --- a/controllers/rest/security_test.go +++ b/controllers/rest/security_test.go @@ -1,12 +1,16 @@ package rest_test import ( + "github.com/casbin/casbin/v2" + casbinmodel "github.com/casbin/casbin/v2/model" "github.com/getkin/kin-openapi/openapi3" "github.com/labstack/echo/v4" "github.com/wepala/weos/controllers/rest" "github.com/wepala/weos/model" + "golang.org/x/net/context" "net/http" "net/http/httptest" + "os" "testing" ) @@ -49,7 +53,7 @@ func TestSecurityConfiguration_Middleware(t *testing.T) { if err != nil { t.Fatalf("unexpected error setting up security configuration '%s'", err) } - //set mock authenticator so we can check that is was set and called + //set mock authenticator, so we can check that it was set and called mockAuthenticator := &ValidatorMock{ValidateFunc: func(ctxt echo.Context) (bool, interface{}, string, string, error) { return true, nil, "", "", nil }} @@ -73,6 +77,23 @@ func TestSecurityConfiguration_Middleware(t *testing.T) { GetConfigFunc: func() *openapi3.Swagger { return swagger }, + GetPermissionEnforcerFunc: func(name string) (*casbin.Enforcer, error) { + //default REST permission model + text := `[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act) +` + m, _ := casbinmodel.NewModelFromString(text) + return casbin.NewEnforcer(m, "./fixtures/permissions.csv") + }, } mw := config.Middleware(container, &ProjectionMock{}, &CommandDispatcherMock{}, &EventRepositoryMock{}, &EntityFactoryMock{}, path, path.Post) nextMiddlewareHit := false @@ -95,6 +116,120 @@ func TestSecurityConfiguration_Middleware(t *testing.T) { t.Errorf("expected the next middleware to be hit") } }) + t.Run("invalid scopes", func(t *testing.T) { + api, err := rest.New("./fixtures/blog-security.yaml") + if err != nil { + t.Fatalf("unexpected error loading api config '%s'", err) + } + swagger := api.Swagger + config, err := new(rest.SecurityConfiguration).FromSchema(swagger.Components.SecuritySchemes) + if err != nil { + t.Fatalf("unexpected error setting up security configuration '%s'", err) + } + api.RegisterSecurityConfiguration(config) + path := swagger.Paths.Find("/blogs/{id}") + mw := config.Middleware(api, &ProjectionMock{}, &CommandDispatcherMock{}, &EventRepositoryMock{}, &EntityFactoryMock{}, path, path.Get) + handler := mw(func(context echo.Context) error { + return nil + }) + e := echo.New() + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/blogs/1234", nil) + token := os.Getenv("OAUTH_TEST_KEY") + req.Header.Add("Authorization", "Bearer "+token) + e.GET("/blogs/1234", handler) + e.ServeHTTP(resp, req) + + if resp.Result().StatusCode != http.StatusForbidden { + t.Errorf("expected the response to be %d, got %d", http.StatusForbidden, resp.Result().StatusCode) + } + }) + + t.Run("allowed user access", func(t *testing.T) { + api, err := rest.New("./fixtures/blog-security.yaml") + if err != nil { + t.Fatalf("unexpected error loading api config '%s'", err) + } + swagger := api.Swagger + err = api.Initialize(context.TODO()) + if err != nil { + t.Fatalf("error initializing api") + } + config := api.GetSecurityConfiguration() + path := swagger.Paths.Find("/blogs") + mw := config.Middleware(api, &ProjectionMock{}, &CommandDispatcherMock{}, &EventRepositoryMock{}, &EntityFactoryMock{}, path, path.Get) + handler := mw(func(context echo.Context) error { + return nil + }) + e := echo.New() + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/blogs", nil) + token := os.Getenv("OAUTH_TEST_KEY") + req.Header.Add("Authorization", "Bearer "+token) + e.POST("/blogs", handler) + e.ServeHTTP(resp, req) + + if resp.Result().StatusCode != http.StatusOK { + t.Errorf("expected the response to be %d, got %d", http.StatusOK, resp.Result().StatusCode) + } + }) + t.Run("allowed role access", func(t *testing.T) { + api, err := rest.New("./fixtures/blog-security.yaml") + if err != nil { + t.Fatalf("unexpected error loading api config '%s'", err) + } + swagger := api.Swagger + err = api.Initialize(context.TODO()) + if err != nil { + t.Fatalf("error initializing api") + } + config := api.GetSecurityConfiguration() + path := swagger.Paths.Find("/blogs") + mw := config.Middleware(api, &ProjectionMock{}, &CommandDispatcherMock{}, &EventRepositoryMock{}, &EntityFactoryMock{}, path, path.Get) + handler := mw(func(context echo.Context) error { + return nil + }) + e := echo.New() + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/blogs", nil) + token := os.Getenv("OAUTH_TEST_KEY") + req.Header.Add("Authorization", "Bearer "+token) + e.POST("/blogs", handler) + e.ServeHTTP(resp, req) + + if resp.Result().StatusCode != http.StatusOK { + t.Errorf("expected the response to be %d, got %d", http.StatusOK, resp.Result().StatusCode) + } + }) + + t.Run("unauthorized access", func(t *testing.T) { + api, err := rest.New("./fixtures/blog-security.yaml") + if err != nil { + t.Fatalf("unexpected error loading api config '%s'", err) + } + swagger := api.Swagger + err = api.Initialize(context.TODO()) + if err != nil { + t.Fatalf("error initializing api") + } + config := api.GetSecurityConfiguration() + path := swagger.Paths.Find("/authors/{id}") + mw := config.Middleware(api, &ProjectionMock{}, &CommandDispatcherMock{}, &EventRepositoryMock{}, &EntityFactoryMock{}, path, path.Get) + handler := mw(func(context echo.Context) error { + return nil + }) + e := echo.New() + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/authors/1234", nil) + token := os.Getenv("OAUTH_TEST_KEY") + req.Header.Add("Authorization", "Bearer "+token) + e.GET("/authors/1234", handler) + e.ServeHTTP(resp, req) + + if resp.Result().StatusCode != http.StatusForbidden { + t.Errorf("expected the response to be %d, got %d", http.StatusForbidden, resp.Result().StatusCode) + } + }) } //func TestSecurityConfiguration_SetDefaultSecurity(t *testing.T) { diff --git a/end2end_test.go b/end2end_test.go index f23b8592..8f02c2d6 100644 --- a/end2end_test.go +++ b/end2end_test.go @@ -2143,9 +2143,9 @@ func TestBDD(t *testing.T) { TestSuiteInitializer: InitializeSuite, Options: &godog.Options{ Format: "pretty", - //Tags: "~long && ~skipped", + Tags: "~long && ~skipped", //Tags: "WEOS-1378", - Tags: "WEOS-1343 && ~skipped", + //Tags: "WEOS-1519 && ~skipped", }, }.Run() if status != 0 { diff --git a/features/security-schemes.feature b/features/security-schemes.feature index 3c12e2f6..11884e3e 100644 --- a/features/security-schemes.feature +++ b/features/security-schemes.feature @@ -118,10 +118,7 @@ Feature: Use OpenAPI Security Scheme to protect endpoints name: Authorization schema: type: string - x-auth: - allow: - users: - - auth0|1234 + security: [] requestBody: description: Blog info that is submitted required: true @@ -250,6 +247,9 @@ Feature: Use OpenAPI Security Scheme to protect endpoints schema: $ref: "#/components/schemas/Blog" x-auth: + allow: + users: + - auth0|1234 deny: roles: - Y9IvGucEhViFd58GL0bBoNrgEk3ohW88 @@ -690,7 +690,7 @@ Feature: Use OpenAPI Security Scheme to protect endpoints When the "OpenAPI 3.0" specification is parsed Then an error should be returned - @WEOS-1519 @skipped + @WEOS-1519 Scenario: User Denied based on id not being in the allow list In order to support JWT from different authentication services, the developer should be able to specify which part of @@ -705,7 +705,7 @@ Feature: Use OpenAPI Security Scheme to protect endpoints Then a 403 response should be returned - @WEOS-1519 @skipped + @WEOS-1519 Scenario: User Allowed based on the role being on the allow list In order to support JWT from different authentication services, the developer should be able to specify which part of @@ -716,7 +716,7 @@ Feature: Use OpenAPI Security Scheme to protect endpoints Then a 200 response should be returned - @WEOS-1519 @skipped + @WEOS-1519 Scenario: User denied based on the role being on the deny list Given "Sojourner" is on the "Blog" edit screen with id "1234" From cf5e1cd649a90bf8b7aefd3f9c7364d002b30a3d Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Thu, 23 Jun 2022 14:29:10 -0400 Subject: [PATCH 09/18] feature: FHIR api fixes * Made it so that a gorm model is only built if there is a ref on the "Items" definition * Started setting up item array fix (in progress) --- controllers/rest/fixtures/fhir.yaml | 359 ++++++++++++++++++++++++++++ integration_test.go | 38 +++ projections/gorm.go | 105 +++++++- 3 files changed, 494 insertions(+), 8 deletions(-) create mode 100644 controllers/rest/fixtures/fhir.yaml diff --git a/controllers/rest/fixtures/fhir.yaml b/controllers/rest/fixtures/fhir.yaml new file mode 100644 index 00000000..3c41eb5d --- /dev/null +++ b/controllers/rest/fixtures/fhir.yaml @@ -0,0 +1,359 @@ +openapi: 3.0.3 +info: + title: Wepala FHIR + description: Fast Healthcare Interoperability Resources (FHIR) API. + version: 1.0.0 +servers: + - url: 'https://prod1.weos.sh/fhir' +x-weos-config: + basePath: + database: + driver: sqlite3 + database: test.db +components: + schemas: + Address: + type: object + properties: + use: + type: string + enum: + - home + - work + - temp + - old + - billing + type: + type: string + enum: + - postal + - physical + - both + text: + type: string + nullable: true + description: Text representation of the address + line: + type: string + nullable: true + description: Street name, number, direction & P.O. Box etc. + city: + type: string + nullable: true + description: Name of city, town etc. + district: + type: string + nullable: true + description: District name (aka county) + state: + type: string + nullable: true + description: Sub-unit of country (abbreviations ok) + postalCode: + type: string + nullable: true + description: Postal code for area + country: + type: string + nullable: true + period: + $ref: "#/components/schemas/Period" + Appointment: + type: object + properties: + identifier: + type: array + items: + $ref: "#/components/schemas/Identifier" + status: + type: string + enum: + - proposed + - pending + - booked + - arrived + - fulfilled + - cancelled + - noshow + - entered-in-error + - checked-in + - waitlist + cancelationReason: + $ref: "#/components/schemas/CodeableConcept" + serviceCategory: + type: array + items: + $ref: "#/components/schemas/CodeableConcept" + description: A broad categorization of the service that is to be performed during this appointment + participant: + type: object + properties: + type: + type: array + nullable: true + items: + $ref: "#/components/schemas/CodeableConcept" + actor: + $ref: "#/components/schemas/Patient" + required: + type: string + nullable: true + status: + type: string + period: + $ref: "#/components/schemas/Period" + required: + - participant + + + Attachment: + type: object + properties: + contentType: + type: string + language: + type: string + nullable: true + data: + type: string + nullable: true + url: + type: string + format: url + description: Uri where the data can be found + required: + - contentType + CodeableConcept: + type: object + properties: + coding: + type: array + nullable: true + items: + $ref: "#/components/schemas/Coding" + text: + type: string + nullable: true + example: + coding: + - system: test + Coding: + type: object + properties: + system: + type: string + format: uri + nullable: true + description: Identity of the terminology system + version: + type: string + nullable: true + description: Version of the system - if relevant + code: + type: string + description: Symbol in syntax defined by the system + nullable: true + display: + type: string + description: Representation defined by the system + nullable: true + userSelected: + type: boolean + description: If this coding was chosen directly by the user + nullable: true + ContactPoint: + type: object + properties: + system: + type: string + enum: + - phone + - fax + - email + - pager + - url + - sms + - other + value: + type: string + nullable: true + use: + type: string + enum: + - home + - work + - temp + - old + - mobile + rank: + type: integer + format: uint + nullable: true + period: + $ref: "#/components/schemas/Period" + required: + - system + - use + Identifier: + type: object + properties: + use: + type: string + enum: + - usual + - official + - temp + - secondary + - old + type: + $ref: "#/components/schemas/CodeableConcept" + system: + type: string + format: uri + description: | + The system is a URI that defines a set of identifiers (i.e. how the value is made unique). It might be a + specific application or a recognized standard/specification for a set of identifiers or a way of making + identifiers unique. + + It is up to the implementer organization to determine an appropriate URL or URN structure that will avoid + collisions and to manage that space (and the resolvability of URLs) over time. + + Note that the scope of a given identifier system may extend beyond identifiers that might be captured by a + single resource. For example, some systems might draw all "order" identifiers from a single namespace, + though some might be used on MedicationRequest while others would appear on ServiceRequest. + + If the identifier value itself is naturally a globally unique URI (e.g. an OID, a UUID, or a URI with no + trailing local part), then the system SHALL be "urn:ietf:rfc:3986", and the URI is in the value (OIDs and + UUIDs using urn:oid: and urn:uuid: + + Naturally globally unique identifiers are those for which no system has been assigned and where the value of the identifier is reasonably expected to not be re-used. Typically, these are absolute URIs of some kind. + + In some cases, the system might not be known - only the value is known (e.g. a simple device that scans a + barcode), or the system is known implicitly (simple exchange in a limited context, often driven by barcode + readers). In this case, no useful matching may be performed using the value unless the system can be safely + inferred by the context. Applications should provide a system wherever possible, as information sharing in a + wider context is very likely to arise eventually, and values without a system are inherently limited in use. + value: + type: string + nullable: true + period: + $ref: "#/components/schemas/Period" + assigner: + $ref: "#/components/schemas/Organization" + required: + - use + Organization: + type: object + Period: + type: object + properties: + start: + type: string + format: date-time + nullable: true + end: + type: string + format: date-time + nullable: true + Patient: + type: object + properties: + identifier: + type: array + items: + type: string + name: + type: array + items: + type: string + nullable: true + active: + type: boolean + telecom: + type: array + items: + $ref: "#/components/schemas/ContactPoint" + gender: + type: string + enum: + - male + - female + - other + - unknown + birthDate: + type: string + format: date-time + nullable: true + deceased: + type: boolean + additionalProperties: + type: string + nullable: true + address: + type: array + items: + $ref: "#/components/schemas/Address" + nullable: false + maritalStatus: + $ref: "#/components/schemas/CodeableConcept" + multipleBirth: + type: boolean + nullable: true + additionalProperties: + type: integer + photo: + type: array + nullable: true + items: + $ref: "#/components/schemas/Attachment" + contact: + type: array + items: + type: object + properties: + relationship: + $ref: "#/components/schemas/CodeableConcept" + name: + type: string + nullable: true + telecom: + type: array + items: + $ref: "#/components/schemas/ContactPoint" + address: + $ref: "#/components/schemas/Address" + gender: + type: string + organization: + $ref: "#/components/schemas/Organization" + period: + $ref: "#/components/schemas/Period" + communication: + type: array + items: + type: object + properties: + language: + $ref: "#/components/schemas/CodeableConcept" + preferred: + type: boolean + description: A language which may be used to communicate with the patient about his or her health + generalPractitioner: + type: array + items: + $ref: "#/components/schemas/Organization" + nullable: true + managingOrganization: + $ref: "#/components/schemas/Organization" + + +paths: + /health: + get: + responses: + 200: + description: Health Endpoint + content: + text/html: + example: | + Health \ No newline at end of file diff --git a/integration_test.go b/integration_test.go index 51b76265..ac7ed93b 100644 --- a/integration_test.go +++ b/integration_test.go @@ -602,3 +602,41 @@ func TestIntegration_FilteringByCamelCase(t *testing.T) { }) } + +func TestIntegration_FHIR(t *testing.T) { + //dropDB() + content, err := ioutil.ReadFile("./controllers/rest/fixtures/fhir.yaml") + if err != nil { + t.Fatal(err) + } + contentString := string(content) + contentString = fmt.Sprintf(contentString, dbconfig.Database, dbconfig.Driver, dbconfig.Host, dbconfig.Password, dbconfig.User, dbconfig.Port) + + tapi, err := api.New(contentString) + if err != nil { + t.Fatalf("un expected error loading spec '%s'", err) + } + err = tapi.Initialize(context.TODO()) + if err != nil { + t.Fatalf("un expected error loading spec '%s'", err) + } + //TODO check that the tables are created correctly + gormDBconnection, err := tapi.GetGormDBConnection("Default") + if !gormDBconnection.Migrator().HasTable("Patient") { + t.Fatalf("expected there to be an patient table") + } + + //expectedColumns := []string{"gender"} + //columns, _ := gormDB.Migrator().ColumnTypes("Patient") + //actualColumns := make([]string, len(columns)) + //for k, column := range columns { + // actualColumns[k] = column.Name() + //} + // + //for _, expectedColumn := range expectedColumns { + // if !model.InList(actualColumns, expectedColumn) { + // t.Errorf("expected the column '%s' to exist", expectedColumn) + // } + //} + //dropDB() +} diff --git a/projections/gorm.go b/projections/gorm.go index 2f63be4e..26c51dd8 100644 --- a/projections/gorm.go +++ b/projections/gorm.go @@ -423,16 +423,105 @@ func (p *GORMDB) GORMPropertyDefaultValue(parentName string, name string, schema } case "array": if schema.Value != nil && schema.Value.Items != nil && schema.Value.Items.Value != nil && depth < 3 { - tbuilder, _, err := p.GORMModelBuilder(strings.Replace(schema.Value.Items.Ref, "#/components/schemas/", "", -1), schema.Value.Items.Value, depth+1) - if err != nil { - return nil, nil, nil - } - defaultValue = tbuilder.Build().NewSliceOfStructs() - json.Unmarshal([]byte(`[{ + if schema.Value.Items.Ref != "" { + tbuilder, _, err := p.GORMModelBuilder(strings.Replace(schema.Value.Items.Ref, "#/components/schemas/", "", -1), schema.Value.Items.Value, depth+1) + if err != nil { + return nil, nil, nil + } + defaultValue = tbuilder.Build().NewSliceOfStructs() + json.Unmarshal([]byte(`[{ "table_alias": "`+strings.Title(name)+`" }]`), &defaultValue) - //setup gorm field tag string - gormParts = append(gormParts, "many2many:"+utils.SnakeCase(parentName)+"_"+utils.SnakeCase(name)) + //setup gorm field tag string + gormParts = append(gormParts, "many2many:"+utils.SnakeCase(parentName)+"_"+utils.SnakeCase(name)) + } else if schema.Value.Items.Value.Type != "" { + switch schema.Value.Items.Value.Type { //TODO there must be a better way but too bushed now to find it + //case "integer": + // switch schema.Value.Format { + // case "int32": + // if schema.Value.Nullable { + // var value []*int32 + // defaultValue = value + // } else { + // var value []int32 + // defaultValue = value + // } + // case "int64": + // if schema.Value.Nullable { + // var value []*int64 + // defaultValue = value + // } else { + // var value []int64 + // defaultValue = value + // } + // case "uint": + // if schema.Value.Nullable { + // var value []*uint + // defaultValue = value + // } else { + // var value []uint + // defaultValue = value + // } + // default: + // if schema.Value.Nullable { + // var value []*int + // defaultValue = value + // } else { + // var value []int + // defaultValue = value + // } + // } + //case "number": + // switch schema.Value.Format { + // case "float32": + // if schema.Value.Nullable { + // var value []*float32 + // defaultValue = value + // } else { + // var value []float32 + // defaultValue = value + // } + // case "float64": + // if schema.Value.Nullable { + // var value []*float64 + // defaultValue = value + // } else { + // var value []float64 + // defaultValue = value + // } + // default: + // if schema.Value.Nullable { + // var value []*float32 + // defaultValue = value + // } else { + // var value []float32 + // defaultValue = value + // } + // } + //case "boolean": + // if schema.Value.Nullable { + // var value []*bool + // defaultValue = value + // } else { + // var value []bool + // defaultValue = value + // } + case "string": + switch schema.Value.Format { + case "date-time": + timeNow := weos.NewTime(time.Now()) + defaultValue = &timeNow + default: + if schema.Value.Nullable { + var strings *string + defaultValue = strings + } else { + var strings string + defaultValue = strings + } + } + } + } } default: //Belongs to https://gorm.io/docs/belongs_to.html From 233eee0746b4bab58977eeb0be867bea3c3b9e64 Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Fri, 24 Jun 2022 07:03:30 -0400 Subject: [PATCH 10/18] feature: FHIR api fixes * Added code to parse inline arrays (i.e arrays without schema references) * Added the ability parse inline objects (objects without schema references) --- projections/fixtures/complex-spec.yaml | 359 +++++++++++++++++++++++++ projections/gorm.go | 139 ++++------ projections/gorm_test.go | 56 ++++ 3 files changed, 462 insertions(+), 92 deletions(-) create mode 100644 projections/fixtures/complex-spec.yaml diff --git a/projections/fixtures/complex-spec.yaml b/projections/fixtures/complex-spec.yaml new file mode 100644 index 00000000..3c41eb5d --- /dev/null +++ b/projections/fixtures/complex-spec.yaml @@ -0,0 +1,359 @@ +openapi: 3.0.3 +info: + title: Wepala FHIR + description: Fast Healthcare Interoperability Resources (FHIR) API. + version: 1.0.0 +servers: + - url: 'https://prod1.weos.sh/fhir' +x-weos-config: + basePath: + database: + driver: sqlite3 + database: test.db +components: + schemas: + Address: + type: object + properties: + use: + type: string + enum: + - home + - work + - temp + - old + - billing + type: + type: string + enum: + - postal + - physical + - both + text: + type: string + nullable: true + description: Text representation of the address + line: + type: string + nullable: true + description: Street name, number, direction & P.O. Box etc. + city: + type: string + nullable: true + description: Name of city, town etc. + district: + type: string + nullable: true + description: District name (aka county) + state: + type: string + nullable: true + description: Sub-unit of country (abbreviations ok) + postalCode: + type: string + nullable: true + description: Postal code for area + country: + type: string + nullable: true + period: + $ref: "#/components/schemas/Period" + Appointment: + type: object + properties: + identifier: + type: array + items: + $ref: "#/components/schemas/Identifier" + status: + type: string + enum: + - proposed + - pending + - booked + - arrived + - fulfilled + - cancelled + - noshow + - entered-in-error + - checked-in + - waitlist + cancelationReason: + $ref: "#/components/schemas/CodeableConcept" + serviceCategory: + type: array + items: + $ref: "#/components/schemas/CodeableConcept" + description: A broad categorization of the service that is to be performed during this appointment + participant: + type: object + properties: + type: + type: array + nullable: true + items: + $ref: "#/components/schemas/CodeableConcept" + actor: + $ref: "#/components/schemas/Patient" + required: + type: string + nullable: true + status: + type: string + period: + $ref: "#/components/schemas/Period" + required: + - participant + + + Attachment: + type: object + properties: + contentType: + type: string + language: + type: string + nullable: true + data: + type: string + nullable: true + url: + type: string + format: url + description: Uri where the data can be found + required: + - contentType + CodeableConcept: + type: object + properties: + coding: + type: array + nullable: true + items: + $ref: "#/components/schemas/Coding" + text: + type: string + nullable: true + example: + coding: + - system: test + Coding: + type: object + properties: + system: + type: string + format: uri + nullable: true + description: Identity of the terminology system + version: + type: string + nullable: true + description: Version of the system - if relevant + code: + type: string + description: Symbol in syntax defined by the system + nullable: true + display: + type: string + description: Representation defined by the system + nullable: true + userSelected: + type: boolean + description: If this coding was chosen directly by the user + nullable: true + ContactPoint: + type: object + properties: + system: + type: string + enum: + - phone + - fax + - email + - pager + - url + - sms + - other + value: + type: string + nullable: true + use: + type: string + enum: + - home + - work + - temp + - old + - mobile + rank: + type: integer + format: uint + nullable: true + period: + $ref: "#/components/schemas/Period" + required: + - system + - use + Identifier: + type: object + properties: + use: + type: string + enum: + - usual + - official + - temp + - secondary + - old + type: + $ref: "#/components/schemas/CodeableConcept" + system: + type: string + format: uri + description: | + The system is a URI that defines a set of identifiers (i.e. how the value is made unique). It might be a + specific application or a recognized standard/specification for a set of identifiers or a way of making + identifiers unique. + + It is up to the implementer organization to determine an appropriate URL or URN structure that will avoid + collisions and to manage that space (and the resolvability of URLs) over time. + + Note that the scope of a given identifier system may extend beyond identifiers that might be captured by a + single resource. For example, some systems might draw all "order" identifiers from a single namespace, + though some might be used on MedicationRequest while others would appear on ServiceRequest. + + If the identifier value itself is naturally a globally unique URI (e.g. an OID, a UUID, or a URI with no + trailing local part), then the system SHALL be "urn:ietf:rfc:3986", and the URI is in the value (OIDs and + UUIDs using urn:oid: and urn:uuid: + + Naturally globally unique identifiers are those for which no system has been assigned and where the value of the identifier is reasonably expected to not be re-used. Typically, these are absolute URIs of some kind. + + In some cases, the system might not be known - only the value is known (e.g. a simple device that scans a + barcode), or the system is known implicitly (simple exchange in a limited context, often driven by barcode + readers). In this case, no useful matching may be performed using the value unless the system can be safely + inferred by the context. Applications should provide a system wherever possible, as information sharing in a + wider context is very likely to arise eventually, and values without a system are inherently limited in use. + value: + type: string + nullable: true + period: + $ref: "#/components/schemas/Period" + assigner: + $ref: "#/components/schemas/Organization" + required: + - use + Organization: + type: object + Period: + type: object + properties: + start: + type: string + format: date-time + nullable: true + end: + type: string + format: date-time + nullable: true + Patient: + type: object + properties: + identifier: + type: array + items: + type: string + name: + type: array + items: + type: string + nullable: true + active: + type: boolean + telecom: + type: array + items: + $ref: "#/components/schemas/ContactPoint" + gender: + type: string + enum: + - male + - female + - other + - unknown + birthDate: + type: string + format: date-time + nullable: true + deceased: + type: boolean + additionalProperties: + type: string + nullable: true + address: + type: array + items: + $ref: "#/components/schemas/Address" + nullable: false + maritalStatus: + $ref: "#/components/schemas/CodeableConcept" + multipleBirth: + type: boolean + nullable: true + additionalProperties: + type: integer + photo: + type: array + nullable: true + items: + $ref: "#/components/schemas/Attachment" + contact: + type: array + items: + type: object + properties: + relationship: + $ref: "#/components/schemas/CodeableConcept" + name: + type: string + nullable: true + telecom: + type: array + items: + $ref: "#/components/schemas/ContactPoint" + address: + $ref: "#/components/schemas/Address" + gender: + type: string + organization: + $ref: "#/components/schemas/Organization" + period: + $ref: "#/components/schemas/Period" + communication: + type: array + items: + type: object + properties: + language: + $ref: "#/components/schemas/CodeableConcept" + preferred: + type: boolean + description: A language which may be used to communicate with the patient about his or her health + generalPractitioner: + type: array + items: + $ref: "#/components/schemas/Organization" + nullable: true + managingOrganization: + $ref: "#/components/schemas/Organization" + + +paths: + /health: + get: + responses: + 200: + description: Health Endpoint + content: + text/html: + example: | + Health \ No newline at end of file diff --git a/projections/gorm.go b/projections/gorm.go index 26c51dd8..cf578aa4 100644 --- a/projections/gorm.go +++ b/projections/gorm.go @@ -173,6 +173,19 @@ func (p *GORMDB) GORMModel(name string, schema *openapi3.Schema, payload []byte) return nil, fmt.Errorf("unable to marshal payload into model '%s'", err) } tpayload["table_alias"] = name + //use the schema to convert simple arrays and inline objects to a json string + for k, v := range tpayload { + //get the property from the schema and check if it's an inline object, simple array, array with inline objects + if tproperty, ok := schema.Properties[k]; ok { + if (tproperty.Ref == "" && tproperty.Value.Type == "object") || (tproperty.Value.Type == "array" && tproperty.Value.Items.Ref == "") { + valueBytes, err := json.Marshal(v) + if err != nil { + return nil, fmt.Errorf("error marshalling inline object in '%s' '%s'", name, k) + } + tpayload[k] = string(valueBytes) + } + } + } data, _ := json.Marshal(tpayload) err = json.Unmarshal(data, &model) } @@ -333,6 +346,7 @@ func (p *GORMDB) GORMModelBuilder(name string, ref *openapi3.Schema, depth int) return instance, primaryKeysMap, nil } +//GORMPropertyDefaultValue convert schema property to GORM Model property func (p *GORMDB) GORMPropertyDefaultValue(parentName string, name string, schema *openapi3.SchemaRef, gormParts []string, depth int) (interface{}, []string, map[string]interface{}) { var defaultValue interface{} if schema.Value != nil { @@ -434,93 +448,8 @@ func (p *GORMDB) GORMPropertyDefaultValue(parentName string, name string, schema }]`), &defaultValue) //setup gorm field tag string gormParts = append(gormParts, "many2many:"+utils.SnakeCase(parentName)+"_"+utils.SnakeCase(name)) - } else if schema.Value.Items.Value.Type != "" { - switch schema.Value.Items.Value.Type { //TODO there must be a better way but too bushed now to find it - //case "integer": - // switch schema.Value.Format { - // case "int32": - // if schema.Value.Nullable { - // var value []*int32 - // defaultValue = value - // } else { - // var value []int32 - // defaultValue = value - // } - // case "int64": - // if schema.Value.Nullable { - // var value []*int64 - // defaultValue = value - // } else { - // var value []int64 - // defaultValue = value - // } - // case "uint": - // if schema.Value.Nullable { - // var value []*uint - // defaultValue = value - // } else { - // var value []uint - // defaultValue = value - // } - // default: - // if schema.Value.Nullable { - // var value []*int - // defaultValue = value - // } else { - // var value []int - // defaultValue = value - // } - // } - //case "number": - // switch schema.Value.Format { - // case "float32": - // if schema.Value.Nullable { - // var value []*float32 - // defaultValue = value - // } else { - // var value []float32 - // defaultValue = value - // } - // case "float64": - // if schema.Value.Nullable { - // var value []*float64 - // defaultValue = value - // } else { - // var value []float64 - // defaultValue = value - // } - // default: - // if schema.Value.Nullable { - // var value []*float32 - // defaultValue = value - // } else { - // var value []float32 - // defaultValue = value - // } - // } - //case "boolean": - // if schema.Value.Nullable { - // var value []*bool - // defaultValue = value - // } else { - // var value []bool - // defaultValue = value - // } - case "string": - switch schema.Value.Format { - case "date-time": - timeNow := weos.NewTime(time.Now()) - defaultValue = &timeNow - default: - if schema.Value.Nullable { - var strings *string - defaultValue = strings - } else { - var strings string - defaultValue = strings - } - } - } + } else { + return p.GORMInlineProperty(parentName, name, schema, gormParts, depth) } } default: @@ -546,12 +475,38 @@ func (p *GORMDB) GORMPropertyDefaultValue(parentName string, name string, schema gormParts = append(gormParts, "foreignKey:"+strings.Join(foreignKeys, ",")) gormParts = append(gormParts, "References:"+strings.Join(keyNames, ",")) return defaultValue, gormParts, keys + } else { + return p.GORMInlineProperty(parentName, name, schema, gormParts, depth) + } + } + + } + return defaultValue, gormParts, nil +} + +//GORMInlineProperty convert schema inline property to GORM model property. +func (p *GORMDB) GORMInlineProperty(parentName string, name string, schema *openapi3.SchemaRef, gormParts []string, depth int) (interface{}, []string, map[string]interface{}) { + var defaultValue interface{} + if schema.Value != nil { + switch schema.Value.Type { + case "array": + if schema.Value != nil && schema.Value.Items != nil && schema.Value.Items.Value != nil && depth < 3 { + if schema.Value.Nullable { + var strings *string + defaultValue = strings + } else { + var strings string + defaultValue = strings + } + } + default: + if schema.Value.Nullable { + var strings *string + defaultValue = strings + } else { + var strings string + defaultValue = strings } - //if depth >= 3 { - // var strings string - // defaultValue = strings - //} - //TODO I think here is where I'd put code to setup a json blob } } diff --git a/projections/gorm_test.go b/projections/gorm_test.go index ab2473d5..c8545bb5 100644 --- a/projections/gorm_test.go +++ b/projections/gorm_test.go @@ -276,3 +276,59 @@ func TestGORMDB_GORMModel(t *testing.T) { } }) } + +func TestGORMDB_GORMModels(t *testing.T) { + //load open api spec + api, err := rest.New("./fixtures/complex-spec.yaml") + if err != nil { + t.Fatalf("unexpected error setting up api: %s", err) + } + projection, err := projections.NewProjection(context.Background(), gormDB, api.EchoInstance().Logger) + if err != nil { + t.Fatal(err) + } + t.Run("convert inline arrays", func(t *testing.T) { + payload := make(map[string]interface{}) + payload["contact"] = []struct { + Name string `json:"name"` + RelationShip struct { + Text string + } `json:"relationShip"` + }{ + { + Name: "Medgar Evans", + }, + } + + payloadData, err := json.Marshal(&payload) + if err != nil { + t.Fatalf("unexpected error setting up payload '%s'", err) + } + + model, err := projection.GORMModel("Patient", api.GetConfig().Components.Schemas["Patient"].Value, payloadData) + if err != nil { + t.Fatalf("unexpected error setting up model '%s'", err) + } + + reader := ds.NewReader(model) + if !reader.HasField("Contact") { + t.Fatalf("expected contact field to exist") + } + + contactString := reader.GetField("Contact").String() + if contactString == "" { + t.Fatalf("expected contact to be json string") + } + + var actualContacts []map[string]interface{} + err = json.Unmarshal([]byte(contactString), &actualContacts) + if err != nil { + t.Fatalf("error unmarshiling conact '%s'", err) + } + + if actualContacts[0]["name"] != "Medgar Evans" { + t.Errorf("expected contact name to be '%s', got '%s'", "Medgar Evans", actualContacts[0]["name"]) + } + + }) +} From d0410043661c2c1ec2a78105966da8531efe0121 Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Fri, 24 Jun 2022 10:25:08 -0400 Subject: [PATCH 11/18] bugfix: Remove use of old schema code --- controllers/rest/fixtures/fhir.yaml | 375 +++++++++++++++++++++++- controllers/rest/global_initializers.go | 5 - 2 files changed, 372 insertions(+), 8 deletions(-) diff --git a/controllers/rest/fixtures/fhir.yaml b/controllers/rest/fixtures/fhir.yaml index 3c41eb5d..265bbd07 100644 --- a/controllers/rest/fixtures/fhir.yaml +++ b/controllers/rest/fixtures/fhir.yaml @@ -82,9 +82,72 @@ components: $ref: "#/components/schemas/CodeableConcept" serviceCategory: type: array + nullable: true items: $ref: "#/components/schemas/CodeableConcept" description: A broad categorization of the service that is to be performed during this appointment + serviceType: + type: array + nullable: true + items: + $ref: "#/components/schemas/CodeableConcept" + speciality: + type: array + nullable: true + items: + $ref: "#/components/schemas/CodeableConcept" + appointmentType: + $ref: "#/components/schemas/CodeableConcept" + reasonCode: + type: array + nullable: true + items: + $ref: "#/components/schemas/CodeableConcept" + reasonReference: + type: array + nullable: true + items: + $ref: "#/components/schemas/Procedure" + priority: + type: integer + nullable: true + description: + type: string + nullable: true + supportInformation: + type: string + nullable: true + start: + type: string + format: date-time + nullable: true + end: + type: string + format: date-time + nullable: true + minutesDuration: + type: integer + nullable: true + slot: + type: array + nullable: true + items: + $ref: "#/components/schemas/Slot" + created: + type: string + format: date-time + nullable: true + comment: + type: string + nullable: true + patientInstruction: + type: string + nullable: true + basedOn: + type: array + nullable: true + items: + $ref: "#/components/schemas/ServiceRequest" participant: type: object properties: @@ -105,6 +168,23 @@ components: required: - participant + Procedure: + type: object + properties: + identifier: + type: array + items: + $ref: "#/components/schemas/Identifier" + + ServiceRequest: + type: object + properties: + identifier: + type: array + items: + $ref: "#/components/schemas/Identifier" + + Attachment: type: object @@ -194,6 +274,38 @@ components: required: - system - use + HumanName: + type: object + properties: + use: + type: string + enum: + - usual + - official + - temp + - nickname + - anonymous + - old + - maiden + text: + type: string + nullable: true + family: + type: string + nullable: true + given: + type: string + nullable: true + prefix: + type: string + nullable: true + suffix: + type: string + nullable: true + period: + $ref: "#/components/schemas/Period" + required: + - use Identifier: type: object properties: @@ -261,11 +373,11 @@ components: identifier: type: array items: - type: string + $ref: "#/components/schemas/Identifier" name: type: array items: - type: string + $ref: "#/components/schemas/HumanName" nullable: true active: type: boolean @@ -345,6 +457,54 @@ components: nullable: true managingOrganization: $ref: "#/components/schemas/Organization" + Slot: + type: object + properties: + identifier: + type: array + items: + $ref: "#/components/schemas/Identifier" + nullable: true + serviceCategory: + type: array + items: + $ref: "#/components/schemas/CodeableConcept" + nullable: true + serviceType: + type: array + items: + $ref: "#/components/schemas/CodeableConcept" + nullable: true + specialty: + type: array + items: + $ref: "#/components/schemas/CodeableConcept" + nullable: true + appointmentType: + $ref: "#/components/schemas/CodeableConcept" + schedule: + $ref: "#/components/schemas/CodeableConcept" + status: + type: string + start: + type: string + format: date-time + nullable: true + end: + type: string + format: date-time + nullable: true + overbooked: + type: boolean + nullable: true + comment: + type: string + nullable: true + required: + - status + + + paths: @@ -356,4 +516,213 @@ paths: content: text/html: example: | - Health \ No newline at end of file + Health + + /patient: + post: + operationId: Create Patient + summary: Create a patient + requestBody: + description: Patient info that is submitted + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Patient" + responses: + 201: + description: Patient Created + content: + application/json: + schema: + $ref: "#/components/schemas/Patient" + 400: + description: Invalid Information + + get: + operationId: Get Patients + summary: Get List of Patients + responses: + 200: + description: List of Patients + content: + application/json: + schema: + type: object + items: + $ref: "#/components/schemas/Patient" + + + /patient/{id}: + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Get Patient by id + operationId: Get patient + responses: + 200: + description: Get Patient Information + content: + application/json: + schema: + $ref: "#/components/schemas/Patient" + + put: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Update patient information + operationId: Update patient + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Patient" + responses: + 200: + description: Update Patient Information + 400: + description: Bad Request + 401: + description: User is not authenticated + 403: + description: User is authenticated but is not authorized to access this information + + delete: + parameters: + - in: path + name: id + schema: + type: string + required: true + description: patient + requestBody: + description: patient + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Patient" + summary: Delete Patient + operationId: Delete Patient + responses: + 200: + description: Patient Deleted + content: + application/json: + schema: + $ref: "#/components/schemas/Patient" + 400: + description: Invalid Patient Submitted + + /appointment: + post: + operationId: Create Appointment + summary: Create an Appointment + requestBody: + description: Appointment info that is submitted + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Appointment" + responses: + 201: + description: Appointment Created + content: + application/json: + schema: + $ref: "#/components/schemas/Appointment" + 400: + description: Invalid Information + + get: + operationId: Get Appointments + summary: Get List of Appointments + responses: + 200: + description: List of Appointments + content: + application/json: + schema: + type: object + items: + $ref: "#/components/schemas/Appointment" + + /appointment/{id}: + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Get appointment by id + operationId: Get appointment + responses: + 200: + description: Get Appointment Information + content: + application/json: + schema: + $ref: "#/components/schemas/Appointment" + + put: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Update appointment information + operationId: Update appointment + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Appointment" + responses: + 200: + description: Update Appointment Information + 400: + description: Bad Request + 401: + description: User is not authenticated + 403: + description: User is authenticated but is not authorized to access this information + + delete: + parameters: + - in: path + name: id + schema: + type: string + required: true + description: Appointment + requestBody: + description: Appointment + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Appointment" + summary: Delete Appointment + operationId: Delete Appointment + responses: + 200: + description: Appointment Deleted + content: + application/json: + schema: + $ref: "#/components/schemas/Appointment" + 400: + description: Invalid Appointment Submitted \ No newline at end of file diff --git a/controllers/rest/global_initializers.go b/controllers/rest/global_initializers.go index 9c61d73a..5cc63b87 100644 --- a/controllers/rest/global_initializers.go +++ b/controllers/rest/global_initializers.go @@ -160,11 +160,6 @@ func DefaultProjection(ctxt context.Context, tapi Container, swagger *openapi3.S } } - //get the database schema - schemas := CreateSchema(ctxt, api.EchoInstance(), api.GetConfig()) - api.Schemas = schemas - ctxt = context.WithValue(ctxt, weosContext.SCHEMA_BUILDERS, schemas) - //get fields to be removed during migration step deletedFields := map[string][]string{} for name, sch := range api.GetConfig().Components.Schemas { From b017f055ce50631b89e20d2710bbe17740ce467b Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Fri, 24 Jun 2022 11:40:20 -0400 Subject: [PATCH 12/18] bugfix: Changed depth restriction --- controllers/rest/fixtures/fhir.yaml | 17 +++++++++++++++++ projections/gorm.go | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/controllers/rest/fixtures/fhir.yaml b/controllers/rest/fixtures/fhir.yaml index 265bbd07..8161d495 100644 --- a/controllers/rest/fixtures/fhir.yaml +++ b/controllers/rest/fixtures/fhir.yaml @@ -183,6 +183,12 @@ components: type: array items: $ref: "#/components/schemas/Identifier" + instantiateCanonical: + type: array + items: + $ref: "" + + @@ -518,6 +524,17 @@ paths: example: | Health + /api: + get: + operationId: Get API Details + x-controller: APIDiscovery + responses: + 200: + description: API Details + content: + application/json: + schema: + type: string /patient: post: operationId: Create Patient diff --git a/projections/gorm.go b/projections/gorm.go index cf578aa4..41a9e402 100644 --- a/projections/gorm.go +++ b/projections/gorm.go @@ -436,7 +436,7 @@ func (p *GORMDB) GORMPropertyDefaultValue(parentName string, name string, schema } } case "array": - if schema.Value != nil && schema.Value.Items != nil && schema.Value.Items.Value != nil && depth < 3 { + if schema.Value != nil && schema.Value.Items != nil && schema.Value.Items.Value != nil && depth < 5 { if schema.Value.Items.Ref != "" { tbuilder, _, err := p.GORMModelBuilder(strings.Replace(schema.Value.Items.Ref, "#/components/schemas/", "", -1), schema.Value.Items.Value, depth+1) if err != nil { @@ -454,7 +454,7 @@ func (p *GORMDB) GORMPropertyDefaultValue(parentName string, name string, schema } default: //Belongs to https://gorm.io/docs/belongs_to.html - if schema.Ref != "" && schema.Value != nil && depth < 3 { + if schema.Ref != "" && schema.Value != nil && depth < 5 { tbuilder, keys, err := p.GORMModelBuilder(name, schema.Value, depth+1) if err != nil { return nil, nil, nil @@ -490,7 +490,7 @@ func (p *GORMDB) GORMInlineProperty(parentName string, name string, schema *open if schema.Value != nil { switch schema.Value.Type { case "array": - if schema.Value != nil && schema.Value.Items != nil && schema.Value.Items.Value != nil && depth < 3 { + if schema.Value != nil && schema.Value.Items != nil && schema.Value.Items.Value != nil && depth < 5 { if schema.Value.Nullable { var strings *string defaultValue = strings From 20b8078aaeb5a437b7c354474ffce6b3a9b4f9ea Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Sun, 26 Jun 2022 15:56:08 -0400 Subject: [PATCH 13/18] bugfix: Fix for date time not being unmarshalled into content entity correctly * Updated getList in gorm projection so that the array that the results are unmarshalled into contains ContentEntities instantiated with the schema so that the date time conversion can happen --- controllers/rest/helpers_test.go | 16 ++++++++++++++++ projections/gorm.go | 5 ++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/controllers/rest/helpers_test.go b/controllers/rest/helpers_test.go index 7ce22d79..65e1af18 100644 --- a/controllers/rest/helpers_test.go +++ b/controllers/rest/helpers_test.go @@ -3,6 +3,7 @@ package rest_test import ( "github.com/getkin/kin-openapi/openapi3" "io/ioutil" + "net/http" "os" "regexp" "strings" @@ -27,3 +28,18 @@ func LoadConfig(t *testing.T, file string) (*openapi3.Swagger, error) { loader := openapi3.NewSwaggerLoader() return loader.LoadSwaggerFromData(content) } + +// RoundTripFunc . +type RoundTripFunc func(req *http.Request) *http.Response + +// RoundTrip . +func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req), nil +} + +//NewTestClient returns *http.Client with Transport replaced to avoid making real calls +func NewTestClient(fn RoundTripFunc) *http.Client { + return &http.Client{ + Transport: RoundTripFunc(fn), + } +} diff --git a/projections/gorm.go b/projections/gorm.go index 41a9e402..4d43f35d 100644 --- a/projections/gorm.go +++ b/projections/gorm.go @@ -638,7 +638,10 @@ func (p *GORMDB) GetList(ctx context.Context, entityFactory weos.EntityFactory, if err != nil { return nil, 0, err } - var contentEntities []*weos.ContentEntity + contentEntities := make([]*weos.ContentEntity, result.RowsAffected) + for k, _ := range contentEntities { + contentEntities[k], _ = entityFactory.NewEntity(ctx) + } data, _ := json.Marshal(models) err = json.Unmarshal(data, &contentEntities) if err != nil { From affb372fe1dd2b5c0c84801fbfb602255e6f4a48 Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Sun, 26 Jun 2022 16:35:17 -0400 Subject: [PATCH 14/18] bugfix: Fix for date time not being unmarshalled into content entity correctly * Switched back to using golang time.Time in gorm model --- projections/gorm.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projections/gorm.go b/projections/gorm.go index 4d43f35d..6511ad25 100644 --- a/projections/gorm.go +++ b/projections/gorm.go @@ -424,7 +424,7 @@ func (p *GORMDB) GORMPropertyDefaultValue(parentName string, name string, schema case "string": switch schema.Value.Format { case "date-time": - timeNow := weos.NewTime(time.Now()) + timeNow := time.Now() defaultValue = &timeNow default: if schema.Value.Nullable { From aeb7a0afeca204fe63ef5ce00b400fb2bf8b34ab Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Thu, 30 Jun 2022 20:24:33 -0400 Subject: [PATCH 15/18] feature: added basic template functions --- controllers/rest/controller_standard.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/controllers/rest/controller_standard.go b/controllers/rest/controller_standard.go index c78d37ec..edfc85d4 100644 --- a/controllers/rest/controller_standard.go +++ b/controllers/rest/controller_standard.go @@ -861,7 +861,12 @@ func DefaultResponseMiddleware(tapi Container, projection projections.Projection ctxt.File(fileName) } else if len(templates) != 0 { contextValues := ReturnContextValues(ctx) - t := template.New(path1.Base(templates[0])) + funcMap := template.FuncMap{ + "Title": strings.Title, + "ToUpper": strings.ToUpper, + "ToLower": strings.ToLower, + } + t := template.New(path1.Base(templates[0])).Funcs(funcMap) t, err := t.ParseFiles(templates...) if err != nil { api.e.Logger.Debugf("unexpected error %s ", err) From c99a58dba9b80328765e87cdf5370665ea5a0f77 Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Mon, 4 Jul 2022 06:14:50 -0400 Subject: [PATCH 16/18] fix: Get preload associations working --- projections/gorm.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projections/gorm.go b/projections/gorm.go index 6511ad25..ee3168b0 100644 --- a/projections/gorm.go +++ b/projections/gorm.go @@ -70,7 +70,7 @@ func (p *GORMDB) GetByKey(ctxt context.Context, entityFactory weos.EntityFactory model, err := p.GORMModel(entityFactory.Name(), entityFactory.Schema(), nil) - result := p.db.Debug().Table(entityFactory.Name()).Preload(clause.Associations).Scopes(ContentQuery()).Find(&model, identifiers) + result := p.db.Debug().Table(entityFactory.Name()).Model(model).Preload(clause.Associations).Scopes(ContentQuery()).Find(&model, identifiers) if result.Error != nil { return nil, result.Error } @@ -602,7 +602,7 @@ func (p *GORMDB) GetContentEntity(ctx context.Context, entityFactory weos.Entity model, err := p.GORMModel(entityFactory.Name(), entityFactory.Schema(), nil) - result := p.db.Debug().Table(entityFactory.TableName()).Preload(clause.Associations).Find(&model, "weos_id = ? ", weosID) + result := p.db.Debug().Table(entityFactory.TableName()).Model(model).Preload(clause.Associations).Find(&model, "weos_id = ? ", weosID) if result.Error != nil { p.logger.Errorf("unexpected error retrieving entity , got: '%s'", result.Error) return nil, result.Error From fa31f8ab1819d8cdaf1660c0f51a6d8aad5923cf Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Tue, 12 Jul 2022 21:21:36 -0400 Subject: [PATCH 17/18] feature: Closed #177 * Added parameter replacement in the authorization initializer so that there is a match in casbin (note the :id parameterization didn't work so resorted to using wildcards) * Added check on role for authorization as well * Removed debug statements from gorm projections --- controllers/rest/operation_initializers.go | 7 ++++ controllers/rest/security.go | 5 +++ end2end_test.go | 6 +-- features/security-schemes.feature | 49 ++++++++-------------- projections/gorm.go | 16 +++---- 5 files changed, 40 insertions(+), 43 deletions(-) diff --git a/controllers/rest/operation_initializers.go b/controllers/rest/operation_initializers.go index f73a2246..79af1d97 100644 --- a/controllers/rest/operation_initializers.go +++ b/controllers/rest/operation_initializers.go @@ -48,8 +48,15 @@ func AuthorizationInitializer(ctxt context.Context, tapi Container, path string, if authRaw, ok := operation.Extensions[AuthorizationConfigExtension]; ok { var enforcer *casbin.Enforcer var err error + + //update path so that the open api way of specifying url parameters is change to wildcards. This is to support the casbin policy + //note ideal we would use the open api way of specifying url parameters but this is not supported by casbin + re := regexp.MustCompile(`\{([a-zA-Z0-9\-_]+?)\}`) + path = re.ReplaceAllString(path, `*`) + //check if the default enforcer is setup if enforcer, err = tapi.GetPermissionEnforcer("Default"); err != nil { + var adapter interface{} if gormDB, err := tapi.GetGormDBConnection("Default"); err == nil { adapter, _ = gormadapter.NewAdapterByDB(gormDB) diff --git a/controllers/rest/security.go b/controllers/rest/security.go index 075963e9..c9a92f61 100644 --- a/controllers/rest/security.go +++ b/controllers/rest/security.go @@ -135,6 +135,11 @@ func (s *SecurityConfiguration) Middleware(api Container, projection projections if success { return next(ctxt) } + //check if the role has access to the endpoint + success, err = enforcer.Enforce(role, ctxt.Request().URL.Path, ctxt.Request().Method) + if success { + return next(ctxt) + } return ctxt.NoContent(http.StatusForbidden) } return next(ctxt) diff --git a/end2end_test.go b/end2end_test.go index 8f02c2d6..91e34a8b 100644 --- a/end2end_test.go +++ b/end2end_test.go @@ -1601,7 +1601,7 @@ func theUserIdOnTheEntityEventsShouldBe(userID string) error { return fmt.Errorf("unexpected error getting projection: %s", err) } apiProjection1 := apiProjection.(*projections.GORMDB) - eventResult := apiProjection1.DB().Table("gorm_events").Find(&events, "type = ?", "create") + eventResult := apiProjection1.DB().Table("gorm_events").Find(&events, "type = ?", "update") if eventResult.Error != nil { return fmt.Errorf("unexpected error finding events: %s", eventResult.Error) } @@ -2144,8 +2144,8 @@ func TestBDD(t *testing.T) { Options: &godog.Options{ Format: "pretty", Tags: "~long && ~skipped", - //Tags: "WEOS-1378", - //Tags: "WEOS-1519 && ~skipped", + //Tags: "WEOS-1343", + //Tags: "WEOS-1343 && ~skipped", }, }.Run() if status != 0 { diff --git a/features/security-schemes.feature b/features/security-schemes.feature index 11884e3e..dffb8de6 100644 --- a/features/security-schemes.feature +++ b/features/security-schemes.feature @@ -101,7 +101,7 @@ Feature: Use OpenAPI Security Scheme to protect endpoints required: - title security: - - Auth0: ["email","name"] + - Auth0: ["email"] paths: /: get: @@ -250,6 +250,7 @@ Feature: Use OpenAPI Security Scheme to protect endpoints allow: users: - auth0|1234 + - auth0|60d0c84316f69600691c1614 deny: roles: - Y9IvGucEhViFd58GL0bBoNrgEk3ohW88 @@ -263,6 +264,10 @@ Feature: Use OpenAPI Security Scheme to protect endpoints /post: post: operationId: Add Post + x-auth: + deny: + roles: + - Y9IvGucEhViFd58GL0bBoNrgEk3ohW88 requestBody: description: Blog info that is submitted required: true @@ -293,17 +298,15 @@ Feature: Use OpenAPI Security Scheme to protect endpoints """ And "Sojourner" authenticated and received a JWT And blogs in the api - | id | weos_id | sequence_no | title | description | - | 1234 | 22xu1Xa5CS3DK1Om2tB7OBDfWAF | 2 | Blog 1 | Some Blog | - | 4567 | 22xu4iw0bWMwxqbrUvjqEqu5dof | 1 | Blog 2 | Some Blog 2 | + | id | weos_id | title | description | + | 1234 | 22xu1Xa5CS3DK1Om2tB7OBDfWAF | Blog 1 | Some Blog | And the service is running Scenario: Set security globally If the security is set globally then that security scheme should be applied to each path - Given "Sojourner" is on the "Blog" create screen - And "Sojourner" enters "3" in the "id" field + Given "Sojourner" is on the "Blog" edit screen with id "1234" And "Sojourner" enters "Some Blog" in the "title" field And "Sojourner" enters "Some Description" in the "description" field When the "Blog" is submitted @@ -317,22 +320,12 @@ Feature: Use OpenAPI Security Scheme to protect endpoints Given "Sojourner" is on the "Blog" list screen And "Sojourner" authenticated and received a JWT And blogs in the api - | id | weos_id | sequence_no | title | description | - | 1 | 22xu1Xa5CS3DK1Om2tB7OJDHDSF | 2 | Blog 4 | Some Blog 4 | - | 164 | 55xu4iw0bWMwxqbrUvjqEEGGdfg | 1 | Blog 6 | Some Blog 6 | - | 2 | u6xu4iw0bWMwxqbrUvjqEEGGdfg | 1 | Blog 5 | Some Blog 5 | - | 3 | 43xu4iw0bWMwxqbrUvjqEEGGdfg | 1 | Blog 3 | Some Blog 3 | + | id | weos_id | title | description | + | 1 | 22xu1Xa5CS3DK1Om2tB7OJDHDSF | Blog 4 | Some Blog 4 | And the service is running And the items per page are 5 When the search button is hit Then a 200 response should be returned - And the list results should be - | id | title | description | - | 1 | Blog 4 | Some Blog 4 | - | 1234 | Blog 1 | Some Blog | - | 164 | Blog 6 | Some Blog 6 | - | 2 | Blog 5 | Some Blog 5 | - | 3 | Blog 3 | Some Blog 3 | Scenario: Valid JWT with request on path protected by OpenID @@ -345,25 +338,17 @@ Feature: Use OpenAPI Security Scheme to protect endpoints And a blog should be returned | id | title | description | | 1234 | Blog 1 | Some Blog | - And the "ETag" header should be "22xu1Xa5CS3DK1Om2tB7OBDfWAF.2" Scenario: Valid JWT subject stored with command events If a user logs in with a valid JWT then the header X-USER-ID should be set with the value in the "sub" field of the token - Given "Sojourner" is on the "Blog" create screen + Given "Sojourner" is on the "Blog" edit screen with id "1234" And "Sojourner" authenticated and received a JWT - And "Sojourner"'s id is "123" - And "Sojourner" enters "3" in the "id" field And "Sojourner" enters "Some Blog" in the "title" field And "Sojourner" enters "Some Description" in the "description" field When the "Blog" is submitted - Then the "Blog" is created - | id | title | description | - | 3 | Some Blog | Some Description | - And the "Blog" should have an id - And the "ETag" header should be ".1" - And the user id on the entity events should be "123" + And the user id on the entity events should be "auth0|60d0c84316f69600691c1614" Scenario: Expired JWT with request on path protected by OpenID @@ -696,12 +681,12 @@ Feature: Use OpenAPI Security Scheme to protect endpoints In order to support JWT from different authentication services, the developer should be able to specify which part of the JWT should be used for the user id, role, organization - Given "Sojourner" is on the "Blog" create screen + Given "Sojourner" is on the "Category" create screen And "Sojourner" authenticated and received a JWT And "Sojourner" enters "3" in the "id" field And "Sojourner" enters "Some Blog" in the "title" field And "Sojourner" enters "Some Description" in the "description" field - When the "Blog" is submitted + When the "Category" is submitted Then a 403 response should be returned @@ -719,11 +704,11 @@ Feature: Use OpenAPI Security Scheme to protect endpoints @WEOS-1519 Scenario: User denied based on the role being on the deny list - Given "Sojourner" is on the "Blog" edit screen with id "1234" + Given "Sojourner" is on the "Post" create screen And "Sojourner" authenticated and received a JWT And "Sojourner" enters "Some New Title" in the "title" field And "Sojourner" enters "Some Description" in the "description" field - When the "Blog" is submitted + When the "Post" is submitted Then a 403 response should be returned Scenario: Request with missing required scope diff --git a/projections/gorm.go b/projections/gorm.go index ee3168b0..b999e1cd 100644 --- a/projections/gorm.go +++ b/projections/gorm.go @@ -70,7 +70,7 @@ func (p *GORMDB) GetByKey(ctxt context.Context, entityFactory weos.EntityFactory model, err := p.GORMModel(entityFactory.Name(), entityFactory.Schema(), nil) - result := p.db.Debug().Table(entityFactory.Name()).Model(model).Preload(clause.Associations).Scopes(ContentQuery()).Find(&model, identifiers) + result := p.db.Table(entityFactory.Name()).Model(model).Preload(clause.Associations).Scopes(ContentQuery()).Find(&model, identifiers) if result.Error != nil { return nil, result.Error } @@ -153,7 +153,7 @@ func (p *GORMDB) Migrate(ctx context.Context, schema *openapi3.Swagger) error { } } - err := p.db.Debug().Migrator().AutoMigrate(models...) + err := p.db.Migrator().AutoMigrate(models...) return err } @@ -531,7 +531,7 @@ func (p *GORMDB) GetEventHandler() weos.EventHandler { payload, err := json.Marshal(entity.ToMap()) model, err := p.GORMModel(entityFactory.Name(), entityFactory.Schema(), payload) json.Unmarshal([]byte(`{"weos_id":"`+entity.ID+`","sequence_no":`+strconv.Itoa(int(entity.SequenceNo))+`}`), &model) - db := p.db.Debug().Table(entityFactory.Name()).Create(model) + db := p.db.Table(entityFactory.Name()).Create(model) if db.Error != nil { p.logger.Errorf("error creating %s, got %s", entityFactory.Name(), db.Error) return db.Error @@ -561,7 +561,7 @@ func (p *GORMDB) GetEventHandler() weos.EventHandler { //check to see if the property is an array with items defined that is a reference to another schema (inline array will be stored as json in the future) if property.Value != nil && property.Value.Type == "array" && property.Value.Items != nil && property.Value.Items.Ref != "" { field := reader.GetField(strings.Title(key)) - err = p.db.Debug().Model(model).Association(strings.Title(key)).Replace(field.Interface()) + err = p.db.Model(model).Association(strings.Title(key)).Replace(field.Interface()) if err != nil { p.logger.Errorf("error clearing association %s for %s, got %s", strings.Title(key), entityFactory.Name(), err) return err @@ -570,7 +570,7 @@ func (p *GORMDB) GetEventHandler() weos.EventHandler { } //update database value - db := p.db.Debug().Table(entityFactory.Name()).Updates(model) + db := p.db.Table(entityFactory.Name()).Updates(model) if db.Error != nil { p.logger.Errorf("error creating %s, got %s", entityFactory.Name(), db.Error) return db.Error @@ -583,7 +583,7 @@ func (p *GORMDB) GetEventHandler() weos.EventHandler { p.logger.Errorf("error generating entity model '%s'", err) return err } - db := p.db.Debug().Table(entityFactory.Name()).Where("weos_id = ?", event.Meta.EntityID).Delete(model) + db := p.db.Table(entityFactory.Name()).Where("weos_id = ?", event.Meta.EntityID).Delete(model) if db.Error != nil { p.logger.Errorf("error deleting %s, got %s", entityFactory.Name(), db.Error) return db.Error @@ -602,7 +602,7 @@ func (p *GORMDB) GetContentEntity(ctx context.Context, entityFactory weos.Entity model, err := p.GORMModel(entityFactory.Name(), entityFactory.Schema(), nil) - result := p.db.Debug().Table(entityFactory.TableName()).Model(model).Preload(clause.Associations).Find(&model, "weos_id = ? ", weosID) + result := p.db.Table(entityFactory.TableName()).Model(model).Preload(clause.Associations).Find(&model, "weos_id = ? ", weosID) if result.Error != nil { p.logger.Errorf("unexpected error retrieving entity , got: '%s'", result.Error) return nil, result.Error @@ -634,7 +634,7 @@ func (p *GORMDB) GetList(ctx context.Context, entityFactory weos.EntityFactory, } model, err := p.GORMModel(entityFactory.Name(), entityFactory.Schema(), nil) models, err := p.GORMModels(entityFactory.Name(), entityFactory.Schema()) - result = p.db.Debug().Table(entityFactory.Name()).Scopes(FilterQuery(filtersProp)).Model(model).Preload(clause.Associations).Omit("weos_id, sequence_no, table").Count(&count).Scopes(paginate(page, limit), sort(sortOptions)).Find(models) + result = p.db.Table(entityFactory.Name()).Scopes(FilterQuery(filtersProp)).Model(model).Preload(clause.Associations).Omit("weos_id, sequence_no, table").Count(&count).Scopes(paginate(page, limit), sort(sortOptions)).Find(models) if err != nil { return nil, 0, err } From acfb0d06d5c1535d0e65ca978e5ad1e0f2026e52 Mon Sep 17 00:00:00 2001 From: akeemphilbert Date: Tue, 12 Jul 2022 21:34:58 -0400 Subject: [PATCH 18/18] feature: Fixed content entity check test --- end2end_test.go | 77 ++++++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/end2end_test.go b/end2end_test.go index 91e34a8b..b4d9576f 100644 --- a/end2end_test.go +++ b/end2end_test.go @@ -636,49 +636,60 @@ func theSpecificationIsParsed(arg1 string) error { } func aEntityConfigurationShouldBeSetup(arg1 string, arg2 *godog.DocString) error { - schema := API.Schemas + schema := API.GetConfig().Components.Schemas if _, ok := schema[arg1]; !ok { return fmt.Errorf("no entity named '%s'", arg1) } entityString := strings.SplitAfter(arg2.Content, arg1+" {") - reader := ds.NewReader(schema[arg1].Build().New()) - - s := strings.TrimRight(entityString[1], "}") - s = strings.TrimSpace(s) - entityFields := strings.Split(s, "\n") - - for _, f := range entityFields { - f = strings.TrimSpace(f) - fields := strings.Split(f, " ") - if !reader.HasField(strings.Title(fields[1])) { - return fmt.Errorf("did not find field '%s'", fields[1]) + tprojection, err := API.GetProjection("Default") + if err != nil { + return err + } + //if the projection is a GORMDB projection then check that the model is setup correctly + if projection, ok := tprojection.(*projections.GORMDB); ok { + model, err := projection.GORMModel(arg1, schema[arg1].Value, nil) + if err != nil { + return err } + reader := ds.NewReader(model) - field := reader.GetField(strings.Title(fields[1])) - switch fields[0] { - case "string": - if field.Interface() != "" && field.Interface() != field.PointerString() { - return fmt.Errorf("expected a string, got '%v'", field.Interface()) - } + s := strings.TrimRight(entityString[1], "}") + s = strings.TrimSpace(s) + entityFields := strings.Split(s, "\n") - case "integer": - if field.Interface() != 0 && field.Interface() != field.PointerInt() { - return fmt.Errorf("expected an integer, got '%v'", field.Interface()) + for _, f := range entityFields { + f = strings.TrimSpace(f) + fields := strings.Split(f, " ") + if !reader.HasField(strings.Title(fields[1])) { + return fmt.Errorf("did not find field '%s'", fields[1]) } - case "uint": - if field.Interface() != uint(0) && field.Interface() != field.PointerUint() { - return fmt.Errorf("expected an uint, got '%v'", field.Interface()) - } - case "datetime": - dateTime := field.PointerTime() - if field.Interface() != new(time.Time) && field.Interface() != dateTime { - fmt.Printf("date interface is '%v'", field.Interface()) - fmt.Printf("empty date interface is '%v'", new(time.Time)) - return fmt.Errorf("expected an uint, got '%v'", field.Interface()) + + field := reader.GetField(strings.Title(fields[1])) + switch fields[0] { + case "string": + if field.Interface() != "" && field.Interface() != field.PointerString() { + return fmt.Errorf("expected a string, got '%v'", field.Interface()) + } + + case "integer": + if field.Interface() != 0 && field.Interface() != field.PointerInt() { + return fmt.Errorf("expected an integer, got '%v'", field.Interface()) + } + case "uint": + if field.Interface() != uint(0) && field.Interface() != field.PointerUint() { + return fmt.Errorf("expected an uint, got '%v'", field.Interface()) + } + case "datetime": + dateTime := field.PointerTime() + if field.Interface() != new(time.Time) && field.Interface() != dateTime { + fmt.Printf("date interface is '%v'", field.Interface()) + fmt.Printf("empty date interface is '%v'", new(time.Time)) + return fmt.Errorf("expected an uint, got '%v'", field.Interface()) + } + default: + return fmt.Errorf("got an unexpected field type: %s", fields[0]) } - default: - return fmt.Errorf("got an unexpected field type: %s", fields[0]) } }