feat: initial version
This commit is contained in:
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.idea
|
||||||
|
/release
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Shiny authz-client
|
||||||
|
|
||||||
|
[](https://gitlab.com/unboundsoftware/shiny/authz-client/commits/master)[](https://gitlab.com/unboundsoftware/shiny/authz-client/commits/master)
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+177
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module gitlab.com/unboundsoftware/shiny/authz-client
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require github.com/stretchr/testify v1.4.0
|
||||||
@@ -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=
|
||||||
Reference in New Issue
Block a user