Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
08f2376
feat(policy)!: undo subject mapping operator decomposition
alkalescent Jun 29, 2026
0409e91
fix(policy): drop reserved fields from Condition
alkalescent Jun 30, 2026
15b872b
feat(policy): DSPX-2754 dynamic attribute value entitlement mappings
alkalescent Jun 18, 2026
2c886f1
fix(policy): DSPX-2754 fix service count test and harden dynamic mapp…
alkalescent Jun 18, 2026
1f3c168
fix(policy): DSPX-2754 preserve FK violation for non-existent attribu…
alkalescent Jun 18, 2026
b9189dd
test(policy): DSPX-2754 cover dynamic value mapping list pagination
alkalescent Jun 22, 2026
ffe5f25
test(policy): DSPX-2754 assert dynamic value mapping pages do not ove…
alkalescent Jun 22, 2026
b8728a4
chore(policy): drop Jira and Virtru references from code, migration, …
alkalescent Jun 26, 2026
23867de
feat(policy): gate dynamic value mappings and address review feedback
alkalescent Jun 26, 2026
369b655
feat(policy): adopt operator-based protos in dynamic value mappings
alkalescent Jun 29, 2026
5f9f7fe
fix(policy): address review feedback on dynamic value mappings
alkalescent Jul 1, 2026
c1972c8
fix(policy): drop unreachable unknown-definition guard in DVM indexing
alkalescent Jul 1, 2026
f70b321
Merge remote-tracking branch 'origin/main' into DSPX-2754-rebase-onto…
alkalescent Jul 1, 2026
f6515a1
fix(policy): dedupe DVM actions across entities and expand test coverage
alkalescent Jul 2, 2026
8432ce6
fix(policy): address DVM review feedback
alkalescent Jul 10, 2026
e6c6bf6
fix(policy): address DVM review feedback (RR coexistence guard, migra…
alkalescent Jul 16, 2026
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
10 changes: 5 additions & 5 deletions service/authorization/v2/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func NewRegistration() *serviceregistry.Service[authzV2Connect.AuthorizationServ
}

retriever := access.NewEntitlementPolicyRetriever(as.sdk)
as.cache, err = NewEntitlementPolicyCache(context.Background(), l, retriever, cacheClient, refreshInterval)
as.cache, err = NewEntitlementPolicyCache(context.Background(), l, retriever, cacheClient, refreshInterval, authZCfg.AllowDynamicValueMappings)
if err != nil {
l.Error("failed to create entitlement policy cache", slog.Any("error", err))
panic(fmt.Errorf("failed to create entitlement policy cache: %w", err))
Expand Down Expand Up @@ -147,7 +147,7 @@ func (as *Service) GetEntitlements(ctx context.Context, req *connect.Request[aut
withComprehensiveHierarchy := req.Msg.GetWithComprehensiveHierarchy()

// When authorization service can consume cached policy, switch to the other PDP (process based on policy passed in)
pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements, as.config.EnforceNamespacedEntitlements)
pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements, as.config.AllowDynamicValueMappings, as.config.EnforceNamespacedEntitlements)
if err != nil {
return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToGetEntitlements, ErrFailedToInitPDP, err))
}
Expand All @@ -174,7 +174,7 @@ func (as *Service) GetDecision(ctx context.Context, req *connect.Request[authzV2
return nil, err
}

pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements, as.config.EnforceNamespacedEntitlements)
pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements, as.config.AllowDynamicValueMappings, as.config.EnforceNamespacedEntitlements)
if err != nil {
return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToInitPDP, err))
}
Expand Down Expand Up @@ -224,7 +224,7 @@ func (as *Service) GetDecisionMultiResource(ctx context.Context, req *connect.Re
return nil, err
}

pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements, as.config.EnforceNamespacedEntitlements)
pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements, as.config.AllowDynamicValueMappings, as.config.EnforceNamespacedEntitlements)
if err != nil {
return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToInitPDP, err))
}
Expand Down Expand Up @@ -277,7 +277,7 @@ func (as *Service) GetDecisionBulk(ctx context.Context, req *connect.Request[aut
return nil, err
}

pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements, as.config.EnforceNamespacedEntitlements)
pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements, as.config.AllowDynamicValueMappings, as.config.EnforceNamespacedEntitlements)
if err != nil {
return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToInitPDP, err))
}
Expand Down
65 changes: 57 additions & 8 deletions service/authorization/v2/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import (
)

const (
attributesCacheKey = "attributes_cache_key"
subjectMappingsCacheKey = "subject_mappings_cache_key"
registeredResourcesCacheKey = "registered_resources_cache_key"
obligationsCacheKey = "obligations_cache_key"
attributesCacheKey = "attributes_cache_key"
subjectMappingsCacheKey = "subject_mappings_cache_key"
dynamicValueMappingsCacheKey = "dynamic_value_mappings_cache_key"
registeredResourcesCacheKey = "registered_resources_cache_key"
obligationsCacheKey = "obligations_cache_key"
)

var (
Expand Down Expand Up @@ -48,6 +49,10 @@ type EntitlementPolicyCache struct {
// SDK-connected retriever to fetch fresh data from policy services
retriever *access.EntitlementPolicyRetriever

// allowDynamicValueMappings gates fetching the experimental dynamic value mappings, so cache
// health does not depend on that endpoint when the feature is disabled.
allowDynamicValueMappings bool

// Refresh state
configuredRefreshInterval time.Duration
stopRefresh chan struct{}
Expand All @@ -60,10 +65,11 @@ type EntitlementPolicyCache struct {
// The EntitlementPolicy struct holds all the cached entitlement policy, as generics allow one
// data type per service cache instance.
type EntitlementPolicy struct {
Attributes []*policy.Attribute
SubjectMappings []*policy.SubjectMapping
RegisteredResources []*policy.RegisteredResource
Obligations []*policy.Obligation
Attributes []*policy.Attribute
SubjectMappings []*policy.SubjectMapping
DynamicValueMappings []*policy.DynamicValueMapping
RegisteredResources []*policy.RegisteredResource
Obligations []*policy.Obligation
}

// NewEntitlementPolicyCache holds a platform-provided cache client and manages a periodic refresh of
Expand All @@ -74,6 +80,7 @@ func NewEntitlementPolicyCache(
retriever *access.EntitlementPolicyRetriever,
cacheClient *cache.Cache,
cacheRefreshInterval time.Duration,
allowDynamicValueMappings bool,
) (*EntitlementPolicyCache, error) {
if cacheRefreshInterval == 0 {
return nil, ErrCacheDisabled
Expand All @@ -86,6 +93,7 @@ func NewEntitlementPolicyCache(
logger: l,
cacheClient: cacheClient,
retriever: retriever,
allowDynamicValueMappings: allowDynamicValueMappings,
configuredRefreshInterval: cacheRefreshInterval,
stopRefresh: make(chan struct{}),
refreshCompleted: make(chan struct{}),
Expand Down Expand Up @@ -178,6 +186,15 @@ func (c *EntitlementPolicyCache) Refresh(ctx context.Context) error {
if err != nil {
return err
}
// Only fetch the experimental dynamic value mappings when enabled, so cache readiness does not
// depend on that endpoint while the feature is off.
var dynamicValueMappings []*policy.DynamicValueMapping
if c.allowDynamicValueMappings {
dynamicValueMappings, err = c.retriever.ListAllDynamicValueMappings(ctx)
if err != nil {
return err
}
}
registeredResources, err := c.retriever.ListAllRegisteredResources(ctx)
if err != nil {
return err
Expand All @@ -200,6 +217,16 @@ func (c *EntitlementPolicyCache) Refresh(ctx context.Context) error {
return errors.Join(ErrFailedToSet, err)
}

// Only cache dynamic value mappings when the feature is enabled, so a disabled feature does not
// store an empty slice (the fetch above is gated the same way).
if c.allowDynamicValueMappings {
err = c.cacheClient.Set(ctx, dynamicValueMappingsCacheKey, dynamicValueMappings, authzCacheTags)
if err != nil {
c.isCacheFilled = false
return errors.Join(ErrFailedToSet, err)
}
}

err = c.cacheClient.Set(ctx, registeredResourcesCacheKey, registeredResources, authzCacheTags)
if err != nil {
c.isCacheFilled = false
Expand Down Expand Up @@ -270,6 +297,28 @@ func (c *EntitlementPolicyCache) ListAllSubjectMappings(ctx context.Context) ([]
return subjectMappings, nil
}

// ListAllDynamicValueMappings returns the cached dynamic value entitlement mappings, or none on a cache miss
func (c *EntitlementPolicyCache) ListAllDynamicValueMappings(ctx context.Context) ([]*policy.DynamicValueMapping, error) {
var (
mappings []*policy.DynamicValueMapping
ok bool
)

cached, err := c.cacheClient.Get(ctx, dynamicValueMappingsCacheKey)
if err != nil {
if errors.Is(err, cache.ErrCacheMiss) {
return mappings, nil
}
return nil, fmt.Errorf("%w, dynamic value mappings: %w", ErrFailedToGet, err)
}

mappings, ok = cached.([]*policy.DynamicValueMapping)
if !ok {
return nil, fmt.Errorf("%w: %T", ErrCachedTypeNotExpected, cached)
}
return mappings, nil
}

// ListAllRegisteredResources returns the cached registered resources, or none in the event of a cache miss
func (c *EntitlementPolicyCache) ListAllRegisteredResources(ctx context.Context) ([]*policy.RegisteredResource, error) {
var (
Expand Down
38 changes: 32 additions & 6 deletions service/authorization/v2/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func Test_NewEntitlementPolicyCache(t *testing.T) {
refreshInterval := 10 * time.Second
mockCache, _ := cache.TestCacheClient(mockCacheExpiry)

c, err := NewEntitlementPolicyCache(ctx, l, nil, mockCache, refreshInterval)
c, err := NewEntitlementPolicyCache(ctx, l, nil, mockCache, refreshInterval, false)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
require.NoError(t, err)
assert.NotNil(t, c)
assert.Equal(t, refreshInterval, c.configuredRefreshInterval)
Expand All @@ -33,11 +33,11 @@ func Test_EntitlementPolicyCache_RefreshInterval(t *testing.T) {
ctx := t.Context()
mockCache, _ := cache.TestCacheClient(mockCacheExpiry)

_, err := NewEntitlementPolicyCache(ctx, l, nil, mockCache, refreshInterval)
_, err := NewEntitlementPolicyCache(ctx, l, nil, mockCache, refreshInterval, false)
require.ErrorIs(t, err, ErrCacheDisabled)

refreshInterval = 10 * time.Second
c, err := NewEntitlementPolicyCache(ctx, l, nil, mockCache, refreshInterval)
c, err := NewEntitlementPolicyCache(ctx, l, nil, mockCache, refreshInterval, false)
require.NoError(t, err)
assert.NotNil(t, c)
}
Expand All @@ -53,7 +53,7 @@ func Test_EntitlementPolicyCache_Enabled(t *testing.T) {
assert.False(t, c.IsEnabled())
assert.False(t, c.IsReady(ctx))

c, err = NewEntitlementPolicyCache(ctx, l, nil, mockCache, refreshInterval)
c, err = NewEntitlementPolicyCache(ctx, l, nil, mockCache, refreshInterval, false)
require.NoError(t, err)
assert.NotNil(t, c)
assert.True(t, c.IsEnabled())
Expand All @@ -65,7 +65,7 @@ func Test_EntitlementPolicyCache_CacheMiss(t *testing.T) {
ctx := t.Context()
mockCache, _ := cache.TestCacheClient(mockCacheExpiry)

c, err := NewEntitlementPolicyCache(ctx, l, nil, mockCache, 1*time.Hour)
c, err := NewEntitlementPolicyCache(ctx, l, nil, mockCache, 1*time.Hour, false)
require.NoError(t, err)

// No errors, but empty lists on cache misses
Expand Down Expand Up @@ -93,7 +93,7 @@ func Test_EntitlementPolicyCache_CacheHits(t *testing.T) {
_ = mockCache.Set(ctx, subjectMappingsCacheKey, subjMappingsList, nil)
_ = mockCache.Set(ctx, registeredResourcesCacheKey, resourcesList, nil)

c, err := NewEntitlementPolicyCache(ctx, l, nil, mockCache, 1*time.Hour)
c, err := NewEntitlementPolicyCache(ctx, l, nil, mockCache, 1*time.Hour, false)
require.NoError(t, err)

// Allow for some concurrency overhead in cache library to prevent flakiness in tests
Expand All @@ -114,3 +114,29 @@ func Test_EntitlementPolicyCache_CacheHits(t *testing.T) {
assert.Len(t, registeredResources, 1)
assert.Equal(t, "res1", registeredResources[0].GetName())
}

func Test_EntitlementPolicyCache_DynamicValueMappings(t *testing.T) {
ctx := t.Context()
mockCache, _ := cache.TestCacheClient(mockCacheExpiry)

c, err := NewEntitlementPolicyCache(ctx, l, nil, mockCache, 1*time.Hour, true)
require.NoError(t, err)
assert.True(t, c.allowDynamicValueMappings)

// Cache miss: empty result, no error
mappings, err := c.ListAllDynamicValueMappings(ctx)
require.NoError(t, err)
assert.Empty(t, mappings)

// Cache hit: returns what was set
dvmList := []*policy.DynamicValueMapping{{Id: "dvm-1"}}
_ = mockCache.Set(ctx, dynamicValueMappingsCacheKey, dvmList, nil)

// Allow for some concurrency overhead in cache library to prevent flakiness in tests
time.Sleep(10 * time.Millisecond)

mappings, err = c.ListAllDynamicValueMappings(ctx)
require.NoError(t, err)
assert.Len(t, mappings, 1)
assert.Equal(t, "dvm-1", mappings[0].GetId())
}
4 changes: 4 additions & 0 deletions service/authorization/v2/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ type Config struct {
// enable entity direct entitlements that do not require subject mappings
AllowDirectEntitlements bool `mapstructure:"allow_direct_entitlements" json:"allow_direct_entitlements" default:"false"`

// enable definition-level dynamic value mappings in access decisioning
AllowDynamicValueMappings bool `mapstructure:"allow_dynamic_value_mappings" json:"allow_dynamic_value_mappings" default:"false"`

// enforce strict namespaced entitlement evaluation behavior in access decisioning
EnforceNamespacedEntitlements bool `mapstructure:"enforce_namespaced_entitlements" json:"enforce_namespaced_entitlements" default:"false"`
}
Expand Down Expand Up @@ -107,6 +110,7 @@ func (c *Config) LogValue() slog.Value {
),
),
slog.Bool("allow_direct_entitlements", c.AllowDirectEntitlements),
slog.Bool("allow_dynamic_value_mappings", c.AllowDynamicValueMappings),
slog.Bool("enforce_namespaced_entitlements", c.EnforceNamespacedEntitlements),
)
}
Loading
Loading