commit 6aa7257739dc301bef5c281dadf4fe32689c289f Author: Joakim Olsson Date: Tue Nov 5 21:24:54 2019 +0100 feat: initial version diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..04cd3ad --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true + +[*.go] +indent_style = tab +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ada4d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +/release diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..e65001e --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,41 @@ +variables: + GOCACHE: "${CI_PROJECT_DIR}/_go/cache" + +before_script: + - mkdir -p ${CI_PROJECT_DIR}/_go/{pkg,bin,cache} + - rm -rf /go/pkg || true + - mkdir -p /go + - ln -s ${CI_PROJECT_DIR}/_go/pkg /go/pkg + - ln -s ${CI_PROJECT_DIR}/_go/bin /go/bin + +cache: + key: "$CI_COMMIT_REF_NAME" + paths: + - _go + untracked: true + +stages: + - deps + - test + +deps: + stage: deps + image: golang:1.13 + script: + - go get -mod=readonly + +test: + stage: test + dependencies: + - deps + image: golang:1.13 + script: + - go fmt $(go list ./...) + - go vet $(go list ./...) + - unset "${!CI@}" + - CGO_ENABLED=1 go test -p 1 -mod=readonly -race -coverprofile=.testCoverage.txt -covermode=atomic -coverpkg=$(go list ./... | tr '\n' , | sed 's/,$//') ./... + - go tool cover -html=.testCoverage.txt -o coverage.html + - go tool cover -func=.testCoverage.txt + artifacts: + paths: + - coverage.html diff --git a/README.md b/README.md new file mode 100644 index 0000000..07d1c7c --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Shiny authz-client + +[![Build Status](https://gitlab.com/unboundsoftware/shiny/authz-client/badges/master/pipeline.svg)](https://gitlab.com/unboundsoftware/shiny/authz-client/commits/master)[![coverage report](https://gitlab.com/unboundsoftware/shiny/authz-client/badges/master/coverage.svg)](https://gitlab.com/unboundsoftware/shiny/authz-client/commits/master) diff --git a/client.go b/client.go new file mode 100644 index 0000000..06bdd74 --- /dev/null +++ b/client.go @@ -0,0 +1,122 @@ +package client + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" +) + +// CompanyPrivileges contains the privileges for a combination of email address and company id +type CompanyPrivileges struct { + Admin bool `json:"admin"` + Company bool `json:"company"` + Consumer bool `json:"consumer"` + Time bool `json:"time"` + Invoicing bool `json:"invoicing"` + Accounting bool `json:"accounting"` + Supplier bool `json:"supplier"` +} + +// PrivilegeAdded is the event sent when a new privilege is added +type PrivilegeAdded struct { + Email string `json:"email"` + CompanyID string `json:"companyId"` + Admin bool `json:"admin"` + Company bool `json:"company"` + Consumer bool `json:"consumer"` + Time bool `json:"time"` + Invoicing bool `json:"invoicing"` + Accounting bool `json:"accounting"` + Supplier bool `json:"supplier"` +} + +// PrivilegeHandler processes PrivilegeAdded-events and fetches the initial set of privileges from an authz-service +type PrivilegeHandler struct { + client *http.Client + baseURL string + privileges map[string]map[string]CompanyPrivileges +} + +// OptsFunc is used to configure the PrivilegeHandler +type OptsFunc func(handler *PrivilegeHandler) + +// WithBaseURL sets the base URL to the authz-service +func WithBaseURL(url string) OptsFunc { + return func(handler *PrivilegeHandler) { + handler.baseURL = url + } +} + +// New creates a new PrivilegeHandler. Pass OptsFuncs to configure. +func New(opts ...OptsFunc) *PrivilegeHandler { + handler := &PrivilegeHandler{ + client: &http.Client{}, + baseURL: "http://authz-service", + privileges: map[string]map[string]CompanyPrivileges{}, + } + for _, opt := range opts { + opt(handler) + } + return handler +} + +// Fetch the initial set of privileges from an authz-service +func (h *PrivilegeHandler) Fetch() error { + resp, err := h.client.Get(fmt.Sprintf("%s/authz", h.baseURL)) + if err != nil { + return err + } + + buff, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + err = json.Unmarshal(buff, &h.privileges) + if err != nil { + return err + } + return nil +} + +// Process privilege-related events and update the internal state +func (h *PrivilegeHandler) Process(msg interface{}) bool { + if ev, ok := msg.(*PrivilegeAdded); ok { + h.setPrivileges(ev) + return true + } + return false +} + +func (h *PrivilegeHandler) setPrivileges(ev *PrivilegeAdded) { + if priv, exists := h.privileges[ev.Email]; exists { + priv[ev.CompanyID] = CompanyPrivileges{ + Admin: ev.Admin, + Company: ev.Company, + Consumer: ev.Consumer, + Time: ev.Time, + Invoicing: ev.Invoicing, + Accounting: ev.Accounting, + Supplier: ev.Supplier, + } + } else { + h.privileges[ev.Email] = map[string]CompanyPrivileges{ + ev.CompanyID: {}, + } + h.setPrivileges(ev) + } +} + +// CompaniesByUser return a slice of company ids matching the provided email and predicate func +func (h *PrivilegeHandler) CompaniesByUser(email string, predicate func(privileges CompanyPrivileges) bool) []string { + var result []string + if p, exists := h.privileges[email]; exists { + for k, v := range p { + if predicate(v) { + result = append(result, k) + } + } + } + return result +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..2ceb739 --- /dev/null +++ b/client_test.go @@ -0,0 +1,177 @@ +package client + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" +) + +func TestPrivilegeHandler_Process_InvalidType(t *testing.T) { + handler := New(WithBaseURL("base")) + + result := handler.Process("abc") + + assert.False(t, result) +} + +func TestPrivilegeHandler_GetCompanies_Email_Not_Found(t *testing.T) { + handler := New(WithBaseURL("base")) + + companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool { + return true + }) + + assert.Empty(t, companies) +} + +func TestPrivilegeHandler_GetCompanies_No_Companies_Found(t *testing.T) { + handler := New(WithBaseURL("base")) + + result := handler.Process(&PrivilegeAdded{ + Email: "jim@example.org", + CompanyID: "abc-123", + Admin: false, + Company: false, + Consumer: false, + Time: false, + Invoicing: false, + Accounting: false, + Supplier: false, + }) + assert.True(t, result) + + companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool { + return privileges.Admin + }) + + assert.Empty(t, companies) +} + +func TestPrivilegeHandler_GetCompanies_Company_With_Company_Access_Found(t *testing.T) { + handler := New(WithBaseURL("base")) + + result := handler.Process(&PrivilegeAdded{ + Email: "jim@example.org", + CompanyID: "abc-123", + Admin: false, + Company: true, + Consumer: false, + Time: false, + Invoicing: false, + Accounting: false, + Supplier: false, + }) + assert.True(t, result) + + companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool { + return privileges.Company + }) + + assert.Equal(t, []string{"abc-123"}, companies) +} + +func TestPrivilegeHandler_GetCompanies_Company_With_Admin_Access_Found(t *testing.T) { + handler := New(WithBaseURL("base")) + + result := handler.Process(&PrivilegeAdded{ + Email: "jim@example.org", + CompanyID: "abc-123", + Admin: true, + Company: false, + Consumer: false, + Time: false, + Invoicing: false, + Accounting: false, + Supplier: false, + }) + assert.True(t, result) + + companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool { + return privileges.Admin + }) + + assert.Equal(t, []string{"abc-123"}, companies) +} + +func TestPrivilegeHandler_Fetch_Error_Response(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + })) + + baseURL := server.Listener.Addr().String() + handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL))) + + server.Close() + + err := handler.Fetch() + assert.EqualError(t, err, fmt.Sprintf("Get http://%s/authz: dial tcp %s: connect: connection refused", baseURL, baseURL)) +} + +func TestPrivilegeHandler_Fetch_Error_Unreadable_Body(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Length", "1") + })) + defer server.Close() + + baseURL := server.Listener.Addr().String() + handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL))) + + err := handler.Fetch() + assert.EqualError(t, err, "unexpected EOF") +} + +func TestPrivilegeHandler_Fetch_Error_Broken_JSON(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("{abc")) + })) + defer server.Close() + + baseURL := server.Listener.Addr().String() + handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL))) + + err := handler.Fetch() + assert.EqualError(t, err, "invalid character 'a' looking for beginning of object key string") +} + +func TestPrivilegeHandler_Fetch_Valid(t *testing.T) { + privileges := ` +{ + "jim@example.org": { + "00010203-0405-4607-8809-0a0b0c0d0e0f": { + "admin": false, + "company": true, + "consumer": false, + "time": true, + "invoicing": true, + "accounting": false, + "supplier": false + } + } +}` + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(privileges)) + })) + defer server.Close() + + baseURL := server.Listener.Addr().String() + handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL))) + + err := handler.Fetch() + assert.NoError(t, err) + expectedPrivileges := map[string]map[string]CompanyPrivileges{ + "jim@example.org": { + "00010203-0405-4607-8809-0a0b0c0d0e0f": { + Admin: false, + Company: true, + Consumer: false, + Time: true, + Invoicing: true, + Accounting: false, + Supplier: false, + }, + }, + } + assert.Equal(t, expectedPrivileges, handler.privileges) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..91dfb5b --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module gitlab.com/unboundsoftware/shiny/authz-client + +go 1.13 + +require github.com/stretchr/testify v1.4.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8fdee58 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=