Skip to content

Commit

Permalink
add create/delete of auth relationships to permissions package
Browse files Browse the repository at this point in the history
Signed-off-by: Mike Mason <[email protected]>
  • Loading branch information
mikemrm committed Aug 7, 2023
1 parent 77f1954 commit 168b60b
Show file tree
Hide file tree
Showing 7 changed files with 542 additions and 11 deletions.
6 changes: 6 additions & 0 deletions pkg/permissions/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@ import (
type Config struct {
// URL is the URL checks should be executed against
URL string

// IgnoreNoResponders will ignore no responder errors when auth relationship requests are published.
IgnoreNoResponders bool
}

// MustViperFlags adds permissions config flags and viper bindings
func MustViperFlags(v *viper.Viper, flags *pflag.FlagSet) {
flags.String("permissions-url", "", "sets the permissions url checks should be run against")
viperx.MustBindFlag(v, "permissions.url", flags.Lookup("permissions-url"))

flags.String("permissions-ignore-no-responders", "", "ignores no responder errors when auth relationship requests are published")
viperx.MustBindFlag(v, "permissions.ignoreAuthRelationshipNoResponders", flags.Lookup("permissions-ignore-no-responders"))
}
50 changes: 50 additions & 0 deletions pkg/permissions/mockpermissions/permissions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Package mockpermissions implements permissions.AuthRelationshipRequestHandler.
// Simplifying testing of relationship creation in applications.
package mockpermissions

import (
"context"

"github.com/stretchr/testify/mock"
"go.infratographer.com/permissions-api/pkg/permissions"
"go.infratographer.com/x/events"
"go.infratographer.com/x/gidx"
)

var _ permissions.AuthRelationshipRequestHandler = (*MockPermissions)(nil)

// MockPermissions implements permissions.AuthRelationshipRequestHandler.
type MockPermissions struct {
mock.Mock
}

// ContextWithHandler returns the context with the mock permissions handler defined.
func (p *MockPermissions) ContextWithHandler(ctx context.Context) context.Context {
return context.WithValue(ctx, permissions.AuthRelationshipRequestHandlerCtxKey, p)
}

// CreateAuthRelationships implements permissions.AuthRelationshipRequestHandler.
func (p *MockPermissions) CreateAuthRelationships(ctx context.Context, topic string, resourceID gidx.PrefixedID, relations ...events.AuthRelationshipRelation) error {
calledArgs := []interface{}{topic, resourceID}

for _, rel := range relations {
calledArgs = append(calledArgs, rel)
}

args := p.Called(calledArgs...)

return args.Error(0)
}

// DeleteAuthRelationships implements permissions.AuthRelationshipRequestHandler.
func (p *MockPermissions) DeleteAuthRelationships(ctx context.Context, topic string, resourceID gidx.PrefixedID, relations ...events.AuthRelationshipRelation) error {
calledArgs := []interface{}{topic, resourceID}

for _, rel := range relations {
calledArgs = append(calledArgs, rel)
}

args := p.Called(calledArgs...)

return args.Error(0)
}
49 changes: 49 additions & 0 deletions pkg/permissions/mockpermissions/permissions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package mockpermissions_test

import (
"context"
"testing"

"github.com/stretchr/testify/require"
"go.infratographer.com/permissions-api/pkg/permissions"
"go.infratographer.com/permissions-api/pkg/permissions/mockpermissions"
"go.infratographer.com/x/events"
"go.infratographer.com/x/gidx"
)

func TestPermissions(t *testing.T) {
t.Run("create", func(t *testing.T) {
mockPerms := new(mockpermissions.MockPermissions)

ctx := mockPerms.ContextWithHandler(context.Background())

relation := events.AuthRelationshipRelation{
Relation: "parent",
SubjectID: "tnntten-abc",
}

mockPerms.On("CreateAuthRelationships", "test", gidx.PrefixedID("tnntten-abc123"), relation).Return(nil)

err := permissions.CreateAuthRelationships(ctx, "test", "tnntten-abc123", relation)
require.NoError(t, err)

mockPerms.AssertExpectations(t)
})
t.Run("delete", func(t *testing.T) {
mockPerms := new(mockpermissions.MockPermissions)

ctx := mockPerms.ContextWithHandler(context.Background())

relation := events.AuthRelationshipRelation{
Relation: "parent",
SubjectID: "tnntten-abc",
}

mockPerms.On("DeleteAuthRelationships", "test", gidx.PrefixedID("tnntten-abc123"), relation).Return(nil)

err := permissions.DeleteAuthRelationships(ctx, "test", "tnntten-abc123", relation)
require.NoError(t, err)

mockPerms.AssertExpectations(t)
})
}
10 changes: 10 additions & 0 deletions pkg/permissions/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"

"github.com/labstack/echo/v4/middleware"
"go.infratographer.com/x/events"
"go.uber.org/zap"
)

Expand All @@ -19,6 +20,15 @@ func WithLogger(logger *zap.SugaredLogger) Option {
}
}

// WithEventsPublisher sets the underlying event publisher the auth handler uses
func WithEventsPublisher(publisher events.AuthRelationshipPublisher) Option {
return func(p *Permissions) error {
p.publisher = publisher

return nil
}
}

// WithHTTPClient sets the underlying http client the auth handler uses
func WithHTTPClient(client *http.Client) Option {
return func(p *Permissions) error {
Expand Down
32 changes: 21 additions & 11 deletions pkg/permissions/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/labstack/echo/v4/middleware"
"github.com/pkg/errors"
"go.infratographer.com/x/echojwtx"
"go.infratographer.com/x/events"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
Expand All @@ -35,23 +36,31 @@ var (
}

tracer = otel.GetTracerProvider().Tracer("go.infratographer.com/permissions-api/pkg/permissions")

// ErrPermissionsMiddlewareMissing is returned when a permissions method has been called but the middleware is missing.
ErrPermissionsMiddlewareMissing = errors.New("permissions middleware missing")
)

// Permissions handles supporting authorization checks
type Permissions struct {
enabled bool
logger *zap.SugaredLogger
client *http.Client
url *url.URL
skipper middleware.Skipper
defaultChecker Checker
enableChecker bool

logger *zap.SugaredLogger
publisher events.AuthRelationshipPublisher
client *http.Client
url *url.URL
skipper middleware.Skipper
defaultChecker Checker
ignoreNoResponders bool
}

// Middleware produces echo middleware to handle authorization checks
func (p *Permissions) Middleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if !p.enabled || p.skipper(c) {
setAuthRelationshipRequestHandler(c, p)

if !p.enableChecker || p.skipper(c) {
setCheckerContext(c, p.defaultChecker)

return next(c)
Expand Down Expand Up @@ -161,10 +170,11 @@ func (p *Permissions) checker(c echo.Context, actor, token string) Checker {
// New creates a new Permissions instance
func New(config Config, options ...Option) (*Permissions, error) {
p := &Permissions{
enabled: config.URL != "",
client: defaultClient,
skipper: middleware.DefaultSkipper,
defaultChecker: DefaultDenyChecker,
enableChecker: config.URL != "",
client: defaultClient,
skipper: middleware.DefaultSkipper,
defaultChecker: DefaultDenyChecker,
ignoreNoResponders: config.IgnoreNoResponders,
}

if config.URL != "" {
Expand Down
140 changes: 140 additions & 0 deletions pkg/permissions/relationships.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package permissions

import (
"context"
"errors"

"github.com/labstack/echo/v4"
"go.infratographer.com/x/events"
"go.infratographer.com/x/gidx"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"go.uber.org/multierr"
)

var (
// AuthRelationshipRequestHandlerCtxKey is the context key used to set the auth relationship request handler.
AuthRelationshipRequestHandlerCtxKey = authRelationshipRequestHandlerCtxKey{}
)

type authRelationshipRequestHandlerCtxKey struct{}

func setAuthRelationshipRequestHandler(c echo.Context, requestHandler AuthRelationshipRequestHandler) {

req := c.Request().WithContext(
context.WithValue(
c.Request().Context(),
AuthRelationshipRequestHandlerCtxKey,
requestHandler,
),
)

c.SetRequest(req)
}

// AuthRelationshipRequestHandler defines the required methods to create or update an auth relationship.
type AuthRelationshipRequestHandler interface {
CreateAuthRelationships(ctx context.Context, topic string, resourceID gidx.PrefixedID, relations ...events.AuthRelationshipRelation) error
DeleteAuthRelationships(ctx context.Context, topic string, resourceID gidx.PrefixedID, relations ...events.AuthRelationshipRelation) error
}

func (p *Permissions) submitAuthRelationshipRequest(ctx context.Context, topic string, request events.AuthRelationshipRequest) error {
ctx, span := tracer.Start(ctx, "permissions.submitAuthRelationshipRequest",
trace.WithAttributes(
attribute.String("events.topic", topic),
attribute.String("events.object_id", request.ObjectID.String()),
attribute.String("events.action", string(request.Action)),
),
)

defer span.End()

if err := request.Validate(); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())

return err
}

// if no publisher is defined, requests are disabled.
if p.publisher == nil {
span.AddEvent("publish requests disabled")

return nil
}

var errs []error

resp, err := p.publisher.PublishAuthRelationshipRequest(ctx, topic, request)
if err != nil {
if p.ignoreNoResponders && errors.Is(err, events.ErrRequestNoResponders) {
span.AddEvent("ignored no request responders")

return nil
}

errs = append(errs, err)
}

if resp != nil {
if resp.Error() != nil {
errs = append(errs, err)
}

errs = append(errs, resp.Message().Errors...)
}

err = multierr.Combine(errs...)

if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())

return err
}

return nil
}

// CreateAuthRelationships publishes a create auth relationship request, blocking until a response has been received.
func (p *Permissions) CreateAuthRelationships(ctx context.Context, topic string, resourceID gidx.PrefixedID, relations ...events.AuthRelationshipRelation) error {
request := events.AuthRelationshipRequest{
Action: events.WriteAuthRelationshipAction,
ObjectID: resourceID,
Relations: relations,
}

return p.submitAuthRelationshipRequest(ctx, topic, request)
}

// DeleteAuthRelationships publishes a delete auth relationship request, blocking until a response has been received.
func (p *Permissions) DeleteAuthRelationships(ctx context.Context, topic string, resourceID gidx.PrefixedID, relations ...events.AuthRelationshipRelation) error {
request := events.AuthRelationshipRequest{
Action: events.DeleteAuthRelationshipAction,
ObjectID: resourceID,
Relations: relations,
}

return p.submitAuthRelationshipRequest(ctx, topic, request)
}

// CreateAuthRelationships publishes a create auth relationship request, blocking until a response has been received.
func CreateAuthRelationships(ctx context.Context, topic string, resourceID gidx.PrefixedID, relations ...events.AuthRelationshipRelation) error {
handler, ok := ctx.Value(AuthRelationshipRequestHandlerCtxKey).(AuthRelationshipRequestHandler)
if !ok {
return ErrPermissionsMiddlewareMissing
}

return handler.CreateAuthRelationships(ctx, topic, resourceID, relations...)
}

// DeleteAuthRelationships publishes a delete auth relationship request, blocking until a response has been received.
func DeleteAuthRelationships(ctx context.Context, topic string, resourceID gidx.PrefixedID, relations ...events.AuthRelationshipRelation) error {
handler, ok := ctx.Value(AuthRelationshipRequestHandlerCtxKey).(AuthRelationshipRequestHandler)
if !ok {
return ErrPermissionsMiddlewareMissing
}

return handler.DeleteAuthRelationships(ctx, topic, resourceID, relations...)
}
Loading

0 comments on commit 168b60b

Please sign in to comment.