Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authenticate API requests #53

Merged
merged 5 commits into from
Mar 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 11 additions & 3 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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) {
Expand All @@ -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))
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
15 changes: 15 additions & 0 deletions internal/api/auth.go
Original file line number Diff line number Diff line change
@@ -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
}
11 changes: 9 additions & 2 deletions internal/api/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()})
Expand All @@ -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
Expand Down
13 changes: 10 additions & 3 deletions internal/api/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()})
Expand All @@ -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
Expand Down
37 changes: 17 additions & 20 deletions internal/api/router.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand All @@ -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)
Expand All @@ -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)
}
2 changes: 2 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -9,6 +10,7 @@ import (
)

type AppConfig struct {
OIDC ginjwt.AuthConfig
Logging loggingx.Config
Server ginx.Config
SpiceDB spicedbx.Config
Expand Down
47 changes: 22 additions & 25 deletions internal/query/tenants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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",
Expand All @@ -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
}
Expand All @@ -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
}
}
Expand All @@ -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))
Expand Down
13 changes: 9 additions & 4 deletions internal/query/tenants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions permission-api.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
oidc:
issuer: http://localhost:8081/default
audience: permissions-api
jwksuri: http://mock-oauth2-server:8081/default/jwks
Loading