feat: organizations and API keys

This commit is contained in:
2023-04-27 07:09:10 +02:00
parent 504f40902e
commit 554a6c252f
22 changed files with 2469 additions and 199 deletions
+101 -21
View File
@@ -8,58 +8,138 @@ import (
"github.com/sparetimecoders/goamqp"
"gitlab.com/unboundsoftware/schemas/domain"
"gitlab.com/unboundsoftware/schemas/hash"
)
const subGraphKey = "%s<->%s"
type Cache struct {
services map[string]map[string]struct{}
organizations map[string]domain.Organization
users map[string][]string
apiKeys map[string]domain.APIKey
services map[string]map[string]map[string]struct{}
subGraphs map[string]string
lastUpdate map[string]string
logger log.Interface
}
func (c *Cache) Services(ref, lastUpdate string) ([]string, string) {
func (c *Cache) OrganizationByAPIKey(apiKey string) *domain.Organization {
key, exists := c.apiKeys[apiKey]
if !exists {
return nil
}
org, exists := c.organizations[key.OrganizationId]
if !exists {
return nil
}
return &org
}
func (c *Cache) OrganizationsByUser(sub string) []domain.Organization {
orgIds := c.users[sub]
orgs := make([]domain.Organization, len(orgIds))
for i, id := range orgIds {
orgs[i] = c.organizations[id]
}
return orgs
}
func (c *Cache) ApiKeyByKey(key string) *domain.APIKey {
k, exists := c.apiKeys[hash.String(key)]
if !exists {
return nil
}
return &k
}
func (c *Cache) Services(orgId, ref, lastUpdate string) ([]string, string) {
key := refKey(orgId, ref)
var services []string
if lastUpdate == "" || c.lastUpdate[ref] > lastUpdate {
for k := range c.services[ref] {
if lastUpdate == "" || c.lastUpdate[key] > lastUpdate {
for k := range c.services[orgId][ref] {
services = append(services, k)
}
}
return services, c.lastUpdate[ref]
return services, c.lastUpdate[key]
}
func (c *Cache) SubGraphId(ref, service string) string {
return c.subGraphs[fmt.Sprintf(subGraphKey, ref, service)]
func (c *Cache) SubGraphId(orgId, ref, service string) string {
return c.subGraphs[subGraphKey(orgId, ref, service)]
}
func (c *Cache) Update(msg any, _ goamqp.Headers) (any, error) {
switch m := msg.(type) {
case *domain.OrganizationAdded:
o := domain.Organization{}
m.UpdateOrganization(&o)
c.organizations[m.ID.String()] = o
c.addUser(m.Initiator, o)
case *domain.APIKeyAdded:
key := domain.APIKey{
Name: m.Name,
OrganizationId: m.OrganizationId,
Key: m.Key,
Refs: m.Refs,
Read: m.Read,
Publish: m.Publish,
CreatedBy: m.Initiator,
CreatedAt: m.When(),
}
c.apiKeys[m.Key] = key
org := c.organizations[m.OrganizationId]
org.APIKeys = append(org.APIKeys, key)
c.organizations[m.OrganizationId] = org
case *domain.SubGraphUpdated:
if _, exists := c.services[m.Ref]; !exists {
c.services[m.Ref] = make(map[string]struct{})
c.updateSubGraph(m.OrganizationId, m.Ref, m.ID.String(), m.Service, m.Time)
case *domain.Organization:
c.organizations[m.ID.String()] = *m
c.addUser(m.CreatedBy, *m)
for _, k := range m.APIKeys {
c.apiKeys[k.Key] = k
}
c.services[m.Ref][m.ID.String()] = struct{}{}
c.subGraphs[fmt.Sprintf(subGraphKey, m.Ref, m.Service)] = m.ID.String()
c.lastUpdate[m.Ref] = m.Time.Format(time.RFC3339Nano)
case *domain.SubGraph:
if _, exists := c.services[m.Ref]; !exists {
c.services[m.Ref] = make(map[string]struct{})
}
c.services[m.Ref][m.ID.String()] = struct{}{}
c.subGraphs[fmt.Sprintf(subGraphKey, m.Ref, m.Service)] = m.ID.String()
c.lastUpdate[m.Ref] = m.ChangedAt.Format(time.RFC3339Nano)
c.updateSubGraph(m.OrganizationId, m.Ref, m.ID.String(), m.Service, m.ChangedAt)
default:
c.logger.Warnf("unexpected message received: %+v", msg)
}
return nil, nil
}
func (c *Cache) updateSubGraph(orgId string, ref string, subGraphId string, service string, updated time.Time) {
if _, exists := c.services[orgId]; !exists {
c.services[orgId] = make(map[string]map[string]struct{})
}
if _, exists := c.services[orgId][ref]; !exists {
c.services[orgId][ref] = make(map[string]struct{})
}
c.services[orgId][ref][subGraphId] = struct{}{}
c.subGraphs[subGraphKey(orgId, ref, service)] = subGraphId
c.lastUpdate[refKey(orgId, ref)] = updated.Format(time.RFC3339Nano)
}
func (c *Cache) addUser(sub string, organization domain.Organization) {
user, exists := c.users[sub]
if !exists {
c.users[sub] = []string{organization.ID.String()}
} else {
c.users[sub] = append(user, organization.ID.String())
}
}
func New(logger log.Interface) *Cache {
return &Cache{
organizations: make(map[string]domain.Organization),
users: make(map[string][]string),
apiKeys: make(map[string]domain.APIKey),
services: make(map[string]map[string]map[string]struct{}),
subGraphs: make(map[string]string),
services: make(map[string]map[string]struct{}),
lastUpdate: make(map[string]string),
logger: logger,
}
}
func refKey(orgId string, ref string) string {
return fmt.Sprintf("%s<->%s", orgId, ref)
}
func subGraphKey(orgId string, ref string, service string) string {
return fmt.Sprintf("%s<->%s<->%s", orgId, ref, service)
}
+72 -16
View File
@@ -35,10 +35,11 @@ import (
type CLI struct {
AmqpURL string `name:"amqp-url" env:"AMQP_URL" help:"URL to use to connect to RabbitMQ" default:"amqp://user:password@localhost:5672/"`
Port int `name:"port" env:"PORT" help:"Listen-port for GraphQL API" default:"8080"`
APIKey string `name:"api-key" env:"API_KEY" help:"The API-key that is required"`
LogLevel string `name:"log-level" env:"LOG_LEVEL" help:"The level of logging to use (debug, info, warn, error, fatal)" default:"info"`
DatabaseURL string `name:"postgres-url" env:"POSTGRES_URL" help:"URL to use to connect to Postgres" default:"postgres://postgres:postgres@:5432/schemas?sslmode=disable"`
DatabaseDriverName string `name:"db-driver" env:"DB_DRIVER" help:"Driver to use to connect to db" default:"postgres"`
Issuer string `name:"issuer" env:"ISSUER" help:"The JWT token issuer to use" default:"unbound.eu.auth0.com"`
StrictSSL bool `name:"strict-ssl" env:"STRICT_SSL" help:"Should strict SSL handling be enabled" default:"true"`
SentryConfig
}
@@ -88,16 +89,31 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url
db.DB,
pg.WithEventTypes(
&domain.SubGraphUpdated{},
&domain.OrganizationAdded{},
&domain.APIKeyAdded{},
),
)
if err != nil {
return fmt.Errorf("failed to create eventstore: %v", err)
}
if err := store.RunEventStoreMigrations(db); err != nil {
return fmt.Errorf("event migrations: %w", err)
}
publisher, err := goamqp.NewPublisher(
goamqp.Route{
Type: domain.SubGraphUpdated{},
Key: "SubGraph.Updated",
},
goamqp.Route{
Type: domain.OrganizationAdded{},
Key: "Organization.Added",
},
goamqp.Route{
Type: domain.APIKeyAdded{},
Key: "Organization.APIKeyAdded",
},
)
if err != nil {
return fmt.Errorf("failed to create publisher: %v", err)
@@ -112,19 +128,11 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url
}
serviceCache := cache.New(logger)
roots, err := eventStore.GetAggregateRoots(rootCtx, reflect.TypeOf(domain.SubGraph{}))
if err != nil {
return err
}
for _, root := range roots {
subGraph := &domain.SubGraph{BaseAggregate: eventsourced.BaseAggregateFromString(root.String())}
if _, err := eventsourced.NewHandler(rootCtx, subGraph, eventStore); err != nil {
return err
}
_, err := serviceCache.Update(subGraph, nil)
if err != nil {
return err
if err := loadOrganizations(rootCtx, eventStore, serviceCache); err != nil {
return fmt.Errorf("caching organizations: %w", err)
}
if err := loadSubGraphs(rootCtx, eventStore, serviceCache); err != nil {
return fmt.Errorf("caching subgraphs: %w", err)
}
setups := []goamqp.Setup{
goamqp.UseLogger(logger.Errorf),
@@ -132,6 +140,8 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url
goamqp.WithPrefetchLimit(20),
goamqp.EventStreamPublisher(publisher),
goamqp.TransientEventStreamConsumer("SubGraph.Updated", serviceCache.Update, domain.SubGraphUpdated{}),
goamqp.TransientEventStreamConsumer("Organization.Added", serviceCache.Update, domain.OrganizationAdded{}),
goamqp.TransientEventStreamConsumer("Organization.APIKeyAdded", serviceCache.Update, domain.APIKeyAdded{}),
}
if err := conn.Start(rootCtx, setups...); err != nil {
return fmt.Errorf("failed to setup AMQP: %v", err)
@@ -200,8 +210,10 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url
Resolvers: resolver,
Complexity: generated.ComplexityRoot{},
}
apiKeyMiddleware := middleware.NewApiKey(cli.APIKey, logger)
config.Directives.HasApiKey = apiKeyMiddleware.Directive
apiKeyMiddleware := middleware.NewApiKey()
mw := middleware.NewAuth0("https://schemas.unbound.se", cli.Issuer, cli.StrictSSL)
authMiddleware := middleware.NewAuth(serviceCache)
config.Directives.Auth = authMiddleware.Directive
srv := handler.NewDefaultServer(generated.NewExecutableSchema(
config,
))
@@ -209,7 +221,15 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url
sentryHandler := sentryhttp.New(sentryhttp.Options{Repanic: true})
mux.Handle("/", sentryHandler.HandleFunc(playground.Handler("GraphQL playground", "/query")))
mux.Handle("/health", http.HandlerFunc(healthFunc))
mux.Handle("/query", cors.AllowAll().Handler(sentryHandler.Handle(apiKeyMiddleware.Handler(srv))))
mux.Handle("/query", cors.AllowAll().Handler(
sentryHandler.Handle(
mw.Middleware().CheckJWT(
apiKeyMiddleware.Handler(
authMiddleware.Handler(srv),
),
),
),
))
logger.Infof("connect to http://localhost:%d/ for GraphQL playground", cli.Port)
@@ -223,6 +243,42 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url
return nil
}
func loadOrganizations(ctx context.Context, eventStore eventsourced.EventStore, serviceCache *cache.Cache) error {
roots, err := eventStore.GetAggregateRoots(ctx, reflect.TypeOf(domain.Organization{}))
if err != nil {
return err
}
for _, root := range roots {
organization := &domain.Organization{BaseAggregate: eventsourced.BaseAggregateFromString(root.String())}
if _, err := eventsourced.NewHandler(ctx, organization, eventStore); err != nil {
return err
}
_, err := serviceCache.Update(organization, nil)
if err != nil {
return err
}
}
return nil
}
func loadSubGraphs(ctx context.Context, eventStore eventsourced.EventStore, serviceCache *cache.Cache) error {
roots, err := eventStore.GetAggregateRoots(ctx, reflect.TypeOf(domain.SubGraph{}))
if err != nil {
return err
}
for _, root := range roots {
subGraph := &domain.SubGraph{BaseAggregate: eventsourced.BaseAggregateFromString(root.String())}
if _, err := eventsourced.NewHandler(ctx, subGraph, eventStore); err != nil {
return err
}
_, err := serviceCache.Update(subGraph, nil)
if err != nil {
return err
}
}
return nil
}
func healthFunc(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("OK"))
}
+51
View File
@@ -8,8 +8,56 @@ import (
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
)
type Organization struct {
eventsourced.BaseAggregate
Name string
Users []string
APIKeys []APIKey
CreatedBy string
CreatedAt time.Time
ChangedBy string
ChangedAt time.Time
}
func (o *Organization) Apply(event eventsourced.Event) error {
switch e := event.(type) {
case *OrganizationAdded:
e.UpdateOrganization(o)
case *APIKeyAdded:
o.APIKeys = append(o.APIKeys, APIKey{
Name: e.Name,
OrganizationId: o.ID.String(),
Key: e.Key,
Refs: e.Refs,
Read: e.Read,
Publish: e.Publish,
CreatedBy: e.Initiator,
CreatedAt: e.When(),
})
o.ChangedBy = e.Initiator
o.ChangedAt = e.When()
default:
return fmt.Errorf("unexpected event type: %+v", event)
}
return nil
}
var _ eventsourced.Aggregate = &Organization{}
type APIKey struct {
Name string
OrganizationId string
Key string
Refs []string
Read bool
Publish bool
CreatedBy string
CreatedAt time.Time
}
type SubGraph struct {
eventsourced.BaseAggregate
OrganizationId string
Ref string
Service string
Url *string
@@ -28,6 +76,9 @@ func (s *SubGraph) Apply(event eventsourced.Event) error {
s.CreatedBy = e.Initiator
s.CreatedAt = e.When()
}
if s.OrganizationId == "" {
s.OrganizationId = e.OrganizationId
}
s.ChangedBy = e.Initiator
s.ChangedAt = e.When()
s.Ref = e.Ref
+62
View File
@@ -6,9 +6,70 @@ import (
"strings"
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
"gitlab.com/unboundsoftware/schemas/hash"
)
type AddOrganization struct {
Name string
Initiator string
}
func (a AddOrganization) Validate(_ context.Context, aggregate eventsourced.Aggregate) error {
if aggregate.Identity() != nil {
return fmt.Errorf("organization already exists")
}
if len(a.Name) == 0 {
return fmt.Errorf("name is required")
}
return nil
}
func (a AddOrganization) Event(context.Context) eventsourced.Event {
return &OrganizationAdded{
Name: a.Name,
Initiator: a.Initiator,
}
}
var _ eventsourced.Command = AddOrganization{}
type AddAPIKey struct {
Name string
Key string
Refs []string
Read bool
Publish bool
Initiator string
}
func (a AddAPIKey) Validate(_ context.Context, aggregate eventsourced.Aggregate) error {
if aggregate.Identity() == nil {
return fmt.Errorf("organization does not exist")
}
for _, k := range aggregate.(*Organization).APIKeys {
if k.Name == a.Name {
return fmt.Errorf("a key named '%s' already exist", a.Name)
}
}
return nil
}
func (a AddAPIKey) Event(context.Context) eventsourced.Event {
return &APIKeyAdded{
Name: a.Name,
Key: hash.String(a.Key),
Refs: a.Refs,
Read: a.Read,
Publish: a.Publish,
Initiator: a.Initiator,
}
}
var _ eventsourced.Command = AddAPIKey{}
type UpdateSubGraph struct {
OrganizationId string
Ref string
Service string
Url *string
@@ -40,6 +101,7 @@ func (u UpdateSubGraph) Validate(_ context.Context, aggregate eventsourced.Aggre
func (u UpdateSubGraph) Event(context.Context) eventsourced.Event {
return &SubGraphUpdated{
OrganizationId: u.OrganizationId,
Ref: u.Ref,
Service: u.Service,
Url: u.Url,
+63
View File
@@ -0,0 +1,63 @@
package domain
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
)
func TestAddAPIKey_Event(t *testing.T) {
type fields struct {
Name string
Key string
Refs []string
Read bool
Publish bool
Initiator string
}
type args struct {
in0 context.Context
}
tests := []struct {
name string
fields fields
args args
want eventsourced.Event
}{
{
name: "event",
fields: fields{
Name: "test",
Key: "us_ak_1234567890123456",
Refs: []string{"Example@dev"},
Read: true,
Publish: true,
Initiator: "jim@example.org",
},
args: args{},
want: &APIKeyAdded{
Name: "test",
Key: "dXNfYWtfMTIzNDU2Nzg5MDEyMzQ1NuOwxEKY/BwUmvv0yJlvuSQnrkHkZJuTTKSVmRt4UrhV",
Refs: []string{"Example@dev"},
Read: true,
Publish: true,
Initiator: "jim@example.org",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := AddAPIKey{
Name: tt.fields.Name,
Key: tt.fields.Key,
Refs: tt.fields.Refs,
Read: tt.fields.Read,
Publish: tt.fields.Publish,
Initiator: tt.fields.Initiator,
}
assert.Equalf(t, tt.want, a.Event(tt.args.in0), "Event(%v)", tt.args.in0)
})
}
}
+41 -6
View File
@@ -2,13 +2,48 @@ package domain
import "gitlab.com/unboundsoftware/eventsourced/eventsourced"
type OrganizationAdded struct {
eventsourced.EventAggregateId
eventsourced.EventTime
Name string `json:"name"`
Initiator string `json:"initiator"`
}
func (a *OrganizationAdded) UpdateOrganization(o *Organization) {
o.Name = a.Name
o.Users = []string{a.Initiator}
o.CreatedBy = a.Initiator
o.CreatedAt = a.When()
o.ChangedBy = a.Initiator
o.ChangedAt = a.When()
}
type APIKeyAdded struct {
eventsourced.EventAggregateId
eventsourced.EventTime
OrganizationId string `json:"organizationId"`
Name string `json:"name"`
Key string `json:"key"`
Refs []string `json:"refs"`
Read bool `json:"read"`
Publish bool `json:"publish"`
Initiator string `json:"initiator"`
}
func (a *APIKeyAdded) EnrichFromAggregate(aggregate eventsourced.Aggregate) {
a.OrganizationId = aggregate.Identity().String()
}
var _ eventsourced.EnrichableEvent = &APIKeyAdded{}
type SubGraphUpdated struct {
eventsourced.EventAggregateId
eventsourced.EventTime
Ref string
Service string
Url *string
WSUrl *string
Sdl string
Initiator string
OrganizationId string `json:"organizationId"`
Ref string `json:"ref"`
Service string `json:"service"`
Url *string `json:"url"`
WSUrl *string `json:"wsUrl"`
Sdl string `json:"sdl"`
Initiator string `json:"initiator"`
}
+6 -3
View File
@@ -7,8 +7,12 @@ require (
github.com/Khan/genqlient v0.5.0
github.com/alecthomas/kong v0.7.1
github.com/apex/log v1.9.0
github.com/auth0/go-jwt-middleware/v2 v2.1.0
github.com/getsentry/sentry-go v0.20.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/jmoiron/sqlx v1.3.5
github.com/pkg/errors v0.9.1
github.com/pressly/goose/v3 v3.10.0
github.com/rs/cors v1.9.0
github.com/sparetimecoders/goamqp v0.1.3
github.com/stretchr/testify v1.8.2
@@ -32,7 +36,6 @@ require (
github.com/kr/text v0.2.0 // indirect
github.com/lib/pq v1.10.8 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/qri-io/jsonpointer v0.1.1 // indirect
github.com/qri-io/jsonschema v0.2.1 // indirect
@@ -44,10 +47,10 @@ require (
github.com/tidwall/sjson v1.2.5 // indirect
github.com/urfave/cli/v2 v2.24.4 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/tools v0.6.0 // indirect
golang.org/x/tools v0.7.0 // indirect
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+27 -5
View File
@@ -25,6 +25,8 @@ github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy
github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/auth0/go-jwt-middleware/v2 v2.1.0 h1:VU4LsC3aFPoqXVyEp8EixU6FNM+ZNIjECszRTvtGQI8=
github.com/auth0/go-jwt-middleware/v2 v2.1.0/go.mod h1:CpzcJoleayAACpv+vt0AP8/aYn5TDngsqzLapV1nM4c=
github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
@@ -43,6 +45,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/evanphx/json-patch/v5 v5.1.0 h1:B0aXl1o/1cP8NbviYiBMkcHBtUjIJ1/Ccg6b+SwCLQg=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -50,8 +53,10 @@ github.com/getsentry/sentry-go v0.20.0 h1:bwXW98iMRIWxn+4FgPW7vMrjmbym6HblXALmhj
github.com/getsentry/sentry-go v0.20.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -73,6 +78,7 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -92,6 +98,7 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
@@ -109,12 +116,15 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/pressly/goose/v3 v3.10.0 h1:Gn5E9CkPqTtWvfaDVqtJqMjYtsrZ9K5mU/8wzTsvg04=
github.com/pressly/goose/v3 v3.10.0/go.mod h1:c5D3a7j66cT0fhRPj7KsXolfduVrhLlxKZjmCVSey5w=
github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA=
github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64=
github.com/qri-io/jsonschema v0.2.1 h1:NNFoKms+kut6ABPf6xiKNM5214jzxAhDBrPHCJ97Wg0=
github.com/qri-io/jsonschema v0.2.1/go.mod h1:g7DPkiOsK1xv6T/Ao5scXRkd+yTFygcANPBaaqW+VrI=
github.com/rabbitmq/amqp091-go v1.5.0 h1:VouyHPBu1CrKyJVfteGknGOGCzmOz0zcv/tONLkb7rg=
github.com/rabbitmq/amqp091-go v1.5.0/go.mod h1:JsV0ofX5f1nwOGafb8L5rBItt9GyhfQfcJj+oyz0dGg=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE=
github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
@@ -193,13 +203,14 @@ golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -244,8 +255,8 @@ golang.org/x/tools v0.0.0-20200815165600-90abf76919f3/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -258,6 +269,7 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -270,3 +282,13 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/sqlite v1.21.0 h1:4aP4MdUf15i3R3M2mx6Q90WHKz3nZLoz96zlB6tNdow=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+49
View File
@@ -0,0 +1,49 @@
package graph
import (
"gitlab.com/unboundsoftware/schemas/domain"
"gitlab.com/unboundsoftware/schemas/graph/model"
)
func ToGqlOrganizations(orgs []domain.Organization) []*model.Organization {
result := make([]*model.Organization, len(orgs))
for i, o := range orgs {
result[i] = ToGqlOrganization(o)
}
return result
}
func ToGqlOrganization(o domain.Organization) *model.Organization {
users := ToGqlUsers(o.Users)
apiKeys := ToGqlAPIKeys(o.APIKeys)
return &model.Organization{
ID: o.ID.String(),
Name: o.Name,
Users: users,
APIKeys: apiKeys,
}
}
func ToGqlUsers(users []string) []*model.User {
result := make([]*model.User, len(users))
for i, u := range users {
result[i] = &model.User{ID: u}
}
return result
}
func ToGqlAPIKeys(keys []domain.APIKey) []*model.APIKey {
result := make([]*model.APIKey, len(keys))
for i, k := range keys {
result[i] = &model.APIKey{
ID: apiKeyId(k.OrganizationId, k.Name),
Name: k.Name,
Key: &k.Key,
Organization: nil,
Refs: k.Refs,
Read: k.Read,
Publish: k.Publish,
}
}
return result
}
File diff suppressed because it is too large Load Diff
+29
View File
@@ -10,6 +10,24 @@ type Supergraph interface {
IsSupergraph()
}
type APIKey struct {
ID string `json:"id"`
Name string `json:"name"`
Key *string `json:"key,omitempty"`
Organization *Organization `json:"organization"`
Refs []string `json:"refs"`
Read bool `json:"read"`
Publish bool `json:"publish"`
}
type InputAPIKey struct {
Name string `json:"name"`
OrganizationID string `json:"organizationId"`
Refs []string `json:"refs"`
Read bool `json:"read"`
Publish bool `json:"publish"`
}
type InputSubGraph struct {
Ref string `json:"ref"`
Service string `json:"service"`
@@ -18,6 +36,13 @@ type InputSubGraph struct {
Sdl string `json:"sdl"`
}
type Organization struct {
ID string `json:"id"`
Name string `json:"name"`
Users []*User `json:"users"`
APIKeys []*APIKey `json:"apiKeys"`
}
type SubGraph struct {
ID string `json:"id"`
Service string `json:"service"`
@@ -42,3 +67,7 @@ type Unchanged struct {
}
func (Unchanged) IsSupergraph() {}
type User struct {
ID string `json:"id"`
}
+5
View File
@@ -2,6 +2,7 @@ package graph
import (
"context"
"fmt"
"github.com/apex/log"
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
@@ -29,3 +30,7 @@ type Resolver struct {
func (r *Resolver) handler(ctx context.Context, aggregate eventsourced.Aggregate) (eventsourced.CommandHandler, error) {
return eventsourced.NewHandler(ctx, aggregate, r.EventStore, eventsourced.WithEventPublisher(r.Publisher))
}
func apiKeyId(orgId, name string) string {
return fmt.Sprintf("%s-%s", orgId, name)
}
+35 -4
View File
@@ -1,10 +1,33 @@
type Query {
subGraphs(ref: String!): [SubGraph!]! @hasApiKey @deprecated(reason: "Use supergraph instead")
supergraph(ref: String!, isAfter: String): Supergraph! @hasApiKey
organizations: [Organization!]! @auth(user: true)
supergraph(ref: String!, isAfter: String): Supergraph! @auth(organization: true)
}
type Mutation {
updateSubGraph(input: InputSubGraph!): SubGraph! @hasApiKey
addOrganization(name: String!): Organization! @auth(user: true)
addAPIKey(input: InputAPIKey): APIKey! @auth(user: true)
updateSubGraph(input: InputSubGraph!): SubGraph! @auth(organization: true)
}
type Organization {
id: ID!
name: String!
users: [User!]!
apiKeys: [APIKey!]!
}
type User {
id: String!
}
type APIKey {
id: ID!
name: String!
key: String
organization: Organization!
refs: [String!]!
read: Boolean!
publish: Boolean!
}
union Supergraph = Unchanged | SubGraphs
@@ -30,6 +53,14 @@ type SubGraph {
changedAt: Time!
}
input InputAPIKey {
name: String!
organizationId: ID!
refs: [String!]!
read: Boolean!
publish: Boolean!
}
input InputSubGraph {
ref: String!
service: String!
@@ -40,4 +71,4 @@ input InputSubGraph {
scalar Time
directive @hasApiKey on FIELD_DEFINITION
directive @auth(user: Boolean, organization: Boolean) on FIELD_DEFINITION
+71 -14
View File
@@ -15,11 +15,71 @@ import (
"gitlab.com/unboundsoftware/schemas/domain"
"gitlab.com/unboundsoftware/schemas/graph/generated"
"gitlab.com/unboundsoftware/schemas/graph/model"
"gitlab.com/unboundsoftware/schemas/middleware"
"gitlab.com/unboundsoftware/schemas/rand"
)
// AddOrganization is the resolver for the addOrganization field.
func (r *mutationResolver) AddOrganization(ctx context.Context, name string) (*model.Organization, error) {
sub := middleware.UserFromContext(ctx)
org := &domain.Organization{}
h, err := r.handler(ctx, org)
if err != nil {
return nil, err
}
_, err = h.Handle(ctx, &domain.AddOrganization{
Name: name,
Initiator: sub,
})
if err != nil {
return nil, err
}
return ToGqlOrganization(*org), nil
}
// AddAPIKey is the resolver for the addAPIKey field.
func (r *mutationResolver) AddAPIKey(ctx context.Context, input *model.InputAPIKey) (*model.APIKey, error) {
sub := middleware.UserFromContext(ctx)
org := &domain.Organization{BaseAggregate: eventsourced.BaseAggregateFromString(input.OrganizationID)}
h, err := r.handler(ctx, org)
if err != nil {
return nil, err
}
key := fmt.Sprintf("us_ak_%s", rand.String(16))
_, err = h.Handle(ctx, &domain.AddAPIKey{
Name: input.Name,
Key: key,
Refs: input.Refs,
Read: input.Read,
Publish: input.Publish,
Initiator: sub,
})
if err != nil {
return nil, err
}
return &model.APIKey{
ID: apiKeyId(input.OrganizationID, input.Name),
Name: input.Name,
Key: &key,
Organization: &model.Organization{
ID: input.OrganizationID,
Name: org.Name,
},
Refs: input.Refs,
Read: input.Read,
Publish: input.Publish,
}, nil
}
// UpdateSubGraph is the resolver for the updateSubGraph field.
func (r *mutationResolver) UpdateSubGraph(ctx context.Context, input model.InputSubGraph) (*model.SubGraph, error) {
subGraphId := r.Cache.SubGraphId(input.Ref, input.Service)
orgId := middleware.OrganizationFromContext(ctx)
key, err := middleware.ApiKeyFromContext(ctx)
if err != nil {
return nil, err
}
apiKey := r.Cache.ApiKeyByKey(key)
subGraphId := r.Cache.SubGraphId(orgId, input.Ref, input.Service)
subGraph := &domain.SubGraph{}
if subGraphId != "" {
subGraph.BaseAggregate = eventsourced.BaseAggregateFromString(subGraphId)
@@ -34,7 +94,7 @@ func (r *mutationResolver) UpdateSubGraph(ctx context.Context, input model.Input
return r.toGqlSubGraph(subGraph), nil
}
serviceSDLs := []string{input.Sdl}
services, _ := r.Cache.Services(input.Ref, "")
services, _ := r.Cache.Services(orgId, input.Ref, "")
for _, id := range services {
sg, err := r.fetchSubGraph(ctx, id)
if err != nil {
@@ -49,12 +109,13 @@ func (r *mutationResolver) UpdateSubGraph(ctx context.Context, input model.Input
return nil, err
}
_, err = handler.Handle(ctx, domain.UpdateSubGraph{
OrganizationId: orgId,
Ref: input.Ref,
Service: input.Service,
Url: input.URL,
WSUrl: input.WsURL,
Sdl: input.Sdl,
Initiator: "Fetch name from API-key?",
Initiator: apiKey.Name,
})
if err != nil {
return nil, err
@@ -62,25 +123,21 @@ func (r *mutationResolver) UpdateSubGraph(ctx context.Context, input model.Input
return r.toGqlSubGraph(subGraph), nil
}
// SubGraphs is the resolver for the subGraphs field.
func (r *queryResolver) SubGraphs(ctx context.Context, ref string) ([]*model.SubGraph, error) {
res, err := r.Supergraph(ctx, ref, nil)
if err != nil {
return nil, err
}
if s, ok := res.(*model.SubGraphs); ok {
return s.SubGraphs, nil
}
return nil, fmt.Errorf("unexpected response")
// Organizations is the resolver for the organizations field.
func (r *queryResolver) Organizations(ctx context.Context) ([]*model.Organization, error) {
sub := middleware.UserFromContext(ctx)
orgs := r.Cache.OrganizationsByUser(sub)
return ToGqlOrganizations(orgs), nil
}
// Supergraph is the resolver for the supergraph field.
func (r *queryResolver) Supergraph(ctx context.Context, ref string, isAfter *string) (model.Supergraph, error) {
orgId := middleware.OrganizationFromContext(ctx)
after := ""
if isAfter != nil {
after = *isAfter
}
services, lastUpdate := r.Cache.Services(ref, after)
services, lastUpdate := r.Cache.Services(orgId, ref, after)
if after == lastUpdate {
return &model.Unchanged{
ID: lastUpdate,
+11
View File
@@ -0,0 +1,11 @@
package hash
import (
"crypto/sha256"
"encoding/base64"
)
func String(s string) string {
encoded := sha256.New().Sum([]byte(s))
return base64.StdEncoding.EncodeToString(encoded)
}
+5 -25
View File
@@ -4,26 +4,17 @@ import (
"context"
"fmt"
"net/http"
"github.com/99designs/gqlgen/graphql"
"github.com/apex/log"
)
type ContextKey string
const (
ApiKey = ContextKey("apikey")
)
func NewApiKey(apiKey string, logger log.Interface) *ApiKeyMiddleware {
return &ApiKeyMiddleware{
apiKey: apiKey,
}
func NewApiKey() *ApiKeyMiddleware {
return &ApiKeyMiddleware{}
}
type ApiKeyMiddleware struct {
apiKey string
}
type ApiKeyMiddleware struct{}
func (m *ApiKeyMiddleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -37,23 +28,12 @@ func (m *ApiKeyMiddleware) Handler(next http.Handler) http.Handler {
})
}
func (m *ApiKeyMiddleware) Directive(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
key, err := m.fromContext(ctx)
if err != nil {
return nil, err
}
if key != m.apiKey {
return nil, fmt.Errorf("invalid API-key")
}
return next(ctx)
}
func (m *ApiKeyMiddleware) fromContext(ctx context.Context) (string, error) {
func ApiKeyFromContext(ctx context.Context) (string, error) {
if value := ctx.Value(ApiKey); value != nil {
if u, ok := value.(string); ok {
return u, nil
}
return "", fmt.Errorf("current API-key is in wrong format")
}
return "", fmt.Errorf("no API-key found")
return "", nil
}
+90
View File
@@ -0,0 +1,90 @@
package middleware
import (
"context"
"fmt"
"net/http"
"github.com/99designs/gqlgen/graphql"
"github.com/golang-jwt/jwt/v4"
"gitlab.com/unboundsoftware/schemas/domain"
"gitlab.com/unboundsoftware/schemas/hash"
)
const (
UserKey = ContextKey("user")
OrganizationKey = ContextKey("organization")
)
type Cache interface {
OrganizationByAPIKey(apiKey string) *domain.Organization
}
func NewAuth(cache Cache) *AuthMiddleware {
return &AuthMiddleware{
cache: cache,
}
}
type AuthMiddleware struct {
cache Cache
}
func (m *AuthMiddleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
token, err := TokenFromContext(r.Context())
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("Invalid JWT token format"))
return
}
if token != nil {
ctx = context.WithValue(ctx, UserKey, token.Claims.(jwt.MapClaims)["sub"])
}
apiKey, err := ApiKeyFromContext(r.Context())
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("Invalid API Key format"))
return
}
if organization := m.cache.OrganizationByAPIKey(hash.String(apiKey)); organization != nil {
ctx = context.WithValue(ctx, OrganizationKey, *organization)
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func UserFromContext(ctx context.Context) string {
if value := ctx.Value(UserKey); value != nil {
if u, ok := value.(string); ok {
return u
}
}
return ""
}
func OrganizationFromContext(ctx context.Context) string {
if value := ctx.Value(OrganizationKey); value != nil {
if u, ok := value.(domain.Organization); ok {
return u.ID.String()
}
}
return ""
}
func (m *AuthMiddleware) Directive(ctx context.Context, _ interface{}, next graphql.Resolver, user *bool, organization *bool) (res interface{}, err error) {
if user != nil && *user {
if u := UserFromContext(ctx); u == "" {
return nil, fmt.Errorf("no user available in request")
}
}
if organization != nil && *organization {
if orgId := OrganizationFromContext(ctx); orgId == "" {
return nil, fmt.Errorf("no organization available in request")
}
}
return next(ctx)
}
+189
View File
@@ -0,0 +1,189 @@
package middleware
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"strings"
"sync"
"time"
mw "github.com/auth0/go-jwt-middleware/v2"
"github.com/golang-jwt/jwt/v4"
"github.com/pkg/errors"
)
type Auth0 struct {
domain string
audience string
client *http.Client
cache JwksCache
}
func NewAuth0(audience, domain string, strictSsl bool) *Auth0 {
customTransport := http.DefaultTransport.(*http.Transport).Clone()
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: !strictSsl}
client := &http.Client{Transport: customTransport}
return &Auth0{
domain: domain,
audience: audience,
client: client,
cache: JwksCache{
RWMutex: &sync.RWMutex{},
cache: make(map[string]cacheItem),
},
}
}
type Response struct {
Message string `json:"message"`
}
type Jwks struct {
Keys []JSONWebKeys `json:"keys"`
}
type JSONWebKeys struct {
Kty string `json:"kty"`
Kid string `json:"kid"`
Use string `json:"use"`
N string `json:"n"`
E string `json:"e"`
X5c []string `json:"x5c"`
}
func (a *Auth0) ValidationKeyGetter() func(token *jwt.Token) (interface{}, error) {
issuer := fmt.Sprintf("https://%s/", a.domain)
return func(token *jwt.Token) (interface{}, error) {
// Verify 'aud' claim
aud := a.audience
checkAud := token.Claims.(jwt.MapClaims).VerifyAudience(aud, false)
if !checkAud {
return token, errors.New("Invalid audience.")
}
// Verify 'iss' claim
iss := issuer
checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(iss, false)
if !checkIss {
return token, errors.New("Invalid issuer.")
}
cert, err := a.getPemCert(token)
if err != nil {
panic(err.Error())
}
result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
return result, nil
}
}
func (a *Auth0) Middleware() *mw.JWTMiddleware {
jwtMiddleware := mw.New(func(ctx context.Context, token string) (interface{}, error) {
jwtToken, err := jwt.Parse(token, a.ValidationKeyGetter())
if err != nil {
return nil, err
}
if _, ok := jwtToken.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", jwtToken.Header["alg"])
}
err = jwtToken.Claims.Valid()
if err != nil {
return nil, err
}
return jwtToken, nil
},
mw.WithTokenExtractor(func(r *http.Request) (string, error) {
token := r.Header.Get("Authorization")
if strings.HasPrefix(token, "Bearer ") {
return token[7:], nil
}
return "", nil
}),
mw.WithCredentialsOptional(true),
)
return jwtMiddleware
}
func TokenFromContext(ctx context.Context) (*jwt.Token, error) {
if value := ctx.Value(mw.ContextKey{}); value != nil {
if u, ok := value.(*jwt.Token); ok {
return u, nil
}
return nil, fmt.Errorf("token is in wrong format")
}
return nil, nil
}
func (a *Auth0) cacheGetWellknown(url string) (*Jwks, error) {
if value := a.cache.get(url); value != nil {
return value, nil
}
jwks := &Jwks{}
resp, err := a.client.Get(url)
if err != nil {
return jwks, err
}
defer func() {
_ = resp.Body.Close()
}()
err = json.NewDecoder(resp.Body).Decode(jwks)
if err == nil && jwks != nil {
a.cache.put(url, jwks)
}
return jwks, err
}
func (a *Auth0) getPemCert(token *jwt.Token) (string, error) {
jwks, err := a.cacheGetWellknown(fmt.Sprintf("https://%s/.well-known/jwks.json", a.domain))
if err != nil {
return "", err
}
var cert string
for k := range jwks.Keys {
if token.Header["kid"] == jwks.Keys[k].Kid {
cert = "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----"
}
}
if cert == "" {
err := errors.New("Unable to find appropriate key.")
return cert, err
}
return cert, nil
}
type JwksCache struct {
*sync.RWMutex
cache map[string]cacheItem
}
type cacheItem struct {
data *Jwks
expiration time.Time
}
func (c *JwksCache) get(url string) *Jwks {
c.RLock()
defer c.RUnlock()
if value, ok := c.cache[url]; ok {
if time.Now().After(value.expiration) {
return nil
}
return value.data
}
return nil
}
func (c *JwksCache) put(url string, jwks *Jwks) {
c.Lock()
defer c.Unlock()
c.cache[url] = cacheItem{
data: jwks,
expiration: time.Now().Add(time.Minute * 60),
}
}
+3
View File
@@ -0,0 +1,3 @@
package middleware
type ContextKey string
+24
View File
@@ -0,0 +1,24 @@
package rand
import (
"math/rand"
"time"
)
const charset = "abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var seededRand *rand.Rand = rand.New(
rand.NewSource(time.Now().UnixNano()))
func StringWithCharset(length int, charset string) string {
b := make([]byte, length)
for i := range b {
b[i] = charset[seededRand.Intn(len(charset))]
}
return string(b)
}
func String(length int) string {
return StringWithCharset(length, charset)
}
@@ -0,0 +1,52 @@
-- +goose Up
-- Add Unbound Software Development organization
insert into aggregates (id, name)
values ('d46ffcb0-19e8-4769-8697-590326ef7b51', 'domain.Organization');
insert into events (name, aggregate_id, sequence_no, payload, tstamp, aggregate_name)
values ('domain.OrganizationAdded', 'd46ffcb0-19e8-4769-8697-590326ef7b51', 1, '{"id":"d46ffcb0-19e8-4769-8697-590326ef7b51","time":"2023-04-26T14:46:04.43462+02:00","name":"Unbound Software Development","initiator":"google-oauth2|101953650269257914934"}', '2023-04-26T14:46:04.43462+02:00', 'domain.Organization');
-- Add API keys
insert into events (name, aggregate_id, sequence_no, payload, tstamp, aggregate_name)
values ('domain.APIKeyAdded', 'd46ffcb0-19e8-4769-8697-590326ef7b51', 2,
'{"id":"d46ffcb0-19e8-4769-8697-590326ef7b51","time":"2023-04-26T15:46:54.181929+02:00","organizationId":"","name":"CI","key":"dXNfYWtfeUl2R3RRQUJQTmJzVEFrUeOwxEKY/BwUmvv0yJlvuSQnrkHkZJuTTKSVmRt4UrhV","refs":["Shiny@staging","Shiny@prod"],"read":false,"publish":true,"initiator":"google-oauth2|101953650269257914934"}',
'2023-04-26 15:46:54.181929 +02:00', 'domain.Organization');
insert into events (name, aggregate_id, sequence_no, payload, tstamp, aggregate_name)
values ('domain.APIKeyAdded', 'd46ffcb0-19e8-4769-8697-590326ef7b51', 3,
'{"id":"d46ffcb0-19e8-4769-8697-590326ef7b51","time":"2023-04-26T15:52:55.955203+02:00","organizationId":"","name":"Gateway","key":"dXNfYWtfdnkzSkRseDNlSDNjcnZzOeOwxEKY/BwUmvv0yJlvuSQnrkHkZJuTTKSVmRt4UrhV","refs":["Shiny@staging","Shiny@prod"],"read":true,"publish":false,"initiator":"google-oauth2|101953650269257914934"}',
'2023-04-26 15:52:55.955203 +02:00', 'domain.Organization');
insert into events (name, aggregate_id, sequence_no, payload, tstamp, aggregate_name)
values ('domain.APIKeyAdded', 'd46ffcb0-19e8-4769-8697-590326ef7b51', 4,
'{"id":"d46ffcb0-19e8-4769-8697-590326ef7b51","time":"2023-04-26T16:30:00.0011+02:00","organizationId":"","name":"Local dev","key":"dXNfYWtfM0kzaGZndmVaQllyQzdjVOOwxEKY/BwUmvv0yJlvuSQnrkHkZJuTTKSVmRt4UrhV","refs":["Shiny@dev"],"read":true,"publish":true,"initiator":"google-oauth2|101953650269257914934"}',
'2023-04-26 16:30:00.001100 +02:00', 'domain.Organization');
insert into events (name, aggregate_id, sequence_no, payload, tstamp, aggregate_name)
values ('domain.APIKeyAdded', 'd46ffcb0-19e8-4769-8697-590326ef7b51', 5,
'{"id":"d46ffcb0-19e8-4769-8697-590326ef7b51","time":"2023-04-27T07:43:26.599544+02:00","organizationId":"","name":"Acctest","key":"dXNfYWtfdlVqMzdBMXVraklmaGtKSOOwxEKY/BwUmvv0yJlvuSQnrkHkZJuTTKSVmRt4UrhV","refs":["Shiny@test"],"read":true,"publish":true,"initiator":"google-oauth2|101953650269257914934"}',
'2023-04-27 07:43:26.599544 +02:00', 'domain.Organization');
-- Update events since json-tags were added
UPDATE events e
SET payload = jsonb_build_object(
'id', payload::jsonb ->> 'id',
'time', payload::jsonb ->> 'time',
'ref', payload::jsonb ->> 'Ref',
'sdl', payload::jsonb ->> 'Sdl',
'url', payload::jsonb ->> 'Url',
'wsUrl', payload::jsonb ->> 'WSUrl',
'service', payload::jsonb ->> 'Service',
'initiator', 'CI'
)
WHERE e.name = 'domain.SubGraphUpdated';
-- Add organization id to all existing subgraphs
update events e
set payload = jsonb_set(payload::jsonb, '{organizationId}', '"d46ffcb0-19e8-4769-8697-590326ef7b51"', true)
where e.name = 'domain.SubGraphUpdated';
DELETE
from snapshots;
-- +goose Down
+13
View File
@@ -1,7 +1,10 @@
package store
import (
"embed"
"github.com/jmoiron/sqlx"
"github.com/pressly/goose/v3"
)
func SetupDB(driverName, url string) (*sqlx.DB, error) {
@@ -25,3 +28,13 @@ func SetupDB(driverName, url string) (*sqlx.DB, error) {
//
// return goose.Up(db.DB, "migrations")
//}
//go:embed event_store_migrations/*.sql
var embedEventStoreMigrations embed.FS
func RunEventStoreMigrations(db *sqlx.DB) error {
goose.SetTableName("goose_db_version_event")
goose.SetBaseFS(embedEventStoreMigrations)
return goose.Up(db.DB, "event_store_migrations")
}