Skip to content
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
29 changes: 29 additions & 0 deletions service/internal/auth/authz/casbin/casbin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,35 @@ func (s *CasbinAuthorizerSuite) TestAuthorizeV2_AdminWildcard() {
s.Equal(authz.ModeV2, decision.Mode)
}

func (s *CasbinAuthorizerSuite) TestAuthorizeV2_DefaultPolicyIncludesDefaultRoleGroupings() {
cfg := authz.Config{
Version: "v2",
PolicyConfig: authz.PolicyConfig{
GroupsClaim: "realm_access.roles",
},
Logger: s.logger,
}

authorizer, err := NewAuthorizer(cfg)
s.Require().NoError(err)

token := createTestToken(s.T(), map[string]interface{}{
"realm_access": map[string]interface{}{
"roles": []interface{}{"opentdf-admin"},
},
})
req := &authz.Request{
Token: token,
RPC: "/policy.attributes.AttributesService/UpdateAttribute",
Action: "write",
}

decision, err := authorizer.Authorize(s.T().Context(), req)
s.Require().NoError(err)
s.Require().NotNil(decision)
s.True(decision.Allowed, "default v2 policy should map opentdf-admin to role:admin")
}

func (s *CasbinAuthorizerSuite) TestAuthorizeV2_NamespaceScopedAccess() {
// Policy: hr-admin can only access HR namespace
cfg := authz.Config{
Expand Down
2 changes: 2 additions & 0 deletions service/internal/auth/authz/casbin/policy.csv
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
# Admin Role - Full Access
# ============================================================================
p, role:admin, *, *, allow
g, role:opentdf-admin, role:admin

# ============================================================================
# Standard Role - Authenticated Users
# ============================================================================
g, role:opentdf-standard, role:standard

# Discovery and health endpoints
p, role:standard, /wellknownconfiguration.WellKnownService/*, *, allow
Expand Down
53 changes: 53 additions & 0 deletions service/internal/auth/interceptor_authz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import (
"strings"
"testing"

"connectrpc.com/connect"
"github.com/creasty/defaults"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/opentdf/platform/protocol/go/policy/attributes"
"github.com/opentdf/platform/service/internal/auth/authz"
_ "github.com/opentdf/platform/service/internal/auth/authz/casbin" // Register casbin authorizer
"github.com/opentdf/platform/service/logger"
"github.com/stretchr/testify/suite"
"google.golang.org/grpc"
)

// InterceptorAuthzSuite tests the authorization flow through the interceptor
Expand Down Expand Up @@ -456,6 +459,47 @@ p, role:finance-admin, /policy.attributes.AttributesService/*, namespace=finance
s.True(decision.Allowed, "finance-admin should be allowed with namespace=finance dimension")
}

func (s *InterceptorAuthzSuite) TestAuthorizeV2_InvokesRegisteredResolver() {
csvPolicy := "p, role:hr-admin, /policy.attributes.AttributesService/*, namespace=hr, allow"
registry := authz.NewResolverRegistry()
scopedRegistry := registry.ScopedForService(&grpc.ServiceDesc{
ServiceName: "policy.attributes.AttributesService",
Methods: []grpc.MethodDesc{
{MethodName: "UpdateAttribute"},
},
})

resolverCalled := false
scopedRegistry.MustRegister("UpdateAttribute", func(_ context.Context, _ connect.AnyRequest) (authz.ResolverContext, error) {
resolverCalled = true
resolverCtx := authz.NewResolverContext()
res := resolverCtx.NewResource()
res.AddDimension("namespace", "hr")
resolverCtx.SetResolvedData("attribute", "resolved")
return resolverCtx, nil
})

authn := &Authentication{
logger: s.logger,
authorizer: s.createV2Authorizer(csvPolicy),
authzResolverRegistry: registry,
}

req := &authzTestRequest{
Request: connect.NewRequest(&attributes.GetAttributeRequest{}),
procedure: "/policy.attributes.AttributesService/UpdateAttribute",
}

result := authn.authorize(s.T().Context(), s.logger, s.newTokenWithRoles("hr-admin"), req, ActionWrite)
Comment thread
c-r33d marked this conversation as resolved.

s.Require().NoError(result.err)
s.Require().NotNil(result.decision)
s.True(result.decision.Allowed)
s.True(resolverCalled, "registered resolver should be invoked")
s.Require().NotNil(result.resourceContext)
s.Equal("resolved", result.resourceContext.GetResolvedData("attribute"))
}

func (s *InterceptorAuthzSuite) TestV2_EmptyToken() {
csvPolicy := "p, role:admin, *, *, allow"
authorizer := s.createV2Authorizer(csvPolicy)
Expand All @@ -475,6 +519,15 @@ func (s *InterceptorAuthzSuite) TestV2_EmptyToken() {
s.False(decision.Allowed, "empty token should be denied")
}

type authzTestRequest struct {
*connect.Request[attributes.GetAttributeRequest]
procedure string
}

func (r *authzTestRequest) Spec() connect.Spec {
return connect.Spec{Procedure: r.procedure}
}

// =============================================================================
// Action Mapping Tests (used by getAction in the interceptor)
// =============================================================================
Expand Down
8 changes: 7 additions & 1 deletion service/internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/opentdf/platform/sdk"
sdkAudit "github.com/opentdf/platform/sdk/audit"
"github.com/opentdf/platform/service/internal/auth"
"github.com/opentdf/platform/service/internal/auth/authz"
"github.com/opentdf/platform/service/internal/security"
"github.com/opentdf/platform/service/internal/server/memhttp"
"github.com/opentdf/platform/service/logger"
Expand Down Expand Up @@ -70,6 +71,9 @@ type Config struct {
EnablePprof bool `mapstructure:"enable_pprof" json:"enable_pprof" default:"false"`
// Trace is for configuring open telemetry based tracing.
Trace tracing.Config `mapstructure:"trace" json:"trace"`

// AuthzResolverRegistry contains service-registered resolvers used by the auth interceptor.
AuthzResolverRegistry *authz.ResolverRegistry `mapstructure:"-" json:"-"`
}

func (c Config) LogValue() slog.Value {
Expand Down Expand Up @@ -265,6 +269,7 @@ func NewOpenTDFServer(config Config, logger *logger.Logger, cacheManager *cache.
config.Auth,
logger,
config.WellKnownConfigRegister,
auth.WithAuthzResolverRegistry(config.AuthzResolverRegistry),
)
if err != nil {
return nil, fmt.Errorf("failed to create authentication interceptor: %w", err)
Expand Down Expand Up @@ -352,7 +357,8 @@ func newHTTPServer(c Config, connectRPC http.Handler, extraHTTP http.Handler, a
effectiveExposed := c.CORS.EffectiveExposedHeaders()

// Log effective CORS config for operator visibility
l.Info("CORS middleware enabled",
l.Info(
"CORS middleware enabled",
slog.Any("allowed_origins", c.CORS.AllowedOrigins),
slog.Any("effective_methods", effectiveMethods),
slog.Any("effective_headers", effectiveHeaders),
Expand Down
9 changes: 5 additions & 4 deletions service/pkg/server/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ func Start(f ...StartOptions) error {
cfg.Server.CORS.AdditionalExposedHeaders = append(cfg.Server.CORS.AdditionalExposedHeaders, startConfig.additionalCORSExposedHeaders...)
}

// Create the global authz resolver registry before the server/authenticator.
// Services receive scoped views of this same registry during startup.
authzResolverRegistry := authz.NewResolverRegistry()
cfg.Server.AuthzResolverRegistry = authzResolverRegistry

// Create new server for grpc & http. Also will support in process grpc potentially too
logger.Debug("initializing opentdf server")
cfg.Server.WellKnownConfigRegister = wellknown.RegisterConfiguration
Expand Down Expand Up @@ -283,10 +288,6 @@ func Start(f ...StartOptions) error {

defer client.Close()

// Create the global authz resolver registry
// Services will receive scoped registries that can only register resolvers for their own methods
authzResolverRegistry := authz.NewResolverRegistry()

logger.Info("starting services")
err = startServices(ctx, startServicesParams{
cfg: cfg,
Expand Down
Loading