Skip to content
This repository was archived by the owner on Jun 12, 2024. It is now read-only.

Commit b440554

Browse files
rdnerLucasRoesler
authored andcommitted
Add authorization middleware, id_resolver (#12)
* Add authorization middleware, id_resolver **What** - Renamed `http/middleware` -> `http/middlewares` - Added `http/middlewares/authorization` - Added `db/id_resolver`
1 parent 8af741c commit b440554

27 files changed

+1092
-19
lines changed

.circleci/config.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ jobs:
33
build:
44
docker:
55
- image: circleci/golang:1.12
6+
- image: circleci/postgres:alpine
7+
environment:
8+
POSTGRES_PASSWORD: localdev
9+
POSTGRES_USER: contiamo_test
10+
POSTGRES_DB: postgres
611
environment:
712
GO111MODULE: "on"
813
steps: # steps that comprise the `build` job
@@ -20,9 +25,19 @@ jobs:
2025
- run:
2126
name: Verify formatting
2227
command: make fmt lint
28+
- run:
29+
name: Waiting for Postgres to be ready
30+
command: |
31+
for i in `seq 1 10`;
32+
do
33+
nc -z localhost 5432 && echo Success && exit 0
34+
echo -n .
35+
sleep 1
36+
done
37+
echo Failed waiting for Postgres && exit 1
2338
- run:
2439
name: Run unit tests
25-
command: make test
40+
command: make .test-ci
2641
- save_cache: # Store cache in the /go/pkg directory
2742
key: go-mod-cache
2843
paths:

Makefile

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,28 @@ setup-env:
3131
$(shell go mod download)
3232
$(shell curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s v1.20.0)
3333

34+
.PHONY: .run-test-db
35+
.run-test-db: ## start the test db
36+
@echo "+ setup test db"
37+
@docker run --rm -d --name go-base-postgres \
38+
-p 0.0.0.0:5432:5432 \
39+
-e POSTGRES_PASSWORD=$$(cat pkg/db/test/password) \
40+
-e POSTGRES_USER=contiamo_test \
41+
-e POSTGRES_DB=postgres \
42+
postgres:alpine -c fsync=off -c full_page_writes=off -c synchronous_commit=off &>/dev/null
43+
@bash -c "export PGPASSWORD=$$(cat pkg/db/test/password); until psql -q -Ucontiamo_test -l -h localhost &>/dev/null; do echo -n .; sleep 1; done"
44+
@echo ""
45+
@echo "+ test db started"
46+
47+
.PHONY: .stop-test-db
48+
.stop-test-db: ## teardown the test db
49+
@echo "+ teardown test db"
50+
@docker rm -v -f go-base-postgres &>/dev/null
51+
52+
.PHONY: .test-ci
53+
.test-ci:
54+
go test -cover ./...
55+
3456
.PHONY: changelog
3557
changelog: ## Print git hitstory based changelog
3658
@git --no-pager log --no-merges --pretty=format:"%h : %s (by %an)" $(shell git describe --tags --abbrev=0)...HEAD
@@ -54,8 +76,10 @@ staticcheck: ## Verifies `staticcheck` passes
5476

5577
.PHONY: test
5678
test: ## Runs the go tests
79+
@$(MAKE) .run-test-db
5780
@echo "+ $@"
58-
@go test -cover ./...
81+
-@$(MAKE) .test-ci
82+
@$(MAKE) .stop-test-db
5983

6084
.PHONY: lint
6185
lint: setup-env ## Verifies `golangci-lint` passes

go.mod

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@ require (
55
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
66
github.com/bakins/net-http-recover v0.0.0-20141007104922-6cba69d01459
77
github.com/cenkalti/backoff v2.2.1+incompatible
8-
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect
8+
github.com/contiamo/goserver v0.5.0
9+
github.com/contiamo/jwt v0.2.2
910
github.com/dgrijalva/jwt-go v3.2.0+incompatible
1011
github.com/go-chi/chi v4.0.2+incompatible
1112
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible
13+
github.com/go-sql-driver/mysql v1.4.1 // indirect
1214
github.com/golang/protobuf v1.3.2
1315
github.com/google/uuid v1.1.1
1416
github.com/gorilla/websocket v1.4.0
1517
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
1618
github.com/kr/pretty v0.1.0 // indirect
1719
github.com/lib/pq v1.1.1
20+
github.com/mattn/go-sqlite3 v1.11.0 // indirect
1821
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
1922
github.com/modern-go/reflect2 v1.0.1 // indirect
2023
github.com/opentracing/opentracing-go v1.1.0
@@ -25,15 +28,13 @@ require (
2528
github.com/satori/go.uuid v1.2.0
2629
github.com/sirupsen/logrus v1.4.2
2730
github.com/stretchr/testify v1.3.0
28-
github.com/uber-go/atomic v1.4.0 // indirect
2931
github.com/uber/jaeger-client-go v2.16.0+incompatible
3032
github.com/uber/jaeger-lib v2.0.0+incompatible // indirect
3133
github.com/urfave/negroni v1.0.0
32-
go.uber.org/atomic v1.4.0 // indirect
3334
golang.org/x/lint v0.0.0-20190409202823-959b441ac422
3435
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b // indirect
3536
google.golang.org/appengine v1.4.0 // indirect
3637
google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610 // indirect
37-
google.golang.org/grpc v1.22.0
38+
google.golang.org/grpc v1.22.0 // indirect
3839
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
3940
)

go.sum

Lines changed: 47 additions & 0 deletions
Large diffs are not rendered by default.

pkg/config/database.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,26 +68,26 @@ func (cfg *Database) GetConnectionString() (connStr string, err error) {
6868
connStr = "sslmode=disable "
6969

7070
if cfg.Host != "" {
71-
connStr += fmt.Sprintf("host=%v ", cfg.Host)
71+
connStr += fmt.Sprintf("host=%s ", cfg.Host)
7272
}
7373

7474
if cfg.Port != 0 {
7575
connStr += fmt.Sprintf("port=%d ", cfg.Port)
7676
}
7777

7878
if cfg.Name != "" {
79-
connStr += fmt.Sprintf("dbname=%v ", cfg.Name)
79+
connStr += fmt.Sprintf("dbname=%s ", cfg.Name)
8080
}
8181
if cfg.Username != "" {
82-
connStr += fmt.Sprintf("user=%v ", cfg.Username)
82+
connStr += fmt.Sprintf("user=%s ", cfg.Username)
8383
}
8484
if cfg.PasswordPath != "" {
8585
pw, err := cfg.GetPassword()
8686
if err != nil {
8787
return "", err
8888
}
8989
if pw != "" {
90-
connStr += fmt.Sprintf("password=%v ", pw)
90+
connStr += fmt.Sprintf("password=%s ", pw)
9191
}
9292
}
9393

pkg/data/managers/id_resolver.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package managers
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"strings"
8+
9+
squirrel "github.com/Masterminds/squirrel"
10+
dserrors "github.com/contiamo/go-base/pkg/errors"
11+
uuid "github.com/satori/go.uuid"
12+
)
13+
14+
// IDResolver makes possible to access database records by their IDs or unique values
15+
type IDResolver interface {
16+
// Resolve returns an ID of the given record identified by the value which can be either
17+
// an UUID or a unique string value of the given secondary column.
18+
// where is a map of where statements to their list of arguments
19+
Resolve(ctx context.Context, sql squirrel.StatementBuilderType, value string, filter squirrel.Sqlizer) (string, error)
20+
// Sqlizer returns a Sqlizer interface that contains where statements for a given
21+
// filter and the ID column, so you can immediately use it with
22+
// the where of the select builder
23+
Sqlizer(ctx context.Context, sql squirrel.StatementBuilderType, value string, filter squirrel.Sqlizer) (squirrel.Sqlizer, error)
24+
}
25+
26+
type idResolver struct {
27+
table,
28+
idColumn,
29+
secondaryColumn string
30+
}
31+
32+
func (r *idResolver) Sqlizer(ctx context.Context, sql squirrel.StatementBuilderType, value string, filter squirrel.Sqlizer) (squirrel.Sqlizer, error) {
33+
id, err := r.Resolve(ctx, sql, value, filter)
34+
if err != nil {
35+
return nil, err
36+
}
37+
38+
idPred := squirrel.Eq{
39+
r.idColumn: id,
40+
}
41+
if filter == nil {
42+
return idPred, nil
43+
}
44+
return squirrel.And{
45+
filter,
46+
idPred,
47+
}, nil
48+
}
49+
50+
func (r *idResolver) Resolve(ctx context.Context, sql squirrel.StatementBuilderType, value string, filter squirrel.Sqlizer) (string, error) {
51+
if value == "" {
52+
return value, dserrors.ValidationErrors{
53+
"id": errors.New("the id parameter can't be empty"),
54+
}.Filter()
55+
}
56+
57+
uuidVal, err := uuid.FromString(strings.TrimSpace(value))
58+
if err == nil {
59+
return uuidVal.String(), nil
60+
}
61+
62+
rows, err := sql.
63+
Select(r.idColumn).
64+
From(r.table).
65+
Where(filter).
66+
Where(squirrel.Eq{r.secondaryColumn: value}).
67+
Limit(2).
68+
QueryContext(ctx)
69+
70+
if err != nil {
71+
return "", err
72+
}
73+
74+
defer rows.Close()
75+
76+
// no results at all
77+
if !rows.Next() {
78+
return "", dserrors.ErrNotFound
79+
}
80+
var id string
81+
err = rows.Scan(&id)
82+
if err != nil {
83+
return id, err
84+
}
85+
// non-unique result
86+
if rows.Next() {
87+
return "", fmt.Errorf(
88+
"id for `%s = %s` can't be resolved in `%s` due to non-unique results",
89+
r.secondaryColumn,
90+
value,
91+
r.table,
92+
)
93+
}
94+
95+
return id, err
96+
}
97+
98+
// NewIDResolver creates a new name->id resolver for a table, for example
99+
// var (
100+
// CollectionIDResolver = NewIDResolver("collections", "collection_id", "name")
101+
// TableIDResolver = NewIDResolver("tables", "table_id", "name")
102+
// )
103+
func NewIDResolver(table, idColumn, secondaryColumn string) IDResolver {
104+
return &idResolver{
105+
table: table,
106+
idColumn: idColumn,
107+
secondaryColumn: secondaryColumn,
108+
}
109+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package managers
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"io/ioutil"
7+
"testing"
8+
"time"
9+
10+
squirrel "github.com/Masterminds/squirrel"
11+
dbtest "github.com/contiamo/go-base/pkg/db/test"
12+
uuid "github.com/satori/go.uuid"
13+
"github.com/sirupsen/logrus"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func init() {
18+
logrus.SetOutput(ioutil.Discard)
19+
}
20+
21+
func Test_Sqlizer(t *testing.T) {
22+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
23+
defer cancel()
24+
_, db := dbtest.GetDatabase(t, nil)
25+
defer db.Close()
26+
27+
r := NewIDResolver("test", "id", "name")
28+
manager := NewBaseManager(db, "id_resolver_test")
29+
builder := manager.GetQueryBuilder()
30+
id := uuid.NewV4().String()
31+
where, err := r.Sqlizer(ctx, builder, id, squirrel.Eq{
32+
"some": "cool",
33+
})
34+
require.NoError(t, err)
35+
sql, _, err := where.ToSql()
36+
require.NoError(t, err)
37+
require.Equal(t, "(some = ? AND id = ?)", sql)
38+
}
39+
40+
func Test_Resolve(t *testing.T) {
41+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
42+
defer cancel()
43+
44+
_, db := dbtest.GetDatabase(t, func(ctx context.Context, db *sql.DB) error {
45+
_, err := db.ExecContext(ctx, `CREATE TABLE test(
46+
id UUID PRIMARY KEY,
47+
parent_id UUID,
48+
name text,
49+
UNIQUE(name, parent_id)
50+
);`)
51+
return err
52+
})
53+
defer db.Close()
54+
55+
secIDs := []uuid.UUID{
56+
uuid.NewV4(),
57+
uuid.NewV4(),
58+
}
59+
ids := []uuid.UUID{
60+
uuid.NewV4(),
61+
uuid.NewV4(),
62+
uuid.NewV4(),
63+
}
64+
65+
_, err := db.ExecContext(ctx, `INSERT INTO test (id, parent_id, name) VALUES ($1,$2, 'unique')`, ids[0], secIDs[0])
66+
require.NoError(t, err)
67+
_, err = db.ExecContext(ctx, `INSERT INTO test (id, parent_id, name) VALUES ($1,$2, 'regular')`, ids[1], secIDs[0])
68+
require.NoError(t, err)
69+
_, err = db.ExecContext(ctx, `INSERT INTO test (id, parent_id, name) VALUES ($1,$2, 'regular')`, ids[2], secIDs[1])
70+
require.NoError(t, err)
71+
72+
r := NewIDResolver("test", "id", "name")
73+
74+
cases := []struct {
75+
name,
76+
value,
77+
expectedID string
78+
where squirrel.Sqlizer
79+
expErr bool
80+
}{
81+
{
82+
name: "Resolves UUID into ID",
83+
value: ids[0].String(),
84+
expectedID: ids[0].String(),
85+
where: nil,
86+
},
87+
{
88+
name: "Resolves a unique name into ID",
89+
value: "unique",
90+
expectedID: ids[0].String(),
91+
where: nil,
92+
},
93+
{
94+
name: "Resolves a non-unique name into ID for different parent IDs",
95+
value: "regular",
96+
expectedID: ids[1].String(),
97+
where: squirrel.Eq{
98+
"parent_id": secIDs[0],
99+
},
100+
},
101+
{
102+
name: "Resolves a second non-unique name into ID",
103+
value: "regular",
104+
expectedID: ids[2].String(),
105+
where: squirrel.Eq{
106+
"parent_id": secIDs[1],
107+
},
108+
},
109+
{
110+
name: "Triggers error when resolve a non-existent name",
111+
value: "wrong",
112+
expectedID: "",
113+
where: squirrel.Eq{
114+
"parent_id": secIDs[0],
115+
},
116+
expErr: true,
117+
},
118+
{
119+
name: "Triggers error when there are more than one result",
120+
value: "regular",
121+
expectedID: "",
122+
expErr: true,
123+
},
124+
{
125+
name: "Triggers validation error when the value is empty",
126+
expErr: true,
127+
},
128+
}
129+
130+
for _, tc := range cases {
131+
manager := NewBaseManager(db, "id_resolver_test")
132+
builder := manager.GetQueryBuilder()
133+
t.Run(tc.name, func(t *testing.T) {
134+
id, err := r.Resolve(ctx, builder, tc.value, tc.where)
135+
if tc.expErr {
136+
require.Error(t, err)
137+
return
138+
}
139+
require.NoError(t, err)
140+
require.Equal(t, tc.expectedID, id)
141+
})
142+
}
143+
}

0 commit comments

Comments
 (0)