From 9a661c203b924257822049ea21e4cb1750e1e32e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerson=20Pozo=20Gonz=C3=A1lez?= Date: Thu, 6 Oct 2016 10:26:25 +0200 Subject: [PATCH 1/3] read proxy resources from DB with auto refresh (closes #4) --- api/authz.go | 16 +- api/group.go | 22 +- api/manager.go | 33 +- api/policy.go | 12 +- api/proxy.go | 32 ++ api/proxy_test.go | 48 +++ api/testutils_test.go | 36 ++- api/user.go | 12 +- cmd/proxy/main.go | 10 +- cmd/worker/main.go | 10 +- database/postgresql/postgres_db.go | 17 +- database/postgresql/postgres_db_test.go | 23 ++ database/postgresql/proxy.go | 44 +++ database/postgresql/proxy_test.go | 82 +++++ dist/proxy.toml | 32 +- dist/test/cert.pem | 31 ++ dist/test/key.pem | 51 +++ dist/worker.toml | 2 +- foulkon/proxy.go | 118 ++++--- foulkon/worker.go | 6 +- glide.lock | 2 +- glide.yaml | 2 +- http/handler.go | 14 - http/http_test.go | 143 ++++++--- http/proxy.go | 12 +- http/server.go | 202 ++++++++++++ http/server_test.go | 409 ++++++++++++++++++++++++ 27 files changed, 1228 insertions(+), 193 deletions(-) create mode 100644 api/proxy.go create mode 100644 api/proxy_test.go create mode 100644 database/postgresql/proxy.go create mode 100644 database/postgresql/proxy_test.go create mode 100644 dist/test/cert.pem create mode 100644 dist/test/key.pem create mode 100644 http/server.go create mode 100644 http/server_test.go diff --git a/api/authz.go b/api/authz.go index 6270934..8c4ae93 100644 --- a/api/authz.go +++ b/api/authz.go @@ -39,7 +39,7 @@ func (e ExternalResource) GetUrn() string { // AUTHZ API IMPLEMENTATION // GetAuthorizedUsers returns authorized users for specified resource+action -func (api AuthAPI) GetAuthorizedUsers(requestInfo RequestInfo, resourceUrn string, action string, users []User) ([]User, error) { +func (api WorkerAPI) GetAuthorizedUsers(requestInfo RequestInfo, resourceUrn string, action string, users []User) ([]User, error) { resourcesToAuthorize := []Resource{} for _, usr := range users { resourcesToAuthorize = append(resourcesToAuthorize, usr) @@ -56,7 +56,7 @@ func (api AuthAPI) GetAuthorizedUsers(requestInfo RequestInfo, resourceUrn strin } // GetAuthorizedGroups returns authorized users for specified user combined with resource+action -func (api AuthAPI) GetAuthorizedGroups(requestInfo RequestInfo, resourceUrn string, action string, groups []Group) ([]Group, error) { +func (api WorkerAPI) GetAuthorizedGroups(requestInfo RequestInfo, resourceUrn string, action string, groups []Group) ([]Group, error) { resourcesToAuthorize := []Resource{} for _, group := range groups { resourcesToAuthorize = append(resourcesToAuthorize, group) @@ -73,7 +73,7 @@ func (api AuthAPI) GetAuthorizedGroups(requestInfo RequestInfo, resourceUrn stri } // GetAuthorizedPolicies returns authorized policies for specified user combined with resource+action -func (api AuthAPI) GetAuthorizedPolicies(requestInfo RequestInfo, resourceUrn string, action string, policies []Policy) ([]Policy, error) { +func (api WorkerAPI) GetAuthorizedPolicies(requestInfo RequestInfo, resourceUrn string, action string, policies []Policy) ([]Policy, error) { resourcesToAuthorize := []Resource{} for _, policy := range policies { resourcesToAuthorize = append(resourcesToAuthorize, policy) @@ -90,7 +90,7 @@ func (api AuthAPI) GetAuthorizedPolicies(requestInfo RequestInfo, resourceUrn st } // GetAuthorizedExternalResources returns the resources where the specified user has the action granted -func (api AuthAPI) GetAuthorizedExternalResources(requestInfo RequestInfo, action string, resources []string) ([]string, error) { +func (api WorkerAPI) GetAuthorizedExternalResources(requestInfo RequestInfo, action string, resources []string) ([]string, error) { // Validate parameters if err := AreValidActions([]string{action}); err != nil { // Transform to API error @@ -154,7 +154,7 @@ func (api AuthAPI) GetAuthorizedExternalResources(requestInfo RequestInfo, actio // PRIVATE HELPER METHODS // getAuthorizedResources retrieves filtered resources where the authenticated user has permissions -func (api AuthAPI) getAuthorizedResources(requestInfo RequestInfo, resourceUrn string, action string, resources []Resource) ([]Resource, error) { +func (api WorkerAPI) getAuthorizedResources(requestInfo RequestInfo, resourceUrn string, action string, resources []Resource) ([]Resource, error) { // If user is an admin return all resources without restriction if requestInfo.Admin { return resources, nil @@ -183,7 +183,7 @@ func (api AuthAPI) getAuthorizedResources(requestInfo RequestInfo, resourceUrn s } // Get restrictions for this action and full resource or prefix resource, attached to this authenticated user -func (api AuthAPI) getRestrictions(externalID string, action string, resource string) (*Restrictions, error) { +func (api WorkerAPI) getRestrictions(externalID string, action string, resource string) (*Restrictions, error) { // Get user if exists user, err := api.UserRepo.GetUserByExternalID(externalID) @@ -225,7 +225,7 @@ func (api AuthAPI) getRestrictions(externalID string, action string, resource st return authResources, nil } -func (api AuthAPI) getGroupsByUser(userID string) ([]Group, error) { +func (api WorkerAPI) getGroupsByUser(userID string) ([]Group, error) { userGroups, _, err := api.UserRepo.GetGroupsByUserID(userID, &Filter{}) if err != nil { //Transform to DB error @@ -246,7 +246,7 @@ func (api AuthAPI) getGroupsByUser(userID string) ([]Group, error) { } // Retrieve policies attached to a slice of groups -func (api AuthAPI) getPoliciesByGroups(groups []Group) ([]Policy, error) { +func (api WorkerAPI) getPoliciesByGroups(groups []Group) ([]Policy, error) { if groups == nil || len(groups) < 1 { return nil, nil } diff --git a/api/group.go b/api/group.go index 479367c..4fc507b 100755 --- a/api/group.go +++ b/api/group.go @@ -48,7 +48,7 @@ type GroupPolicies struct { // GROUP API IMPLEMENTATION -func (api AuthAPI) AddGroup(requestInfo RequestInfo, org string, name string, path string) (*Group, error) { +func (api WorkerAPI) AddGroup(requestInfo RequestInfo, org string, name string, path string) (*Group, error) { // Validate fields if !IsValidName(name) { return nil, &Error{ @@ -123,7 +123,7 @@ func (api AuthAPI) AddGroup(requestInfo RequestInfo, org string, name string, pa } -func (api AuthAPI) GetGroupByName(requestInfo RequestInfo, org string, name string) (*Group, error) { +func (api WorkerAPI) GetGroupByName(requestInfo RequestInfo, org string, name string) (*Group, error) { // Validate fields if !IsValidName(name) { return nil, &Error{ @@ -178,7 +178,7 @@ func (api AuthAPI) GetGroupByName(requestInfo RequestInfo, org string, name stri } } -func (api AuthAPI) ListGroups(requestInfo RequestInfo, filter *Filter) ([]GroupIdentity, int, error) { +func (api WorkerAPI) ListGroups(requestInfo RequestInfo, filter *Filter) ([]GroupIdentity, int, error) { // Validate fields var total int orderByValidColumns := api.UserRepo.OrderByValidColumns(GROUP_ACTION_LIST_GROUPS) @@ -224,7 +224,7 @@ func (api AuthAPI) ListGroups(requestInfo RequestInfo, filter *Filter) ([]GroupI return groupIDs, total, nil } -func (api AuthAPI) UpdateGroup(requestInfo RequestInfo, org string, name string, newName string, newPath string) (*Group, error) { +func (api WorkerAPI) UpdateGroup(requestInfo RequestInfo, org string, name string, newName string, newPath string) (*Group, error) { // Validate fields if !IsValidName(newName) { return nil, &Error{ @@ -320,7 +320,7 @@ func (api AuthAPI) UpdateGroup(requestInfo RequestInfo, org string, name string, } -func (api AuthAPI) RemoveGroup(requestInfo RequestInfo, org string, name string) error { +func (api WorkerAPI) RemoveGroup(requestInfo RequestInfo, org string, name string) error { // Call repo to retrieve the group group, err := api.GetGroupByName(requestInfo, org, name) @@ -358,7 +358,7 @@ func (api AuthAPI) RemoveGroup(requestInfo RequestInfo, org string, name string) return nil } -func (api AuthAPI) AddMember(requestInfo RequestInfo, externalId string, name string, org string) error { +func (api WorkerAPI) AddMember(requestInfo RequestInfo, externalId string, name string, org string) error { // Call repo to retrieve the group groupDB, err := api.GetGroupByName(requestInfo, org, name) @@ -420,7 +420,7 @@ func (api AuthAPI) AddMember(requestInfo RequestInfo, externalId string, name st return nil } -func (api AuthAPI) RemoveMember(requestInfo RequestInfo, externalId string, name string, org string) error { +func (api WorkerAPI) RemoveMember(requestInfo RequestInfo, externalId string, name string, org string) error { // Call repo to retrieve the group groupDB, err := api.GetGroupByName(requestInfo, org, name) @@ -483,7 +483,7 @@ func (api AuthAPI) RemoveMember(requestInfo RequestInfo, externalId string, name return nil } -func (api AuthAPI) ListMembers(requestInfo RequestInfo, filter *Filter) ([]GroupMembers, int, error) { +func (api WorkerAPI) ListMembers(requestInfo RequestInfo, filter *Filter) ([]GroupMembers, int, error) { // Validate fields var total int orderByValidColumns := api.UserRepo.OrderByValidColumns(GROUP_ACTION_LIST_MEMBERS) @@ -538,7 +538,7 @@ func (api AuthAPI) ListMembers(requestInfo RequestInfo, filter *Filter) ([]Group return members, total, nil } -func (api AuthAPI) AttachPolicyToGroup(requestInfo RequestInfo, org string, name string, policyName string) error { +func (api WorkerAPI) AttachPolicyToGroup(requestInfo RequestInfo, org string, name string, policyName string) error { // Check if group exists group, err := api.GetGroupByName(requestInfo, org, name) @@ -598,7 +598,7 @@ func (api AuthAPI) AttachPolicyToGroup(requestInfo RequestInfo, org string, name return nil } -func (api AuthAPI) DetachPolicyToGroup(requestInfo RequestInfo, org string, name string, policyName string) error { +func (api WorkerAPI) DetachPolicyToGroup(requestInfo RequestInfo, org string, name string, policyName string) error { // Check if group exists group, err := api.GetGroupByName(requestInfo, org, name) @@ -659,7 +659,7 @@ func (api AuthAPI) DetachPolicyToGroup(requestInfo RequestInfo, org string, name return nil } -func (api AuthAPI) ListAttachedGroupPolicies(requestInfo RequestInfo, filter *Filter) ([]GroupPolicies, int, error) { +func (api WorkerAPI) ListAttachedGroupPolicies(requestInfo RequestInfo, filter *Filter) ([]GroupPolicies, int, error) { // Validate fields var total int orderByValidColumns := api.UserRepo.OrderByValidColumns(GROUP_ACTION_LIST_ATTACHED_GROUP_POLICIES) diff --git a/api/manager.go b/api/manager.go index 1a8229c..03495ab 100644 --- a/api/manager.go +++ b/api/manager.go @@ -8,34 +8,41 @@ import ( // TYPE DEFINITIONS -// Interface that all resource types have to implement +// Resource interface that all resource types have to implement type Resource interface { // This method must return resource URN GetUrn() string } -// Interface for User-Group relationships +// UserGroupRelation interface for User-Group relationships type UserGroupRelation interface { GetUser() *User GetGroup() *Group GetDate() time.Time } -// Interface for Policy-Group relationships +// PolicyGroupRelation interface for Policy-Group relationships type PolicyGroupRelation interface { GetGroup() *Group GetPolicy() *Policy GetDate() time.Time } -// Foulkon API that implements API interfaces using repositories -type AuthAPI struct { +// WorkerAPI that implements API interfaces using repositories +type WorkerAPI struct { UserRepo UserRepo GroupRepo GroupRepo PolicyRepo PolicyRepo + ProxyRepo ProxyRepo Logger *log.Logger } +// ProxyAPI that implements API interfaces using repositories +type ProxyAPI struct { + ProxyRepo ProxyRepo + Logger *log.Logger +} + // Filter properties for database search type Filter struct { PathPrefix string @@ -52,6 +59,7 @@ type Filter struct { // API INTERFACES WITH AUTHORIZATION +// UserAPI interface type UserAPI interface { // Store user in database. Throw error when parameters are invalid, // user already exists or unexpected error happen. @@ -78,6 +86,7 @@ type UserAPI interface { ListGroupsByUser(requestInfo RequestInfo, filter *Filter) ([]UserGroups, int, error) } +// GroupAPI interface type GroupAPI interface { // Store group in database. Throw error when the input parameters are invalid, // the group already exist or unexpected error happen. @@ -125,6 +134,7 @@ type GroupAPI interface { ListAttachedGroupPolicies(requestInfo RequestInfo, filter *Filter) ([]GroupPolicies, int, error) } +// PolicyAPI interface type PolicyAPI interface { // Store policy in database. Throw error when the input parameters are invalid, // the policy already exist or unexpected error happen. @@ -153,6 +163,7 @@ type PolicyAPI interface { ListAttachedGroups(requestInfo RequestInfo, filter *Filter) ([]PolicyGroups, int, error) } +// AuthzAPI interface type AuthzAPI interface { // Retrieve list of authorized user resources filtered according to the input parameters. Throw error // if requestInfo doesn't exist, requestInfo doesn't have access to any resources or unexpected error happen. @@ -171,6 +182,12 @@ type AuthzAPI interface { GetAuthorizedExternalResources(requestInfo RequestInfo, action string, resources []string) ([]string, error) } +// ProxyResourcesAPI interface to manage proxy resources +type ProxyResourcesAPI interface { + // Retrieve list of proxy resources. + GetProxyResources() ([]ProxyResource, error) +} + // REPOSITORY INTERFACES // UserRepo contains all database operations @@ -281,3 +298,9 @@ type PolicyRepo interface { // OrderByValidColumns returns valid columns that you can use in OrderBy OrderByValidColumns(action string) []string } + +// ProxyRepo contains all database operations +type ProxyRepo interface { + // Retrieve proxy resources from database. Otherwise it throws an error. + GetProxyResources() ([]ProxyResource, error) +} diff --git a/api/policy.go b/api/policy.go index 6ffc5a8..86e0f0f 100755 --- a/api/policy.go +++ b/api/policy.go @@ -54,7 +54,7 @@ func (s Statement) String() string { // POLICY API IMPLEMENTATION -func (api AuthAPI) AddPolicy(requestInfo RequestInfo, name string, path string, org string, statements []Statement) (*Policy, error) { +func (api WorkerAPI) AddPolicy(requestInfo RequestInfo, name string, path string, org string, statements []Statement) (*Policy, error) { // Validate fields if !IsValidName(name) { return nil, &Error{ @@ -139,7 +139,7 @@ func (api AuthAPI) AddPolicy(requestInfo RequestInfo, name string, path string, } } -func (api AuthAPI) GetPolicyByName(requestInfo RequestInfo, org string, policyName string) (*Policy, error) { +func (api WorkerAPI) GetPolicyByName(requestInfo RequestInfo, org string, policyName string) (*Policy, error) { // Validate fields if !IsValidName(policyName) { return nil, &Error{ @@ -192,7 +192,7 @@ func (api AuthAPI) GetPolicyByName(requestInfo RequestInfo, org string, policyNa } } -func (api AuthAPI) ListPolicies(requestInfo RequestInfo, filter *Filter) ([]PolicyIdentity, int, error) { +func (api WorkerAPI) ListPolicies(requestInfo RequestInfo, filter *Filter) ([]PolicyIdentity, int, error) { // Validate fields var total int orderByValidColumns := api.UserRepo.OrderByValidColumns(POLICY_ACTION_LIST_POLICIES) @@ -237,7 +237,7 @@ func (api AuthAPI) ListPolicies(requestInfo RequestInfo, filter *Filter) ([]Poli return policyIDs, total, nil } -func (api AuthAPI) UpdatePolicy(requestInfo RequestInfo, org string, policyName string, newName string, newPath string, +func (api WorkerAPI) UpdatePolicy(requestInfo RequestInfo, org string, policyName string, newName string, newPath string, newStatements []Statement) (*Policy, error) { // Validate fields if !IsValidName(newName) { @@ -344,7 +344,7 @@ func (api AuthAPI) UpdatePolicy(requestInfo RequestInfo, org string, policyName return updatedPolicy, nil } -func (api AuthAPI) RemovePolicy(requestInfo RequestInfo, org string, name string) error { +func (api WorkerAPI) RemovePolicy(requestInfo RequestInfo, org string, name string) error { // Call repo to retrieve the policy policy, err := api.GetPolicyByName(requestInfo, org, name) @@ -379,7 +379,7 @@ func (api AuthAPI) RemovePolicy(requestInfo RequestInfo, org string, name string return nil } -func (api AuthAPI) ListAttachedGroups(requestInfo RequestInfo, filter *Filter) ([]PolicyGroups, int, error) { +func (api WorkerAPI) ListAttachedGroups(requestInfo RequestInfo, filter *Filter) ([]PolicyGroups, int, error) { // Validate fields var total int orderByValidColumns := api.UserRepo.OrderByValidColumns(POLICY_ACTION_LIST_ATTACHED_GROUPS) diff --git a/api/proxy.go b/api/proxy.go new file mode 100644 index 0000000..4178e7a --- /dev/null +++ b/api/proxy.go @@ -0,0 +1,32 @@ +package api + +import "github.com/Tecsisa/foulkon/database" + +// TYPE DEFINITIONS + +// ProxyResource domain +type ProxyResource struct { + ID string `json:"id, omitempty"` + Host string `json:"host, omitempty"` + Url string `json:"url, omitempty"` + Method string `json:"method, omitempty"` + Urn string `json:"urn, omitempty"` + Action string `json:"action, omitempty"` +} + +// GetProxyResources return proxy resources +func (api ProxyAPI) GetProxyResources() ([]ProxyResource, error) { + resources, err := api.ProxyRepo.GetProxyResources() + + // Error handling + if err != nil { + //Transform to DB error + dbError := err.(*database.Error) + return nil, &Error{ + Code: UNKNOWN_API_ERROR, + Message: dbError.Message, + } + } + + return resources, nil +} diff --git a/api/proxy_test.go b/api/proxy_test.go new file mode 100644 index 0000000..e2278c6 --- /dev/null +++ b/api/proxy_test.go @@ -0,0 +1,48 @@ +package api + +import ( + "testing" + + "github.com/Tecsisa/foulkon/database" +) + +func TestProxyAPI_GetProxyResources(t *testing.T) { + testcases := map[string]struct { + wantError error + + getProxyResourcesMethod []ProxyResource + getProxyResourcesErr error + }{ + "OkCase": { + getProxyResourcesMethod: []ProxyResource{ + { + ID: "ID", + Host: "host", + Url: "/url", + Method: "Method", + Urn: "urn", + Action: "action", + }, + }, + }, + "ErrorCaseInternalError": { + wantError: &Error{ + Code: UNKNOWN_API_ERROR, + }, + getProxyResourcesErr: &database.Error{ + Code: database.INTERNAL_ERROR, + }, + }, + } + + for n, testcase := range testcases { + testRepo := makeTestRepo() + testAPI := makeProxyTestAPI(testRepo) + + testRepo.ArgsOut[GetProxyResourcesMethod][0] = testcase.getProxyResourcesMethod + testRepo.ArgsOut[GetProxyResourcesMethod][1] = testcase.getProxyResourcesErr + + resources, err := testAPI.GetProxyResources() + checkMethodResponse(t, n, testcase.wantError, err, testcase.getProxyResourcesMethod, resources) + } +} diff --git a/api/testutils_test.go b/api/testutils_test.go index a788e40..bad76ab 100644 --- a/api/testutils_test.go +++ b/api/testutils_test.go @@ -38,6 +38,7 @@ const ( GetPoliciesFilteredMethod = "GetPoliciesFiltered" GetAttachedGroupsMethod = "GetAttachedGroups" OrderByValidColumnsMethod = "OrderByValidColumns" + GetProxyResourcesMethod = "GetProxyResources" ) // TestRepo that implements all repo manager interfaces @@ -130,11 +131,13 @@ func makeTestRepo() *TestRepo { testRepo.ArgsOut[GetAttachedGroupsMethod] = make([]interface{}, 3) testRepo.ArgsOut[OrderByValidColumnsMethod] = make([]interface{}, 1) + testRepo.ArgsOut[GetProxyResourcesMethod] = make([]interface{}, 2) + return testRepo } -func makeTestAPI(testRepo *TestRepo) *AuthAPI { - api := &AuthAPI{ +func makeTestAPI(testRepo *TestRepo) *WorkerAPI { + api := &WorkerAPI{ UserRepo: testRepo, GroupRepo: testRepo, PolicyRepo: testRepo, @@ -148,6 +151,19 @@ func makeTestAPI(testRepo *TestRepo) *AuthAPI { return api } +func makeProxyTestAPI(testRepo *TestRepo) *ProxyAPI { + api := &ProxyAPI{ + ProxyRepo: testRepo, + Logger: &log.Logger{ + Out: bytes.NewBuffer([]byte{}), + Formatter: &log.TextFormatter{}, + Hooks: make(log.LevelHooks), + Level: log.DebugLevel, + }, + } + return api +} + ////////////////////// // UserGroupRelation ////////////////////// @@ -562,6 +578,22 @@ func (t TestRepo) OrderByValidColumns(action string) []string { return validColumns } +////////////// +// Proxy repo +////////////// + +func (t TestRepo) GetProxyResources() ([]ProxyResource, error) { + var resources []ProxyResource + if t.ArgsOut[GetProxyResourcesMethod][0] != nil { + resources = t.ArgsOut[GetProxyResourcesMethod][0].([]ProxyResource) + } + var err error + if t.ArgsOut[GetProxyResourcesMethod][1] != nil { + err = t.ArgsOut[GetProxyResourcesMethod][1].(error) + } + return resources, err +} + // Private helper methods func getRandomString(runeValue []rune, n int) string { diff --git a/api/user.go b/api/user.go index 7588487..2dd43d6 100755 --- a/api/user.go +++ b/api/user.go @@ -37,7 +37,7 @@ func (u User) GetUrn() string { // USER API IMPLEMENTATION -func (api AuthAPI) AddUser(requestInfo RequestInfo, externalId string, path string) (*User, error) { +func (api WorkerAPI) AddUser(requestInfo RequestInfo, externalId string, path string) (*User, error) { // Validate fields if !IsValidUserExternalID(externalId) { return nil, &Error{ @@ -104,7 +104,7 @@ func (api AuthAPI) AddUser(requestInfo RequestInfo, externalId string, path stri } } -func (api AuthAPI) GetUserByExternalID(requestInfo RequestInfo, externalId string) (*User, error) { +func (api WorkerAPI) GetUserByExternalID(requestInfo RequestInfo, externalId string) (*User, error) { if !IsValidUserExternalID(externalId) { return nil, &Error{ Code: INVALID_PARAMETER_ERROR, @@ -148,7 +148,7 @@ func (api AuthAPI) GetUserByExternalID(requestInfo RequestInfo, externalId strin } } -func (api AuthAPI) ListUsers(requestInfo RequestInfo, filter *Filter) ([]string, int, error) { +func (api WorkerAPI) ListUsers(requestInfo RequestInfo, filter *Filter) ([]string, int, error) { // Check parameters var total int orderByValidColumns := api.UserRepo.OrderByValidColumns(USER_ACTION_LIST_USERS) @@ -186,7 +186,7 @@ func (api AuthAPI) ListUsers(requestInfo RequestInfo, filter *Filter) ([]string, return externalIds, total, nil } -func (api AuthAPI) UpdateUser(requestInfo RequestInfo, externalId string, newPath string) (*User, error) { +func (api WorkerAPI) UpdateUser(requestInfo RequestInfo, externalId string, newPath string) (*User, error) { if !IsValidPath(newPath) { return nil, &Error{ Code: INVALID_PARAMETER_ERROR, @@ -256,7 +256,7 @@ func (api AuthAPI) UpdateUser(requestInfo RequestInfo, externalId string, newPat } -func (api AuthAPI) RemoveUser(requestInfo RequestInfo, externalId string) error { +func (api WorkerAPI) RemoveUser(requestInfo RequestInfo, externalId string) error { // Call repo to retrieve the user user, err := api.GetUserByExternalID(requestInfo, externalId) if err != nil { @@ -291,7 +291,7 @@ func (api AuthAPI) RemoveUser(requestInfo RequestInfo, externalId string) error return nil } -func (api AuthAPI) ListGroupsByUser(requestInfo RequestInfo, filter *Filter) ([]UserGroups, int, error) { +func (api WorkerAPI) ListGroupsByUser(requestInfo RequestInfo, filter *Filter) ([]UserGroups, int, error) { // Check parameters var total int orderByValidColumns := api.UserRepo.OrderByValidColumns(USER_ACTION_LIST_GROUPS_FOR_USER) diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index 562893a..e9288fe 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -3,7 +3,6 @@ package main import ( "flag" "fmt" - "net/http" "os" "os/signal" "syscall" @@ -58,12 +57,9 @@ func main() { }() proxy.Logger.Infof("Server running in %v:%v", proxy.Host, proxy.Port) - if proxy.CertFile != "" && proxy.KeyFile != "" { - proxy.Logger.Error(http.ListenAndServeTLS(proxy.Host+":"+proxy.Port, proxy.CertFile, proxy.KeyFile, internalhttp.ProxyHandlerRouter(proxy)).Error()) - } else { - proxy.Logger.Error(http.ListenAndServe(proxy.Host+":"+proxy.Port, internalhttp.ProxyHandlerRouter(proxy)).Error()) - } + ps := internalhttp.NewProxy(proxy) + ps.Configuration() + proxy.Logger.Error(ps.Run().Error()) os.Exit(foulkon.CloseProxy()) - } diff --git a/cmd/worker/main.go b/cmd/worker/main.go index ba82bd5..c61ef85 100755 --- a/cmd/worker/main.go +++ b/cmd/worker/main.go @@ -3,7 +3,6 @@ package main import ( "flag" "fmt" - "net/http" "os" @@ -60,12 +59,9 @@ func main() { }() core.Logger.Infof("Server running in %v:%v", core.Host, core.Port) - if core.CertFile != "" && core.KeyFile != "" { - core.Logger.Error(http.ListenAndServeTLS(core.Host+":"+core.Port, core.CertFile, core.KeyFile, internalhttp.WorkerHandlerRouter(core)).Error()) - } else { - core.Logger.Error(http.ListenAndServe(core.Host+":"+core.Port, internalhttp.WorkerHandlerRouter(core)).Error()) - } + ws := internalhttp.NewWorker(core, internalhttp.WorkerHandlerRouter(core)) + ws.Configuration() + core.Logger.Error(ws.Run().Error()) os.Exit(foulkon.CloseWorker()) - } diff --git a/database/postgresql/postgres_db.go b/database/postgresql/postgres_db.go index 5885d6a..2f160ba 100644 --- a/database/postgresql/postgres_db.go +++ b/database/postgresql/postgres_db.go @@ -45,7 +45,7 @@ func InitDb(datasourcename string, idleConns string, maxOpenConns string, connTT } // Create tables if not exist - err = db.AutoMigrate(&User{}, &Group{}, &Policy{}, &Statement{}, &GroupUserRelation{}, &GroupPolicyRelation{}).Error + err = db.AutoMigrate(&User{}, &Group{}, &Policy{}, &Statement{}, &GroupUserRelation{}, &GroupPolicyRelation{}, &ProxyResource{}).Error if err != nil { return nil, err } @@ -158,3 +158,18 @@ func (u PostgresRepo) OrderByValidColumns(action string) []string { return nil } } + +// ProxyResource table +type ProxyResource struct { + ID string `gorm:"primary_key"` + Host string `gorm:"not null"` + Url string `gorm:"not null;unique_index:idx_url_method"` + Method string `gorm:"not null;unique_index:idx_url_method"` + Urn string `gorm:"not null;unique"` + Action string `gorm:"not null"` +} + +// ProxyResource's table name +func (ProxyResource) TableName() string { + return "proxy_resources" +} diff --git a/database/postgresql/postgres_db_test.go b/database/postgresql/postgres_db_test.go index eda7385..c12869b 100644 --- a/database/postgresql/postgres_db_test.go +++ b/database/postgresql/postgres_db_test.go @@ -416,3 +416,26 @@ func getStatementsCountFiltered(id string, policyId string, effect string, actio return number, nil } + +// PROXY + +func cleanProxyResourcesTable() error { + if err := repoDB.Dbmap.Delete(&ProxyResource{}).Error; err != nil { + return err + } + return nil +} + +func insertProxyResource(pr ProxyResource) error { + err := repoDB.Dbmap.Exec("INSERT INTO public.proxy_resources (id, host, url, method, urn, action) VALUES (?, ?, ?, ?, ?, ?)", + pr.ID, pr.Host, pr.Url, pr.Method, pr.Urn, pr.Action).Error + + // Error handling + if err != nil { + return &database.Error{ + Code: database.INTERNAL_ERROR, + Message: err.Error(), + } + } + return nil +} diff --git a/database/postgresql/proxy.go b/database/postgresql/proxy.go new file mode 100644 index 0000000..db574c4 --- /dev/null +++ b/database/postgresql/proxy.go @@ -0,0 +1,44 @@ +package postgresql + +import ( + "github.com/Tecsisa/foulkon/api" + "github.com/Tecsisa/foulkon/database" +) + +// PROXY REPOSITORY IMPLEMENTATION + +func (pr PostgresRepo) GetProxyResources() ([]api.ProxyResource, error) { + resources := []ProxyResource{} + query := pr.Dbmap.Find(&resources) + + // Error Handling + if err := query.Error; err != nil { + return nil, &database.Error{ + Code: database.INTERNAL_ERROR, + Message: err.Error(), + } + } + // Transform proxyResources to API domain + var proxyResources []api.ProxyResource + if resources != nil { + proxyResources = make([]api.ProxyResource, len(resources), cap(resources)) + for i, pr := range resources { + proxyResources[i] = *dbResourceToApiResource(&pr) + } + } + return proxyResources, nil +} + +// PRIVATE HELPER METHODS + +// Transform a proxyResource retrieved from db into a proxyResource for API +func dbResourceToApiResource(pr *ProxyResource) *api.ProxyResource { + return &api.ProxyResource{ + ID: pr.ID, + Host: pr.Host, + Url: pr.Url, + Method: pr.Method, + Urn: pr.Urn, + Action: pr.Action, + } +} diff --git a/database/postgresql/proxy_test.go b/database/postgresql/proxy_test.go new file mode 100644 index 0000000..629957c --- /dev/null +++ b/database/postgresql/proxy_test.go @@ -0,0 +1,82 @@ +package postgresql + +import ( + "testing" + + "github.com/Tecsisa/foulkon/api" + "github.com/Tecsisa/foulkon/database" + "github.com/kylelemons/godebug/pretty" +) + +func TestPostgresRepo_GetProxyResources(t *testing.T) { + testcases := map[string]struct { + // Previous data + previousResources []ProxyResource + // Postgres Repo Args + proxyResources []api.ProxyResource + // Expected result + expectedResponse []api.ProxyResource + expectedError *database.Error + }{ + + "OkCase": { + previousResources: []ProxyResource{ + { + ID: "ID", + Host: "host", + Url: "/url", + Method: "Method", + Urn: "urn", + Action: "action", + }, + }, + proxyResources: []api.ProxyResource{ + { + ID: "ID", + Host: "host", + Url: "/url", + Method: "Method", + Urn: "urn", + Action: "action", + }, + }, + expectedResponse: []api.ProxyResource{ + { + ID: "ID", + Host: "host", + Url: "/url", + Method: "Method", + Urn: "urn", + Action: "action", + }, + }, + }, + } + + for n, test := range testcases { + // Clean proxy_resource database + cleanProxyResourcesTable() + + // Insert previous data + if test.previousResources != nil { + for _, previousResource := range test.previousResources { + if err := insertProxyResource(previousResource); err != nil { + t.Errorf("Test %v failed. Unexpected error inserting previous proxy resources: %v", n, err) + continue + } + } + } + + // Call to repository to get resources + res, err := repoDB.GetProxyResources() + if err != nil { + t.Errorf("Test %v failed. Unexpected error: %v", n, err) + continue + } + // Check response + if diff := pretty.Compare(res, test.expectedResponse); diff != "" { + t.Errorf("Test %v failed. Received different responses (received/wanted) %v", n, diff) + continue + } + } +} diff --git a/dist/proxy.toml b/dist/proxy.toml index 1d94697..2c36283 100644 --- a/dist/proxy.toml +++ b/dist/proxy.toml @@ -14,26 +14,12 @@ level = "debug" [logger.file] dir = "/tmp/foulkon/proxy.log" -# Resources definition example -[[resources]] - id = "resource1" - host = "https://httpbin.org/" - url = "/get" - method = "GET" - urn = "urn:ews:example:instance1:resource/get" - action = "example:get" -[[resources]] - id = "resource2" - host = "https://httpbin.org/" - url = "/status/:code" - method = "GET" - urn = "urn:ews:example:instance1:resource/status/{code}" - action = "example:getCode" -[[resources]] - id = "resource3" - host = "https://httpbin.org/" - url = "/post" - method = "POST" - urn = "urn:ews:example:instance1:resource/post" - action = "example:post" - +# Database config +[database] +type = "postgres" + # Postgres database config + [database.postgres] + datasourcename = "postgres://foulkon:password@localhost:5432/foulkondb?sslmode=disable" + idleconns = "5" + maxopenconns = "20" + connttl = "300" \ No newline at end of file diff --git a/dist/test/cert.pem b/dist/test/cert.pem new file mode 100644 index 0000000..06bf26e --- /dev/null +++ b/dist/test/cert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFXzCCA0egAwIBAgIJALbyfCgRu7a+MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwIBcNMTYxMDIwMTUwODQ5WhgPMjI5MDA4MDUxNTA4NDla +MEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJ +bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQC9MTOTP5UXbZCCpzj/aDglf8DgXhUjcr/xJmor9LnWFOooyPF81R/y +csYA3+OBBuPm0WMDekEPrS5Z8AB/UCkrLMgMk7l89nQiakDJ+4lLHNhg/w0TowDg +2dxvTWdOiGVpwZColvLYT8+YyvWd/PisY+PhyVQguGUpRX1VNB8C+YNybxDabJBu +AQH5yp19JdH04bnO5K1T38AV5/g8tk3Zb2YxBzKDvBy/2JbWRBBMRZlWYYRA1zUh +zXqXS+Lc4bwQeK77kHVmTYMvZ8ziMaIoccalufCYzSECqndM1qVkhXqHifH6TVIQ +zE7XohCsu0b4ImA62jd3TACUUE5WjA7ifBDmIwMy8oRdFftVCIjvL+JOSPIwVVh/ +ULu3MD4BFzjJBAN6CEZarW28/fQ+Hd+ClPZC5WZfCfuXRjvqAzKWKaOywB+1Sp88 +M8T4VBK0+LGbdRJuURqm97qyUooImLspjxS0ka94fbgU/kgMlSW15GAZyp4yV5Ka +e3tmrfovv6hLFUiUBx/ZvQtGmUpYGF15BwGWSj38EHzPphY1kliLYgl3waZQsvD4 +CA2oHbcuyL3GVVON+UxypLKaHM2IRANJontNiXIXpgtt4+QqKGx5+9lZqSv2rVVs +O3/RmY8ntccrUQJPy9qXT6EbaQZ2dQ3bZY1uotlp44/sS5lJr9e4BQIDAQABo1Aw +TjAdBgNVHQ4EFgQU9wJ3VQ6s0UvG1Q64rcKuPYz4ZQkwHwYDVR0jBBgwFoAU9wJ3 +VQ6s0UvG1Q64rcKuPYz4ZQkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEAFJccAB9G2wEVhm5zC5z/caR8MQS6f8mf0lwRrFPa/ljgtLwafjBnEy6NTkIO +bZN081OqrjfhorUSd8mVatuioVHgUiz4gMLY10cOG9z7N6sklKYx25duqowS3uqj +X2kBPXz4sJ0qvtovGainXlwaqe46z3QECTLyh5SxS/EIjYArsuMl+z03hRwULRez +QzTYdgTJPQDmpkdndwp5Cie6MmUKWF/HtBX5b4BuEmy/ehmSR1vbO9fmSGd/57EP +eFBWIQvXeW9sDmIG57gckLLHMW/AnMj0MjkAIDoNVawfNUxGKCxVDTUn16zPiBNH +GDsZHgLVrGvv2xnoCKTSp1e5DFGs6bHO5Bz2Ng37lJddd/XjqnWsCJmN8cBt5U5j +0IhyGnowbxZT+xxtMswf27+LAlQmnMheI2xBGV3o0v3eHptkaYdzvrSrfKXfrl/M +BCpGXMQIuRWIoP4jQz/oGDCJJ1gsYDNFN4kqfHEU5/RyP/GQsT86/KUoq3Ir6T+U +SSb91jwJBhDt6rA646pvqHonGtf9cU1+Zs1H85GBxI+PgBdCAuduaYl/CtG23V1k +MEzSJZn5xrj30rKkdCueo4PqCOgw81r1zpgzimtxhnbxxTtghoQu1qXpwQMXt00b +00/GvcDBDEH/YDx/GIWNqxdfS7MrlsGQrWSUSTIKe/Qbsiw= +-----END CERTIFICATE----- diff --git a/dist/test/key.pem b/dist/test/key.pem new file mode 100644 index 0000000..2b2ce7b --- /dev/null +++ b/dist/test/key.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/dist/worker.toml b/dist/worker.toml index 8fe56d4..5da985d 100644 --- a/dist/worker.toml +++ b/dist/worker.toml @@ -36,4 +36,4 @@ type = "oidc" [authenticator.oidc] issuer = "https://accounts.google.com" clientids = "google-client-identity" - + \ No newline at end of file diff --git a/foulkon/proxy.go b/foulkon/proxy.go index a9926a2..45f9f4a 100644 --- a/foulkon/proxy.go +++ b/foulkon/proxy.go @@ -8,8 +8,13 @@ import ( "fmt" + "time" + "github.com/Sirupsen/logrus" "github.com/pelletier/go-toml" + + "github.com/Tecsisa/foulkon/api" + "github.com/Tecsisa/foulkon/database/postgresql" ) var proxyLogfile *os.File @@ -27,21 +32,14 @@ type Proxy struct { CertFile string KeyFile string - // Logger - Logger *logrus.Logger + // API + ProxyApi api.ProxyResourcesAPI - // API Resources - APIResources []APIResource -} + // Refresh time + RefreshTime time.Duration -// APIResource represents external API resources to authorize -type APIResource struct { - Id string - Host string - Url string - Method string - Urn string - Action string + // Logger + Logger *logrus.Logger } func NewProxy(config *toml.TomlTree) (*Proxy, error) { @@ -64,64 +62,98 @@ func NewProxy(config *toml.TomlTree) (*Proxy, error) { loglevel = logrus.InfoLevel } - logger := &logrus.Logger{ + log = &logrus.Logger{ Out: logOut, Formatter: &logrus.JSONFormatter{}, Hooks: make(logrus.LevelHooks), Level: loglevel, } - logger.Infof("Logger type: %v, LogLevel: %v", loggerType, logger.Level.String()) - - // API Resources - resources := []APIResource{} - // Retrieve resource tree from toml config file - tree, ok := config.Get("resources").([]*toml.TomlTree) - if !ok { - err := errors.New("No resources retrieved from file") - logger.Error(err) + log.Infof("Logger type: %v, LogLevel: %v", loggerType, log.Level.String()) + + // Start DB with API + var prApi api.ProxyAPI + + dbType, err := getMandatoryValue(config, "database.type") + if err != nil { + log.Error(err) return nil, err } - for _, t := range tree { - resources = append(resources, APIResource{ - Id: getDefaultValue(t, "id", ""), - Host: getDefaultValue(t, "host", ""), - Url: getDefaultValue(t, "url", ""), - Method: getDefaultValue(t, "method", ""), - Urn: getDefaultValue(t, "urn", ""), - Action: getDefaultValue(t, "action", ""), - }) - logger.Infof("Added resource %v", getDefaultValue(t, "id", "")) + switch dbType { + case "postgres": // PostgreSQL DB + log.Info("Connecting to postgres database") + dbdsn, err := getMandatoryValue(config, "database.postgres.datasourcename") + if err != nil { + log.Error(err) + return nil, err + } + gormDB, err := postgresql.InitDb(dbdsn, + getDefaultValue(config, "database.postgres.idleconns", "5"), + getDefaultValue(config, "database.postgres.maxopenconns", "20"), + getDefaultValue(config, "database.postgres.connttl", "300"), + ) + if err != nil { + log.Error(err) + return nil, err + } + db = gormDB.DB() + log.Info("Connected to postgres database") + + // Create repository + repoDB := postgresql.PostgresRepo{ + Dbmap: gormDB, + } + prApi = api.ProxyAPI{ + ProxyRepo: repoDB, + } + + default: + err := errors.New("Unexpected db_type value in configuration file (Maybe it is empty)") + log.Error(err) + return nil, err } + prApi.Logger = log + host, err := getMandatoryValue(config, "server.host") if err != nil { - logger.Error(err) + log.Error(err) return nil, err } port, err := getMandatoryValue(config, "server.port") if err != nil { - logger.Error(err) + log.Error(err) return nil, err } workerHost, err := getMandatoryValue(config, "server.worker-host") if err != nil { - logger.Error(err) + log.Error(err) + return nil, err + } + + refresh, err := time.ParseDuration(getDefaultValue(config, "resources.refresh", "10s")) + if err != nil { + log.Error(err) return nil, err } return &Proxy{ - Host: host, - Port: port, - WorkerHost: workerHost, - CertFile: getDefaultValue(config, "server.certfile", ""), - KeyFile: getDefaultValue(config, "server.keyfile", ""), - Logger: logger, - APIResources: resources, + Host: host, + Port: port, + WorkerHost: workerHost, + CertFile: getDefaultValue(config, "server.certfile", ""), + KeyFile: getDefaultValue(config, "server.keyfile", ""), + Logger: log, + ProxyApi: prApi, + RefreshTime: refresh, }, nil } func CloseProxy() int { status := 0 + if err := db.Close(); err != nil { + log.Errorf("Couldn't close DB connection: %v", err) + status = 1 + } if proxyLogfile != nil { if err := proxyLogfile.Close(); err != nil { fmt.Fprintf(os.Stderr, "Couldn't close logfile: %v", err) diff --git a/foulkon/worker.go b/foulkon/worker.go index d6c4f57..70343b2 100755 --- a/foulkon/worker.go +++ b/foulkon/worker.go @@ -44,6 +44,7 @@ type Worker struct { GroupApi api.GroupAPI PolicyApi api.PolicyAPI AuthzApi api.AuthzAPI + ProxyApi api.ProxyResourcesAPI // Logger Logger *logrus.Logger @@ -83,7 +84,7 @@ func NewWorker(config *toml.TomlTree) (*Worker, error) { log.Infof("Logger type: %v, LogLevel: %v", loggerType, log.Level.String()) // Start DB with API - var authApi api.AuthAPI + var authApi api.WorkerAPI dbType, err := getMandatoryValue(config, "database.type") if err != nil { @@ -114,10 +115,11 @@ func NewWorker(config *toml.TomlTree) (*Worker, error) { repoDB := postgresql.PostgresRepo{ Dbmap: gormDB, } - authApi = api.AuthAPI{ + authApi = api.WorkerAPI{ GroupRepo: repoDB, UserRepo: repoDB, PolicyRepo: repoDB, + ProxyRepo: repoDB, } default: diff --git a/glide.lock b/glide.lock index 9c5018c..ebf852c 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ hash: f40938bb4d7a5e474205c1293018b19d6d29ba1fc623ca43d39aeccace0a8714 -updated: 2016-09-29T11:43:08.991766214+02:00 +updated: 2016-10-10T13:09:16.535628366+02:00 imports: - name: github.com/dgrijalva/jwt-go version: 24c63f56522a87ec5339cc3567883f1039378fdb diff --git a/glide.yaml b/glide.yaml index facfa54..a4ef9cc 100644 --- a/glide.yaml +++ b/glide.yaml @@ -17,4 +17,4 @@ import: - package: github.com/pelletier/go-toml version: 0.3.5 - package: github.com/kylelemons/godebug - version: eadb3ce320cbab8393bea5ca17bebac3f78a021b + version: eadb3ce320cbab8393bea5ca17bebac3f78a021b \ No newline at end of file diff --git a/http/handler.go b/http/handler.go index 99405c6..7ff1a1d 100755 --- a/http/handler.go +++ b/http/handler.go @@ -360,20 +360,6 @@ func (ph *ProxyHandler) RespondInternalServerError(w http.ResponseWriter, proxyE w.Write(b) } -// Handler returns an http.Handler for the Proxy including all resources defined in proxy file. -func ProxyHandlerRouter(proxy *foulkon.Proxy) http.Handler { - // Create the muxer to handle the actual endpoints - router := httprouter.New() - - proxyHandler := ProxyHandler{proxy: proxy, client: http.DefaultClient} - - for _, res := range proxy.APIResources { - router.Handle(res.Method, res.Url, proxyHandler.HandleRequest(res)) - } - - return router -} - // Private Helper Methods func writeErrorWithStatus(w http.ResponseWriter, apiError *api.Error, statusCode int) (http.ResponseWriter, error) { b, err := json.Marshal(apiError) diff --git a/http/http_test.go b/http/http_test.go index 00059e7..908f4d5 100644 --- a/http/http_test.go +++ b/http/http_test.go @@ -16,6 +16,7 @@ import ( "github.com/Tecsisa/foulkon/middleware/auth" "github.com/Tecsisa/foulkon/middleware/logger" "github.com/Tecsisa/foulkon/middleware/xrequestid" + "github.com/julienschmidt/httprouter" ) const ( @@ -53,6 +54,9 @@ const ( GetAuthorizedGroupsMethod = "GetAuthorizedGroups" GetAuthorizedPoliciesMethod = "GetAuthorizedPolicies" GetAuthorizedExternalResourcesMethod = "GetAuthorizedExternalResources" + + // PROXY API + GetProxyResourcesMethod = "GetProxyResources" ) // Test server used to test handlers @@ -60,6 +64,7 @@ var server *httptest.Server var proxy *httptest.Server var testApi *TestAPI var authConnector *TestConnector +var proxyCore *foulkon.Proxy var testFilter = &api.Filter{ PathPrefix: "", Org: "", @@ -134,7 +139,6 @@ func TestMain(m *testing.M) { // Authenticator middleware authenticatorMiddleware := auth.NewAuthenticatorMiddleware(authConnector, adminUser, adminPassword) middlewares[middleware.AUTHENTICATOR_MIDDLEWARE] = authenticatorMiddleware - log.Infof("Created authenticator with admin username %v", adminUser) // X-Request-Id middleware xrequestidMiddleware := xrequestid.NewXRequestIdMiddleware() @@ -156,62 +160,13 @@ func TestMain(m *testing.M) { server = httptest.NewServer(WorkerHandlerRouter(worker)) - proxyCore := &foulkon.Proxy{ + proxyCore = &foulkon.Proxy{ Logger: log, WorkerHost: server.URL, - APIResources: []foulkon.APIResource{ - { - Id: "resource1", - Host: server.URL, - Url: USER_ID_URL, - Method: "GET", - Urn: "urn:ews:example:instance1:resource/{userid}", - Action: "example:user", - }, - { - Id: "hostUnreachable", - Host: "fail", - Url: "/fail", - Method: "GET", - Urn: "urn:ews:example:instance1:resource/fail", - Action: "example:fail", - }, - { - Id: "invalidHost", - Host: "%&", - Url: "/invalid", - Method: "GET", - Urn: "urn:ews:example:instance1:resource/invalid", - Action: "example:invalid", - }, - { - Id: "invalidUrn", - Host: server.URL, - Url: "/invalidUrn", - Method: "GET", - Urn: "%&", - Action: "example:invalid", - }, - { - Id: "urnPrefix", - Host: server.URL, - Url: "/urnPrefix", - Method: "GET", - Urn: "urn:*", - Action: "&%", - }, - { - Id: "invalidAction", - Host: server.URL, - Url: "/invalidAction", - Method: "GET", - Urn: "urn:ews:example:instance1:resource/user", - Action: "&%", - }, - }, + ProxyApi: testApi, } - proxy = httptest.NewServer(ProxyHandlerRouter(proxyCore)) + proxy = httptest.NewServer(proxyHandlerRouter(proxyCore)) // Run tests result := m.Run() @@ -259,6 +214,8 @@ func makeTestApi() *TestAPI { testApi.ArgsIn[GetAuthorizedPoliciesMethod] = make([]interface{}, 4) testApi.ArgsIn[GetAuthorizedExternalResourcesMethod] = make([]interface{}, 3) + testApi.ArgsIn[GetProxyResourcesMethod] = make([]interface{}, 0) + testApi.ArgsOut[AddUserMethod] = make([]interface{}, 2) testApi.ArgsOut[GetUserByExternalIdMethod] = make([]interface{}, 2) testApi.ArgsOut[ListUsersMethod] = make([]interface{}, 3) @@ -290,6 +247,8 @@ func makeTestApi() *TestAPI { testApi.ArgsOut[GetAuthorizedPoliciesMethod] = make([]interface{}, 2) testApi.ArgsOut[GetAuthorizedExternalResourcesMethod] = make([]interface{}, 2) + testApi.ArgsOut[GetProxyResourcesMethod] = make([]interface{}, 2) + return testApi } @@ -684,6 +643,20 @@ func (t TestAPI) GetAuthorizedExternalResources(authenticatedUser api.RequestInf return resourcesToReturn, err } +// PROXY API + +func (t TestAPI) GetProxyResources() ([]api.ProxyResource, error) { + var proxyResources []api.ProxyResource + if t.ArgsOut[GetProxyResourcesMethod][0] != nil { + proxyResources = t.ArgsOut[GetProxyResourcesMethod][0].([]api.ProxyResource) + } + var err error + if t.ArgsOut[GetProxyResourcesMethod][1] != nil { + err = t.ArgsOut[GetProxyResourcesMethod][1].(error) + } + return proxyResources, err +} + // Private helper methods func addQueryParams(filter *api.Filter, r *http.Request) { @@ -697,3 +670,67 @@ func addQueryParams(filter *api.Filter, r *http.Request) { r.URL.RawQuery = q.Encode() } } + +func proxyHandlerRouter(proxy *foulkon.Proxy) http.Handler { + // Create the muxer to handle the actual endpoints + router := httprouter.New() + + proxyHandler := ProxyHandler{proxy: proxy, client: http.DefaultClient} + + APIResources := []api.ProxyResource{ + { + ID: "resource1", + Host: server.URL, + Url: USER_ID_URL, + Method: "GET", + Urn: "urn:ews:example:instance1:resource/{userid}", + Action: "example:user", + }, + { + ID: "hostUnreachable", + Host: "fail", + Url: "/fail", + Method: "GET", + Urn: "urn:ews:example:instance1:resource/fail", + Action: "example:fail", + }, + { + ID: "invalidHost", + Host: "%&", + Url: "/invalid", + Method: "GET", + Urn: "urn:ews:example:instance1:resource/invalid", + Action: "example:invalid", + }, + { + ID: "invalidUrn", + Host: server.URL, + Url: "/invalidUrn", + Method: "GET", + Urn: "%&", + Action: "example:invalid", + }, + { + ID: "urnPrefix", + Host: server.URL, + Url: "/urnPrefix", + Method: "GET", + Urn: "urn:*", + Action: "&%", + }, + { + ID: "invalidAction", + Host: server.URL, + Url: "/invalidAction", + Method: "GET", + Urn: "urn:ews:example:instance1:resource/user", + Action: "&%", + }, + } + + for _, res := range APIResources { + router.Handle(res.Method, res.Url, proxyHandler.HandleRequest(res)) + } + + return router +} diff --git a/http/proxy.go b/http/proxy.go index 925f17a..a4b940a 100644 --- a/http/proxy.go +++ b/http/proxy.go @@ -10,7 +10,6 @@ import ( "strings" "github.com/Tecsisa/foulkon/api" - "github.com/Tecsisa/foulkon/foulkon" "github.com/Tecsisa/foulkon/middleware" "github.com/julienschmidt/httprouter" "github.com/satori/go.uuid" @@ -25,9 +24,15 @@ const ( FORBIDDEN_ERROR = "ForbiddenError" ) +// RESPONSES + +type ProxyResources struct { + Resources []api.ProxyResource `json:"resources, omitempty"` +} + var rUrnParam, _ = regexp.Compile(`\{(\w+)\}`) -func (h *ProxyHandler) HandleRequest(resource foulkon.APIResource) httprouter.Handle { +func (h *ProxyHandler) HandleRequest(resource api.ProxyResource) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { requestID := uuid.NewV4().String() w.Header().Set(middleware.REQUEST_ID_HEADER, requestID) @@ -67,6 +72,7 @@ func (h *ProxyHandler) HandleRequest(resource foulkon.APIResource) httprouter.Ha } } + defer res.Body.Close() buffer := new(bytes.Buffer) if _, err := buffer.ReadFrom(res.Body); err != nil { h.TransactionErrorLog(r, requestID, workerRequestID, fmt.Sprintf("Error reading response from destination: %v", err.Error())) @@ -125,6 +131,8 @@ func (h *ProxyHandler) checkAuthorization(r *http.Request, urn string, action st return workerRequestID, getErrorMessage(HOST_UNREACHABLE, err.Error()) } + defer res.Body.Close() + workerRequestID = res.Header.Get(middleware.REQUEST_ID_HEADER) switch res.StatusCode { diff --git a/http/server.go b/http/server.go new file mode 100644 index 0000000..f0b6c8c --- /dev/null +++ b/http/server.go @@ -0,0 +1,202 @@ +package http + +import ( + "net/http" + + "time" + + "crypto/tls" + "net" + + "sync" + + "github.com/Tecsisa/foulkon/api" + "github.com/Tecsisa/foulkon/foulkon" + "github.com/julienschmidt/httprouter" + "github.com/kylelemons/godebug/pretty" +) + +type ReloadHandlerFunc func(watch *ProxyServer) + +// ProxyServer struct with reload Handler extension +type ProxyServer struct { + certFile string + keyFile string + + resourceLock sync.Mutex + reloadFunc ReloadHandlerFunc + refreshTime time.Duration + + reloadServe chan struct{} + currentResources []api.ProxyResource + http.Server +} + +// WorkerServer struct +type WorkerServer struct { + certFile string + keyFile string + + http.Server +} + +// Server interface that WorkerServer and ProxyServer have to implement +type Server interface { + Run() error + Configuration() error +} + +// Run starts an HTTP WorkerServer +func (ws *WorkerServer) Run() error { + var err error + if ws.certFile != "" || ws.keyFile != "" { + err = ws.ListenAndServeTLS(ws.certFile, ws.keyFile) + } else { + err = ws.ListenAndServe() + } + + return err +} + +// Configuration an HTTP ProxyServer with a given address +func (ps *ProxyServer) Configuration() error { + if ps.certFile != "" || ps.keyFile != "" { + if ps.Addr == "" { + ps.Addr = ":https" + } + + if !strSliceContains(ps.TLSConfig.NextProtos, "http/1.1") { + ps.TLSConfig.NextProtos = append(ps.TLSConfig.NextProtos, "http/1.1") + } + + configHasCert := len(ps.TLSConfig.Certificates) > 0 || ps.TLSConfig.GetCertificate != nil + + if !configHasCert || ps.certFile != "" || ps.keyFile != "" { + var err error + ps.TLSConfig.Certificates = make([]tls.Certificate, 1) + ps.TLSConfig.Certificates[0], err = tls.LoadX509KeyPair(ps.certFile, ps.keyFile) + if err != nil { + return err + } + } + } + + if ps.Addr == "" { + ps.Addr = ":http" + } + + return nil +} + +// Configuration an HTTP WorkerServer +func (ws *WorkerServer) Configuration() error { return nil } + +// Run starts an HTTP ProxyServer +func (ps *ProxyServer) Run() error { + // Call reloadFunc every refreshTime + timer := time.NewTicker(ps.refreshTime) + // now wait for the other times when we needed to + go func() { + for range timer.C { + // change the handler + ps.reloadFunc(ps) + ps.reloadServe <- struct{}{} // reset the listening binding + } + }() + + var err error + ln, err := net.Listen("tcp", ps.Addr) + if err != nil { + return err + } + + for { + l := ln.(*net.TCPListener) + defer l.Close() + go func(l net.Listener) { + err = ps.Serve(l) + }(l) + if err != nil { + return err + } + <-ps.reloadServe + } +} + +// NewProxy returns a new ProxyServer +func NewProxy(proxy *foulkon.Proxy) Server { + // Initialization + ps := new(ProxyServer) + ps.reloadServe = make(chan struct{}, 1) + ps.TLSConfig = &tls.Config{} + + // Set Proxy parameters + ps.certFile = proxy.CertFile + ps.keyFile = proxy.KeyFile + + ps.Addr = proxy.Host + ":" + proxy.Port + ps.refreshTime = proxy.RefreshTime + ps.reloadFunc = ps.RefreshResources(proxy) + + ps.reloadFunc(ps) + + return ps +} + +// NewWorker returns a new WorkerServer +func NewWorker(worker *foulkon.Worker, h http.Handler) Server { + ws := new(WorkerServer) + ws.certFile = worker.CertFile + ws.keyFile = worker.KeyFile + ws.Addr = worker.Host + ":" + worker.Port + + ws.Handler = h + + return ws +} + +// RefreshResources implements reloadFunc +func (ps *ProxyServer) RefreshResources(proxy *foulkon.Proxy) func(s *ProxyServer) { + return func(srv *ProxyServer) { + router := httprouter.New() + + proxyHandler := ProxyHandler{proxy: proxy, client: http.DefaultClient} + + // Get proxy resources + newProxyResources, err := proxy.ProxyApi.GetProxyResources() + if err != nil { + proxy.Logger.Errorf("Unexpected error reading proxy resources from database %v", err) + return + } + + if diff := pretty.Compare(srv.currentResources, newProxyResources); diff != "" { + + defer srv.resourceLock.Unlock() + srv.resourceLock.Lock() + // writer lock + ps.currentResources = newProxyResources + + proxy.Logger.Info("Updating resources ...") + for _, res := range newProxyResources { + // Check if Url is empty. Handle doesn't accept empty path + if res.Url == "" { + res.Url = "/" + } + router.Handle(res.Method, res.Url, proxyHandler.HandleRequest(res)) + } + // TODO: test when resources are empty + // If we had resources and those were deleted then handler must be + // created with empty router. + ps.Server.Handler = router + } + } +} + +func strSliceContains(ss []string, s string) bool { + for _, v := range ss { + if v == s { + return true + } + } + return false +} diff --git a/http/server_test.go b/http/server_test.go new file mode 100644 index 0000000..0db5b86 --- /dev/null +++ b/http/server_test.go @@ -0,0 +1,409 @@ +package http + +import ( + "testing" + + "crypto/tls" + "path/filepath" + + "net/http" + + "time" + + logrusTest "github.com/Sirupsen/logrus/hooks/test" + "github.com/Tecsisa/foulkon/api" + "github.com/Tecsisa/foulkon/foulkon" + "github.com/julienschmidt/httprouter" + "github.com/kylelemons/godebug/pretty" +) + +func TestNewWorker(t *testing.T) { + // Args + worker := &foulkon.Worker{ + Host: "host", + Port: "port", + CertFile: "cert", + KeyFile: "key", + } + handler := httprouter.New() + // Call func + srv := NewWorker(worker, handler) + ws := srv.(*WorkerServer) + + // Check responses + if diff := pretty.Compare(ws.Addr, worker.Host+":"+worker.Port); diff != "" { + t.Errorf("Test failed. Received different Addr (received/wanted) %v", diff) + return + } + + if diff := pretty.Compare(ws.certFile, worker.CertFile); diff != "" { + t.Errorf("Test failed. Received different certFile (received/wanted) %v", diff) + return + } + + if diff := pretty.Compare(ws.keyFile, worker.KeyFile); diff != "" { + t.Errorf("Test failed. Received different keyFile (received/wanted) %v", diff) + return + } + + if diff := pretty.Compare(ws.Handler, handler); diff != "" { + t.Errorf("Test failed. Received different keyFile (received/wanted) %v", diff) + return + } +} + +func TestNewProxy(t *testing.T) { + testApi := makeTestApi() + logger, hook := logrusTest.NewNullLogger() + testcases := map[string]struct { + proxy *foulkon.Proxy + + getProxyResourcesMethod []api.ProxyResource + getProxyResourcesError error + + expectedResources []api.ProxyResource + expectedError string + }{ + "OKCase": { + proxy: &foulkon.Proxy{ + Host: "host", + Port: "port", + CertFile: "cert", + KeyFile: "key", + RefreshTime: 10, + ProxyApi: testApi, + Logger: logger, + }, + getProxyResourcesMethod: []api.ProxyResource{ + { + ID: "ID2", + Host: "host2", + Url: "/url2", + Method: "Method2", + Urn: "urn2", + Action: "action2", + }, + }, + expectedResources: []api.ProxyResource{ + { + ID: "ID2", + Host: "host2", + Url: "/url2", + Method: "Method2", + Urn: "urn2", + Action: "action2", + }, + }, + }, + "OKCaseEmptyResourceURL": { + proxy: &foulkon.Proxy{ + Host: "host", + Port: "port", + CertFile: "cert", + KeyFile: "key", + RefreshTime: 10, + ProxyApi: testApi, + Logger: logger, + }, + getProxyResourcesMethod: []api.ProxyResource{ + { + ID: "ID2", + Host: "host2", + Url: "", + Method: "Method2", + Urn: "urn2", + Action: "action2", + }, + }, + expectedResources: []api.ProxyResource{ + { + ID: "ID2", + Host: "host2", + Url: "", + Method: "Method2", + Urn: "urn2", + Action: "action2", + }, + }, + }, + "OKCaseEmptyResources": { + proxy: &foulkon.Proxy{ + Host: "host", + Port: "port", + CertFile: "cert", + KeyFile: "key", + RefreshTime: 10, + ProxyApi: testApi, + Logger: logger, + }, + getProxyResourcesMethod: []api.ProxyResource{}, + expectedResources: []api.ProxyResource{}, + }, + "ErrorCaseGetProxyResources": { + proxy: &foulkon.Proxy{ + Host: "host", + Port: "port", + CertFile: "cert", + KeyFile: "key", + RefreshTime: 10, + ProxyApi: testApi, + Logger: logger, + }, + getProxyResourcesError: api.Error{ + Code: INTERNAL_SERVER_ERROR, + Message: "Unknow error", + }, + expectedError: "Unexpected error reading proxy resources from database Code: InternalServerError, Message: Unknow error", + }, + } + for n, test := range testcases { + testApi.ArgsOut[GetProxyResourcesMethod][0] = test.getProxyResourcesMethod + testApi.ArgsOut[GetProxyResourcesMethod][1] = test.getProxyResourcesError + + // Call func + srv := NewProxy(test.proxy) + ps := srv.(*ProxyServer) + + if test.expectedError != "" { + // Check error + if diff := pretty.Compare(test.expectedError, hook.LastEntry().Message); diff != "" { + t.Errorf("Test %v failed. Received different errors (received/wanted) %v", n, diff) + continue + } + } else { + // Check responses + if diff := pretty.Compare(ps.Addr, test.proxy.Host+":"+test.proxy.Port); diff != "" { + t.Errorf("Test %v failed. Received different Addr (received/wanted) %v", n, diff) + continue + } + + if diff := pretty.Compare(ps.certFile, test.proxy.CertFile); diff != "" { + t.Errorf("Test %v failed. Received different certFile (received/wanted) %v", n, diff) + continue + } + + if diff := pretty.Compare(ps.keyFile, test.proxy.KeyFile); diff != "" { + t.Errorf("Test %v failed. Received different keyFile (received/wanted) %v", n, diff) + continue + } + + if diff := pretty.Compare(ps.refreshTime, test.proxy.RefreshTime); diff != "" { + t.Errorf("Test %v failed. Received different refreshTime (received/wanted) %v", n, diff) + continue + } + + if diff := pretty.Compare(ps.currentResources, test.expectedResources); diff != "" { + t.Errorf("Test %v failed. Received different resources (received/wanted) %v", n, diff) + continue + } + } + } +} + +func Test_strSliceContains(t *testing.T) { + testcases := map[string]struct { + ss []string + s string + expectedResult bool + }{ + "OKcaseTrue": { + ss: []string{"a", "b"}, + s: "a", + expectedResult: true, + }, + "OKcaseFalse": { + ss: []string{"a", "b"}, + s: "c", + expectedResult: false, + }, + } + + for n, test := range testcases { + result := strSliceContains(test.ss, test.s) + if result != test.expectedResult { + t.Errorf("Test %v failed. Received different responses (received/wanted)", n) + continue + } + } +} + +func TestWorkerServer_Configuration(t *testing.T) { + ws := WorkerServer{} + err := ws.Configuration() + if diff := pretty.Compare(err, nil); diff != "" { + t.Errorf("Test failed. Received different errors (received/wanted) %v", diff) + return + } +} + +func TestProxyServer_Configuration(t *testing.T) { + certFile, _ := filepath.Abs("../dist/test/cert.pem") + keyFile, _ := filepath.Abs("../dist/test/key.pem") + testcases := map[string]struct { + ps ProxyServer + + expectedAddr string + expectedError string + }{ + "OKcase": { + ps: ProxyServer{ + certFile: "", + keyFile: "", + }, + expectedAddr: ":http", + }, + "OKcaseTLS": { + ps: ProxyServer{ + certFile: certFile, + keyFile: keyFile, + }, + expectedAddr: ":https", + }, + "ErrorCaseTLS": { + ps: ProxyServer{ + certFile: certFile, + keyFile: "", + }, + expectedError: "open : no such file or directory", + }, + } + + for n, test := range testcases { + var err error + test.ps.TLSConfig = &tls.Config{} + err = test.ps.Configuration() + if test.expectedError != "" { + if err != nil { + if diff := pretty.Compare(err.Error(), test.expectedError); diff != "" { + t.Errorf("Test %v failed. Received different errors (received/wanted) %v", n, diff) + continue + } + } else { + t.Errorf("Test %v failed. No errors received", n) + continue + } + } else { + if diff := pretty.Compare(test.ps.Addr, test.expectedAddr); diff != "" { + t.Errorf("Test %v failed. Received different responses (received/wanted) %v", n, diff) + continue + } + } + } +} + +func TestWorkerServer_Run(t *testing.T) { + certFile, _ := filepath.Abs("../dist/test/cert.pem") + keyFile, _ := filepath.Abs("../dist/test/key.pem") + testcases := map[string]struct { + worker *foulkon.Worker + handler http.Handler + + expectedError string + }{ + "ErrorCaseListen": { + worker: &foulkon.Worker{ + Host: "fail", + }, + handler: httprouter.New(), + expectedError: "listen tcp: lookup fail on 127.0.0.1:53: server misbehaving", + }, + "ErrorCaseListenTLS": { + worker: &foulkon.Worker{ + Host: "fail", + CertFile: certFile, + KeyFile: keyFile, + }, + handler: httprouter.New(), + expectedError: "listen tcp: lookup fail on 127.0.0.1:53: server misbehaving", + }, + } + + for n, test := range testcases { + var err error + srv := NewWorker(test.worker, test.handler) + err = srv.Run() + if diff := pretty.Compare(err.Error(), test.expectedError); diff != "" { + t.Errorf("Test %v failed. Received different responses (received/wanted) %v", n, diff) + continue + } + } +} + +func TestProxyServer_Run(t *testing.T) { + testApi := makeTestApi() + testcases := map[string]struct { + proxy *foulkon.Proxy + + expectedResources []api.ProxyResource + expectedError string + }{ + "OKCase": { + proxy: &foulkon.Proxy{ + RefreshTime: 1 * time.Millisecond, + ProxyApi: testApi, + Logger: proxyCore.Logger, + }, + expectedResources: []api.ProxyResource{ + { + ID: "ID2", + Host: "host2", + Url: "/url2", + Method: "Method2", + Urn: "urn2", + Action: "action2", + }, + }, + }, + "ErrorCaseListen": { + proxy: &foulkon.Proxy{ + Host: "fail", + RefreshTime: 1 * time.Millisecond, + ProxyApi: testApi, + Logger: proxyCore.Logger, + }, + expectedError: "listen tcp: lookup fail on 127.0.0.1:53: server misbehaving", + }, + } + for n, test := range testcases { + var err error + srv := NewProxy(test.proxy) + srv.Configuration() + + if test.expectedError != "" { + err = srv.Run() + if err != nil { + if diff := pretty.Compare(err.Error(), test.expectedError); diff != "" { + t.Errorf("Test %v failed. Received different errors (received/wanted) %v", n, diff) + continue + } + } + } else { + testApi.ArgsOut[GetProxyResourcesMethod][0] = []api.ProxyResource{ + { + ID: "ID2", + Host: "host2", + Url: "/url2", + Method: "Method2", + Urn: "urn2", + Action: "action2", + }, + } + + go func() { + srv.Run() + }() + + // Wait reloadFunc + time.Sleep(5 * time.Millisecond) + + ps := srv.(*ProxyServer) + + ps.resourceLock.Lock() + if diff := pretty.Compare(ps.currentResources, test.expectedResources); diff != "" { + t.Errorf("Test %v failed. Received different responses (received/wanted) %v", n, diff) + continue + } + ps.resourceLock.Unlock() + } + } +} From 64907e40b97c96839b1ef0f74940ac91f11a40aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerson=20Pozo=20Gonz=C3=A1lez?= Date: Fri, 21 Oct 2016 13:00:15 +0200 Subject: [PATCH 2/3] fix test --- http/server.go | 15 +++++++++------ http/server_test.go | 17 ++++++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/http/server.go b/http/server.go index f0b6c8c..beed88c 100644 --- a/http/server.go +++ b/http/server.go @@ -16,7 +16,7 @@ import ( "github.com/kylelemons/godebug/pretty" ) -type ReloadHandlerFunc func(watch *ProxyServer) +type ReloadHandlerFunc func(watch *ProxyServer) bool // ProxyServer struct with reload Handler extension type ProxyServer struct { @@ -99,8 +99,9 @@ func (ps *ProxyServer) Run() error { go func() { for range timer.C { // change the handler - ps.reloadFunc(ps) - ps.reloadServe <- struct{}{} // reset the listening binding + if ps.reloadFunc(ps) { + ps.reloadServe <- struct{}{} // reset the listening binding + } } }() @@ -156,8 +157,8 @@ func NewWorker(worker *foulkon.Worker, h http.Handler) Server { } // RefreshResources implements reloadFunc -func (ps *ProxyServer) RefreshResources(proxy *foulkon.Proxy) func(s *ProxyServer) { - return func(srv *ProxyServer) { +func (ps *ProxyServer) RefreshResources(proxy *foulkon.Proxy) func(s *ProxyServer) bool { + return func(srv *ProxyServer) bool { router := httprouter.New() proxyHandler := ProxyHandler{proxy: proxy, client: http.DefaultClient} @@ -166,7 +167,7 @@ func (ps *ProxyServer) RefreshResources(proxy *foulkon.Proxy) func(s *ProxyServe newProxyResources, err := proxy.ProxyApi.GetProxyResources() if err != nil { proxy.Logger.Errorf("Unexpected error reading proxy resources from database %v", err) - return + return false } if diff := pretty.Compare(srv.currentResources, newProxyResources); diff != "" { @@ -188,7 +189,9 @@ func (ps *ProxyServer) RefreshResources(proxy *foulkon.Proxy) func(s *ProxyServe // If we had resources and those were deleted then handler must be // created with empty router. ps.Server.Handler = router + return true } + return false } } diff --git a/http/server_test.go b/http/server_test.go index 0db5b86..f5bbf62 100644 --- a/http/server_test.go +++ b/http/server_test.go @@ -10,6 +10,8 @@ import ( "time" + "strings" + logrusTest "github.com/Sirupsen/logrus/hooks/test" "github.com/Tecsisa/foulkon/api" "github.com/Tecsisa/foulkon/foulkon" @@ -305,7 +307,7 @@ func TestWorkerServer_Run(t *testing.T) { Host: "fail", }, handler: httprouter.New(), - expectedError: "listen tcp: lookup fail on 127.0.0.1:53: server misbehaving", + expectedError: "listen tcp: lookup fail", }, "ErrorCaseListenTLS": { worker: &foulkon.Worker{ @@ -314,7 +316,7 @@ func TestWorkerServer_Run(t *testing.T) { KeyFile: keyFile, }, handler: httprouter.New(), - expectedError: "listen tcp: lookup fail on 127.0.0.1:53: server misbehaving", + expectedError: "listen tcp: lookup fail", }, } @@ -322,8 +324,8 @@ func TestWorkerServer_Run(t *testing.T) { var err error srv := NewWorker(test.worker, test.handler) err = srv.Run() - if diff := pretty.Compare(err.Error(), test.expectedError); diff != "" { - t.Errorf("Test %v failed. Received different responses (received/wanted) %v", n, diff) + if !strings.Contains(err.Error(), test.expectedError) { + t.Errorf("Test %v failed. Received different errors (received: %v / wanted: %v)", n, test.expectedError, err.Error()) continue } } @@ -357,11 +359,12 @@ func TestProxyServer_Run(t *testing.T) { "ErrorCaseListen": { proxy: &foulkon.Proxy{ Host: "fail", + Port: "53", RefreshTime: 1 * time.Millisecond, ProxyApi: testApi, Logger: proxyCore.Logger, }, - expectedError: "listen tcp: lookup fail on 127.0.0.1:53: server misbehaving", + expectedError: "listen tcp: lookup fail", }, } for n, test := range testcases { @@ -372,8 +375,8 @@ func TestProxyServer_Run(t *testing.T) { if test.expectedError != "" { err = srv.Run() if err != nil { - if diff := pretty.Compare(err.Error(), test.expectedError); diff != "" { - t.Errorf("Test %v failed. Received different errors (received/wanted) %v", n, diff) + if !strings.Contains(err.Error(), test.expectedError) { + t.Errorf("Test %v failed. Received different errors (received: %v / wanted: %v)", n, test.expectedError, err.Error()) continue } } From bbb8b64b8000546ae2f1b3b5d1710c0004627ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerson=20Pozo=20Gonz=C3=A1lez?= Date: Fri, 21 Oct 2016 13:12:46 +0200 Subject: [PATCH 3/3] minor change --- http/server.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/http/server.go b/http/server.go index beed88c..dae13f9 100644 --- a/http/server.go +++ b/http/server.go @@ -159,8 +159,6 @@ func NewWorker(worker *foulkon.Worker, h http.Handler) Server { // RefreshResources implements reloadFunc func (ps *ProxyServer) RefreshResources(proxy *foulkon.Proxy) func(s *ProxyServer) bool { return func(srv *ProxyServer) bool { - router := httprouter.New() - proxyHandler := ProxyHandler{proxy: proxy, client: http.DefaultClient} // Get proxy resources @@ -171,6 +169,7 @@ func (ps *ProxyServer) RefreshResources(proxy *foulkon.Proxy) func(s *ProxyServe } if diff := pretty.Compare(srv.currentResources, newProxyResources); diff != "" { + router := httprouter.New() defer srv.resourceLock.Unlock() srv.resourceLock.Lock()