Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -166,14 +166,8 @@ func runService(cfg config.Config) error {
var requireLocalScopeCheck = false

if cfg.Auth.Enabled {
// Check for a static policy bearer token before validation so that
// ValidateAuthConfig can accurately skip OAuth2 field checks.
const policyBearerTokenKey = "policy-bearer-token"
_, hasStaticPolicyTokenErr := credentials.ReadTokenFromFile(secretsPath, policyBearerTokenKey)
hasStaticPolicyToken := hasStaticPolicyTokenErr == nil

// Validate auth configuration
if err := config.ValidateAuthConfig(cfg.Auth, hasStaticPolicyToken); err != nil {
if err := config.ValidateAuthConfig(cfg.Auth, cfg.SelfManaged); err != nil {
logger.Error("invalid auth configuration", zap.Error(err))
return err
}
Expand Down Expand Up @@ -237,27 +231,18 @@ func runService(cfg config.Config) error {

var policyClient policy.Authorizer

// If a bearer token exists in the secrets file, use it to authenticate
// outbound calls to the policy evaluator when no OAuth2 token issuer is available.
// Use a static (no-auth) client in self-managed deployments.
// Otherwise fall back to the OAuth2 client-credentials flow.
if hasStaticPolicyToken {
logger.Warn("using static bearer token for policy evaluator",
zap.String("secrets_path", secretsPath))

tokenReader, err := credentials.NewBearerTokenReader(secretsPath, policyBearerTokenKey)
if err != nil {
logger.Error("failed to create bearer token reader", zap.Error(err))
return fmt.Errorf("failed to create bearer token reader: %w", err)
}
if cfg.SelfManaged {
logger.Warn("self-managed mode: using api-keys client for policy evaluator")

policyClient = policy.NewStaticBearerClient(
policyClient = policy.NewApiKeysClient(
cfg.Auth.Policy.PolicyEvaluatorAddr,
policyConfig,
tokenReader,
middleware.GetSharedHTTPClient(&cfg.HTTP),
)
} else {
logger.Warn("static bearer token not found, using OAuth2 for policy evaluator",
logger.Warn("using OAuth2 for policy evaluator",
zap.String("token_issuer", cfg.Auth.Policy.TokenIssuerAddr))

oidcConfig := &auth.ProviderConfig{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func TestValidateAuthConfig_PolicyProvider(t *testing.T) {
tests := []struct {
name string
cfg AuthConfig
hasStaticPolicyToken bool
selfManaged bool
expectedErr error
}{
{
Expand Down Expand Up @@ -252,9 +252,9 @@ func TestValidateAuthConfig_PolicyProvider(t *testing.T) {
expectedErr: ErrInvalidPolicyCredsRefreshInterval,
},
{
// When a static policy token is available, OAuth2 fields are not required.
name: "valid config with static policy token - oauth2 fields not required",
hasStaticPolicyToken: true,
// In self-managed mode, OAuth2 fields are not required.
name: "valid config in self-managed mode - oauth2 fields not required",
selfManaged: true,
cfg: AuthConfig{
Enabled: true,
Provider: "policy",
Expand All @@ -268,9 +268,9 @@ func TestValidateAuthConfig_PolicyProvider(t *testing.T) {
expectedErr: nil,
},
{
// Even with a static policy token, the always-required fields are still checked.
name: "static policy token does not bypass namespace check",
hasStaticPolicyToken: true,
// Even in self-managed mode, always-required fields are still checked.
name: "self-managed mode does not bypass namespace check",
selfManaged: true,
cfg: AuthConfig{
Enabled: true,
Provider: "policy",
Expand All @@ -286,7 +286,7 @@ func TestValidateAuthConfig_PolicyProvider(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateAuthConfig(tt.cfg, tt.hasStaticPolicyToken)
err := ValidateAuthConfig(tt.cfg, tt.selfManaged)
if tt.expectedErr != nil {
require.Error(t, err)
assert.Equal(t, tt.expectedErr, err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func (c *CliArgs) Register(svcName string) {
c.SetupPublisher()
c.SetupHTTP()
c.SetupClientCredentials()
c.SetupDeploymentMode()
}

func (c *CliArgs) SetupLogging(svcName string) {
Expand Down Expand Up @@ -167,6 +168,10 @@ func (c *CliArgs) SetupClientCredentials() {
c.string("secret", "", "OAuth client secret", true)
}

func (c *CliArgs) SetupDeploymentMode() {
c.bool("self-managed", false, "Enable self-managed deployment mode (disables OAuth2 requirements)", true)
}

func (c *CliArgs) string(name string, defaultValue string, usage string, bindToFlag bool) {
c.rootCmd.Flags().String(name, defaultValue, usage)
if bindToFlag {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type Config struct {
Secret string `mapstructure:"secret"`
DeprecateEndpoints bool `mapstructure:"deprecate-endpoints"`
SecretsPath string `mapstructure:"secrets-path"`
SelfManaged bool `mapstructure:"self-managed"`
}

type PublisherConfig struct {
Expand Down Expand Up @@ -101,9 +102,8 @@ func (p PolicyConfig) WithDefaults() PolicyConfig {
}

// ValidateAuthConfig validates the authentication configuration.
// hasStaticPolicyToken indicates whether a usable static bearer token was found
// in the secrets file; when true the OAuth2 credential fields are not required.
func ValidateAuthConfig(cfg AuthConfig, hasStaticPolicyToken bool) error {
// selfManaged indicates a self-managed deployment where OAuth2 credential fields are not required.
func ValidateAuthConfig(cfg AuthConfig, selfManaged bool) error {
if !cfg.Enabled {
return nil
}
Expand Down Expand Up @@ -135,7 +135,7 @@ func ValidateAuthConfig(cfg AuthConfig, hasStaticPolicyToken bool) error {
if cfg.Policy.PolicyFQDN == "" {
return ErrMissingPolicyFQDN
}
if !hasStaticPolicyToken {
if !selfManaged {
if cfg.Policy.CredsFile == "" {
return ErrMissingPolicyCredsFile
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ func mergePolicyClaims(jwtClaims map[string]interface{}, authResponse PolicyAuth
}

// NewPolicyMiddleware creates a new Policy middleware
func NewPolicyMiddleware(authzClient policy.Authorizer, serviceName string, jwtPubKeySetURL string, jwtTokenExpiration time.Duration, jwkCache *jwk.Cache, httpConfig *config.HTTPClientConfig, logger *otelzap.Logger) mux.MiddlewareFunc {
if authzClient == nil {
func NewPolicyMiddleware(policyClient policy.Authorizer, serviceName string, jwtPubKeySetURL string, jwtTokenExpiration time.Duration, jwkCache *jwk.Cache, httpConfig *config.HTTPClientConfig, logger *otelzap.Logger) mux.MiddlewareFunc {
if policyClient == nil {
if logger != nil {
logger.Error("policy client is nil - denying requests")
}
Expand Down Expand Up @@ -219,7 +219,7 @@ func NewPolicyMiddleware(authzClient policy.Authorizer, serviceName string, jwtP
logger := logging.GetLogger(traceCtx)
logger.InfoContext(traceCtx, "policy: processing request", zap.String("path", r.URL.Path), zap.String("method", r.Method))

policyConfig := authzClient.PolicyConfig()
policyConfig := policyClient.PolicyConfig()
subjectField, apiKeyField := policyInputFields(policyConfig)

// 1. Extract the token (simple bearer token extraction)
Expand Down Expand Up @@ -305,7 +305,7 @@ func NewPolicyMiddleware(authzClient policy.Authorizer, serviceName string, jwtP

// 5. Call Policy service
logger.InfoContext(traceCtx, "policy: calling policy evaluate service")
authzResp, err := authzClient.Evaluate(traceCtx, authReq)
authzResp, err := policyClient.Evaluate(traceCtx, authReq)
if err != nil {
logger.ErrorContext(traceCtx, "policy: evaluation failed", zap.Error(err))
http.Error(w, "Unauthorized", http.StatusUnauthorized)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,44 +23,32 @@ import (
"fmt"
"net/http"

"go.uber.org/zap"

pdpv1 "github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/nvkit/clients/pdp_types"

"github.com/NVIDIA/nvcf/src/control-plane-services/event-ledger/internal/credentials"
)

// staticBearerClient implements Authorizer for self-managed deployments where no
// OAuth2 token issuer is available. It authenticates outbound calls to the policy
// evaluator using a static bearer token read from the Vault Agent secrets file.
type staticBearerClient struct {
// ApiKeysClient implements Authorizer for self-managed deployments where the
// api-keys-api evaluation endpoint requires no authentication.
type ApiKeysClient struct {
evaluatorAddr string
policyCfg *PolicyConfig
tokenReader *credentials.BearerTokenReader
httpClient *http.Client
}

// NewStaticBearerClient creates an Authorizer that presents a static bearer token
// when calling the policy evaluator, instead of performing a live OAuth2 exchange.
func NewStaticBearerClient(
evaluatorAddr string,
policyCfg *PolicyConfig,
tokenReader *credentials.BearerTokenReader,
httpClient *http.Client,
) Authorizer {
return &staticBearerClient{
// NewApiKeysClient creates an Authorizer that calls the api-keys-api evaluation
// endpoint without authentication, for use in self-managed deployments.
func NewApiKeysClient(evaluatorAddr string, policyCfg *PolicyConfig, httpClient *http.Client) Authorizer {
return &ApiKeysClient{
evaluatorAddr: evaluatorAddr,
policyCfg: policyCfg,
tokenReader: tokenReader,
httpClient: httpClient,
}
}

func (c *staticBearerClient) PolicyConfig() *PolicyConfig {
func (c *ApiKeysClient) PolicyConfig() *PolicyConfig {
return c.policyCfg
}

func (c *staticBearerClient) Evaluate(ctx context.Context, req *pdpv1.RuleRequest) (*pdpv1.RuleResponse, error) {
func (c *ApiKeysClient) Evaluate(ctx context.Context, req *pdpv1.RuleRequest) (*pdpv1.RuleResponse, error) {
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal rule request: %w", err)
Expand All @@ -74,7 +62,6 @@ func (c *staticBearerClient) Evaluate(ctx context.Context, req *pdpv1.RuleReques

httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+c.tokenReader.Token())

resp, err := c.httpClient.Do(httpReq)
if err != nil {
Expand All @@ -88,10 +75,7 @@ func (c *staticBearerClient) Evaluate(ctx context.Context, req *pdpv1.RuleReques
}

if resp.StatusCode != http.StatusOK {
zap.L().Error("policy evaluator returned non-200",
zap.Int("status_code", resp.StatusCode),
)
return nil, fmt.Errorf("policy evaluator returned status %d", resp.StatusCode)
return nil, fmt.Errorf("api-keys-api returned status %d", resp.StatusCode)
}

var ruleResp pdpv1.RuleResponse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,20 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"

pdpv1 "github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/nvkit/clients/pdp_types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/NVIDIA/nvcf/src/control-plane-services/event-ledger/internal/credentials"
)

func newTestTokenReader(t *testing.T, token string) *credentials.BearerTokenReader {
t.Helper()
path := filepath.Join(t.TempDir(), "secrets.json")
data, err := json.Marshal(map[string]any{"token": token})
require.NoError(t, err)
require.NoError(t, os.WriteFile(path, data, 0600))
r, err := credentials.NewBearerTokenReader(path, "token")
require.NoError(t, err)
t.Cleanup(func() { r.Close() })
return r
}

func TestStaticBearerClient_PolicyConfig(t *testing.T) {
func TestAPIKeysClient_PolicyConfig(t *testing.T) {
cfg := &PolicyConfig{Namespace: "event-ledger", PolicyFQDN: "apikey.allow"}
client := NewStaticBearerClient("http://example.com", cfg, newTestTokenReader(t, "tok"), &http.Client{})
client := NewApiKeysClient("http://example.com", cfg, &http.Client{})
assert.Equal(t, cfg, client.PolicyConfig())
}

func TestStaticBearerClient_Evaluate(t *testing.T) {
func TestAPIKeysClient_Evaluate(t *testing.T) {
allowResponse := map[string]any{
"result": map[string]any{"allow": true},
}
Expand All @@ -60,14 +44,14 @@ func TestStaticBearerClient_Evaluate(t *testing.T) {
serverResponse any
serverStatus int
wantErr bool
checkAuth bool
checkNoAuth bool
checkURL bool
}{
{
name: "sends correct Authorization header and URL",
name: "sends no Authorization header and correct URL",
serverStatus: http.StatusOK,
serverResponse: allowResponse,
checkAuth: true,
checkNoAuth: true,
checkURL: true,
},
{
Expand All @@ -77,7 +61,7 @@ func TestStaticBearerClient_Evaluate(t *testing.T) {
},
{
name: "returns error on non-200 status",
serverStatus: http.StatusForbidden,
serverStatus: http.StatusInternalServerError,
wantErr: true,
},
}
Expand All @@ -97,7 +81,7 @@ func TestStaticBearerClient_Evaluate(t *testing.T) {
defer srv.Close()

cfg := &PolicyConfig{Namespace: "event-ledger", PolicyFQDN: "apikey.allow"}
client := NewStaticBearerClient(srv.URL, cfg, newTestTokenReader(t, "test-bearer-token"), &http.Client{})
client := NewApiKeysClient(srv.URL, cfg, &http.Client{})

req := &pdpv1.RuleRequest{
Namespace: "event-ledger",
Expand All @@ -112,8 +96,8 @@ func TestStaticBearerClient_Evaluate(t *testing.T) {
}
require.NoError(t, err)

if tc.checkAuth {
assert.Equal(t, "Bearer test-bearer-token", capturedAuth)
if tc.checkNoAuth {
assert.Empty(t, capturedAuth, "expected no Authorization header")
}
if tc.checkURL {
assert.Equal(t, "/v1/namespaces/event-ledger/evaluations/apikey.allow", capturedURL)
Expand Down
Loading