diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 9aabc9bf..b309bc76 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -95,3 +95,12 @@ services: jaeger: image: jaegertracing/all-in-one:1.43.0 network_mode: service:app + + mock-oauth2-server: + image: ghcr.io/navikt/mock-oauth2-server:0.5.8 + networks: + - infradev + environment: + - PORT=8081 + ports: + - 8081:8081 diff --git a/cmd/server.go b/cmd/server.go index b7fe1f39..4cb8149d 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -21,6 +21,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "go.hollow.sh/toolbox/ginjwt" "go.infratographer.com/x/ginx" "go.infratographer.com/x/otelx" "go.infratographer.com/x/versionx" @@ -45,8 +46,11 @@ var serverCmd = &cobra.Command{ func init() { rootCmd.AddCommand(serverCmd) - ginx.MustViperFlags(viper.GetViper(), serverCmd.Flags(), APIDefaultListen) - otelx.MustViperFlags(viper.GetViper(), serverCmd.Flags()) + v := viper.GetViper() + + ginx.MustViperFlags(v, serverCmd.Flags(), APIDefaultListen) + otelx.MustViperFlags(v, serverCmd.Flags()) + ginjwt.RegisterViperOIDCFlags(v, serverCmd) } func serve(ctx context.Context, cfg *config.AppConfig) { @@ -61,7 +65,11 @@ func serve(ctx context.Context, cfg *config.AppConfig) { } s := ginx.NewServer(logger.Desugar(), cfg.Server, versionx.BuildDetails()) - r := api.NewRouter(spiceClient, logger) + + r, err := api.NewRouter(cfg.OIDC, spiceClient, logger) + if err != nil { + logger.Fatalw("unable to initialize router", "error", err) + } s = s.AddHandler(r). AddReadinessCheck("spicedb", spicedbx.Healthcheck(spiceClient)) diff --git a/go.mod b/go.mod index 8195eca0..ae26a33b 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/spf13/cobra v1.6.1 github.com/spf13/viper v1.15.0 github.com/stretchr/testify v1.8.2 + go.hollow.sh/toolbox v0.5.1 go.infratographer.com/x v0.0.3 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.40.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0 @@ -101,5 +102,6 @@ require ( google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 168d8996..bdbc99ad 100644 --- a/go.sum +++ b/go.sum @@ -1869,6 +1869,8 @@ go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/ go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= +go.hollow.sh/toolbox v0.5.1 h1:x/tRdpgUFckT/pw6FySxUpSFxhw+Sc66zxbwlra+br4= +go.hollow.sh/toolbox v0.5.1/go.mod h1:nfUNHobFfItossU5I0m7h2dw+0nY7rQt3DSUm2wGI5Q= go.infratographer.com/x v0.0.3 h1:7txN1iBq1Ts4XF6Ea/9/LWK2vpTeXKnh+DjdJ1OLHVQ= go.infratographer.com/x v0.0.3/go.mod h1:6qJTwrxN8O3X0v/v2/O7pZkq1T8V7rNDSnzZKJeEmRQ= go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= @@ -2826,6 +2828,8 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/telebot.v3 v3.0.0/go.mod h1:7rExV8/0mDDNu9epSrDm/8j22KLaActH1Tbee6YjzWg= gopkg.in/telebot.v3 v3.1.2/go.mod h1:GJKwwWqp9nSkIVN51eRKU78aB5f5OnQuWdwiIZfPbko= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/internal/api/auth.go b/internal/api/auth.go new file mode 100644 index 00000000..527456e8 --- /dev/null +++ b/internal/api/auth.go @@ -0,0 +1,15 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "go.hollow.sh/toolbox/ginjwt" +) + +func newAuthMiddleware(cfg ginjwt.AuthConfig) (func(*gin.Context), error) { + mw, err := ginjwt.NewMultiTokenMiddlewareFromConfigs(cfg) + if err != nil { + return nil, err + } + + return mw.AuthRequired(nil), nil +} diff --git a/internal/api/permissions.go b/internal/api/permissions.go index f7fa679b..829dafc9 100644 --- a/internal/api/permissions.go +++ b/internal/api/permissions.go @@ -6,15 +6,22 @@ import ( "github.com/gin-gonic/gin" "go.infratographer.com/permissions-api/internal/query" + "go.infratographer.com/x/urnx" ) func (r *Router) checkScope(c *gin.Context) { - resourceURN := c.Param("urn") + resourceURNStr := c.Param("urn") scope := c.Param("scope") ctx, span := tracer.Start(c.Request.Context(), "api.checkScope") defer span.End() + resourceURN, err := urnx.Parse(resourceURNStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "error processing resource URN", "error": err.Error()}) + return + } + resource, err := query.NewResourceFromURN(resourceURN) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"message": "error processing resource URN", "error": err.Error()}) @@ -27,7 +34,7 @@ func (r *Router) checkScope(c *gin.Context) { return } - actorResource, err := query.NewResourceFromURN(actor.urn) + actorResource, err := query.NewResourceFromURN(actor) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"message": "error processing actor URN", "error": err.Error()}) return diff --git a/internal/api/resources.go b/internal/api/resources.go index ad3b3802..7e8d74f6 100644 --- a/internal/api/resources.go +++ b/internal/api/resources.go @@ -5,16 +5,23 @@ import ( "github.com/gin-gonic/gin" "go.infratographer.com/permissions-api/internal/query" + "go.infratographer.com/x/urnx" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) func (r *Router) resourceCreate(c *gin.Context) { - resourceURN := c.Param("urn") + resourceURNStr := c.Param("urn") - ctx, span := tracer.Start(c.Request.Context(), "api.resourceCreate", trace.WithAttributes(attribute.String("urn", resourceURN))) + ctx, span := tracer.Start(c.Request.Context(), "api.resourceCreate", trace.WithAttributes(attribute.String("urn", resourceURNStr))) defer span.End() + resourceURN, err := urnx.Parse(resourceURNStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "error processing resource URN", "error": err.Error()}) + return + } + resource, err := query.NewResourceFromURN(resourceURN) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"message": "error processing resource URN", "error": err.Error()}) @@ -32,7 +39,7 @@ func (r *Router) resourceCreate(c *gin.Context) { return } - actorResource, err := query.NewResourceFromURN(actor.urn) + actorResource, err := query.NewResourceFromURN(actor) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"message": "error processing actor URN", "error": err.Error()}) return diff --git a/internal/api/router.go b/internal/api/router.go index d6f26d0b..1c9eb79d 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -1,10 +1,10 @@ package api import ( - "strings" - "github.com/authzed/authzed-go/v1" "github.com/gin-gonic/gin" + "go.hollow.sh/toolbox/ginjwt" + "go.infratographer.com/x/urnx" "go.opentelemetry.io/otel" "go.uber.org/zap" ) @@ -13,22 +13,30 @@ var tracer = otel.Tracer("go.infratographer.com/permissions-api/internal/api") // Router provides a router for the API type Router struct { - // db *sqlx.DB + authMW func(*gin.Context) authzedClient *authzed.Client logger *zap.SugaredLogger } -func NewRouter(authzedClient *authzed.Client, l *zap.SugaredLogger) *Router { - return &Router{ +func NewRouter(authCfg ginjwt.AuthConfig, authzedClient *authzed.Client, l *zap.SugaredLogger) (*Router, error) { + authMW, err := newAuthMiddleware(authCfg) + if err != nil { + return nil, err + } + + out := &Router{ + authMW: authMW, authzedClient: authzedClient, logger: l.Named("api"), } + + return out, nil } // Routes will add the routes for this API version to a router group func (r *Router) Routes(rg *gin.RouterGroup) { // /servers - v1 := rg.Group("api/v1") + v1 := rg.Group("api/v1").Use(r.authMW) { // Creating an OU gets a special v1.POST("/resources/:urn", r.resourceCreate) @@ -38,19 +46,8 @@ func (r *Router) Routes(rg *gin.RouterGroup) { } } -type actorToken struct { - // name string - // email string - urn string - token string -} - -func currentActor(c *gin.Context) (*actorToken, error) { - authHeader := c.GetHeader("authorization") - - a := &actorToken{} - a.token = strings.TrimPrefix(authHeader, "bearer ") - a.urn = a.token +func currentActor(c *gin.Context) (*urnx.URN, error) { + subject := ginjwt.GetSubject(c) - return a, nil + return urnx.Parse(subject) } diff --git a/internal/config/config.go b/internal/config/config.go index 30eca1ba..60a13949 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,7 @@ package config import ( + "go.hollow.sh/toolbox/ginjwt" "go.infratographer.com/x/ginx" "go.infratographer.com/x/loggingx" "go.infratographer.com/x/otelx" @@ -9,6 +10,7 @@ import ( ) type AppConfig struct { + OIDC ginjwt.AuthConfig Logging loggingx.Config Server ginx.Config SpiceDB spicedbx.Config diff --git a/internal/query/tenants.go b/internal/query/tenants.go index 4cfdd111..86bff984 100644 --- a/internal/query/tenants.go +++ b/internal/query/tenants.go @@ -4,10 +4,10 @@ import ( "context" "errors" "fmt" - "strings" pb "github.com/authzed/authzed-go/proto/authzed/api/v1" "github.com/authzed/authzed-go/v1" + "go.infratographer.com/x/urnx" ) var roleActorRelation = "subject" @@ -169,11 +169,11 @@ type Resource struct { } type ResourceType struct { - Name string `json:"name"` - URNPrefix string `json:"upn_prefix"` - APIURI string `json:"api_uri"` - DBType string `json:"db_type"` - Relationships []*ResourceRelationship + Name string + URNNamespace string + URNResourceType string + DBType string + Relationships []*ResourceRelationship } type ResourceRelationship struct { @@ -187,9 +187,10 @@ type ResourceRelationship struct { func GetResourceTypes() []*ResourceType { return []*ResourceType{ { - Name: "Tenant", - DBType: "tenant", - URNPrefix: "urn:infratographer:tenant", + Name: "Tenant", + DBType: "tenant", + URNNamespace: "infratographer", + URNResourceType: "tenant", Relationships: []*ResourceRelationship{ { Name: "Parent tenant", @@ -201,26 +202,21 @@ func GetResourceTypes() []*ResourceType { }, }, { - Name: "Subject", - DBType: "subject", - URNPrefix: "urn:infratographer:subject", + Name: "Subject", + DBType: "subject", + URNNamespace: "infratographer", + URNResourceType: "subject", }, } } -func NewResourceFromURN(urn string) (*Resource, error) { - parts := strings.Split(urn, ":") - +func NewResourceFromURN(urn *urnx.URN) (*Resource, error) { r := &Resource{ - URN: urn, - ID: parts[len(parts)-1], + URN: urn.String(), + ID: urn.ResourceID.String(), } - prefixParts := parts[:len(parts)-1] - - prefix := strings.Join(prefixParts, ":") - - rt, err := ResourceTypeByURN(prefix) + rt, err := ResourceTypeByURN(urn) if err != nil { return nil, err } @@ -230,9 +226,10 @@ func NewResourceFromURN(urn string) (*Resource, error) { return r, nil } -func ResourceTypeByURN(urn string) (*ResourceType, error) { +func ResourceTypeByURN(urn *urnx.URN) (*ResourceType, error) { for _, resType := range GetResourceTypes() { - if resType.URNPrefix == urn { + if resType.URNNamespace == urn.Namespace && + resType.URNResourceType == urn.ResourceType { return resType, nil } } @@ -250,7 +247,7 @@ func (r *Resource) spiceDBObjectReference() *pb.ObjectReference { func CreateSpiceDBRelationships(ctx context.Context, db *authzed.Client, r *Resource, actor *Resource) (string, error) { rels := []*pb.RelationshipUpdate{} - if r.ResourceType.URNPrefix == "urn:infratographer:tenant" { + if r.ResourceType.URNResourceType == "tenant" { rels = append(rels, builtInRoles(r)...) rels = append(rels, actorRoleRel(actor, BuiltInRoleAdmins, r)) diff --git a/internal/query/tenants_test.go b/internal/query/tenants_test.go index b476aa32..54c753dc 100644 --- a/internal/query/tenants_test.go +++ b/internal/query/tenants_test.go @@ -16,6 +16,7 @@ import ( "go.infratographer.com/permissions-api/internal/query" "go.infratographer.com/permissions-api/internal/spicedbx" + "go.infratographer.com/x/urnx" ) func dbTest(ctx context.Context, t *testing.T) *query.Stores { @@ -64,11 +65,13 @@ func TestActorScopes(t *testing.T) { var err error - tenURN := "urn:infratographer:tenant:" + uuid.NewString() + tenURN, err := urnx.Build("infratographer", "tenant", uuid.New()) + require.NoError(t, err) tenRes, err := query.NewResourceFromURN(tenURN) require.NoError(t, err) - userURN := "urn:infratographer:subject:" + uuid.NewString() - userRes, err := query.NewResourceFromURN(userURN) + subjURN, err := urnx.Build("infratographer", "subject", uuid.New()) + require.NoError(t, err) + userRes, err := query.NewResourceFromURN(subjURN) require.NoError(t, err) queryToken, err := query.CreateBuiltInRoles(ctx, s.SpiceDB, tenRes) @@ -85,7 +88,9 @@ func TestActorScopes(t *testing.T) { }) t.Run("error returned when the user doesn't have the global scope", func(t *testing.T) { - otherUserRes, err := query.NewResourceFromURN("urn:infratographer:subject:" + uuid.NewString()) + subjURN, err := urnx.Build("infratographer", "subject", uuid.New()) + require.NoError(t, err) + otherUserRes, err := query.NewResourceFromURN(subjURN) require.NoError(t, err) err = query.ActorHasPermission(ctx, s.SpiceDB, otherUserRes, "loadbalancer_get", tenRes, queryToken) diff --git a/permission-api.example.yaml b/permission-api.example.yaml new file mode 100644 index 00000000..27324109 --- /dev/null +++ b/permission-api.example.yaml @@ -0,0 +1,4 @@ +oidc: + issuer: http://localhost:8081/default + audience: permissions-api + jwksuri: http://mock-oauth2-server:8081/default/jwks diff --git a/pkg/pubsubx/worker.go b/pkg/pubsubx/worker.go index 01ab921f..6a8e6c26 100644 --- a/pkg/pubsubx/worker.go +++ b/pkg/pubsubx/worker.go @@ -13,6 +13,7 @@ import ( "github.com/nats-io/nats.go" "go.infratographer.com/permissions-api/internal/query" + "go.infratographer.com/x/urnx" "go.opentelemetry.io/otel" "go.uber.org/zap" "gocloud.dev/pubsub" @@ -200,17 +201,45 @@ func (s *Subscription) Receive(ctx context.Context, st *query.Stores) error { func (s *Subscription) ProcessMessage(ctx context.Context, db *query.Stores, msg *Message) error { switch { case strings.HasSuffix(msg.EventType, ".added"): - resource, err := query.NewResourceFromURN(msg.SubjectURN) + subjectURN, err := urnx.Parse(msg.SubjectURN) if err != nil { - fmt.Println("Error getting resource from URN for subject") + s.logger.Errorw( + "Error getting resource from URN for subject", + "error", + err, + ) + return err + } + + resource, err := query.NewResourceFromURN(subjectURN) + if err != nil { + s.logger.Errorw( + "Error getting resource from URN for subject", + "error", + err, + ) return err } resource.Fields = msg.SubjectFields - actor, err := query.NewResourceFromURN(msg.ActorURN) + actorURN, err := urnx.Parse(msg.ActorURN) + if err != nil { + s.logger.Errorw( + "Error getting resource from URN for actor", + "error", + err, + ) + return err + } + + actor, err := query.NewResourceFromURN(actorURN) if err != nil { - fmt.Println("Error getting resource from URN for actor") + s.logger.Errorw( + "Error getting resource from URN for actor", + "error", + err, + ) return err }