From d89cc09ebc7f71e69392f896328cc9c9aa2d0a70 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Thu, 11 Sep 2025 13:30:03 -0700 Subject: [PATCH 1/9] feat(authz): custom PDPs extension point proof of concept --- service/authorization/v2/authorization.go | 33 ++++- service/authorization/v2/cache.go | 6 +- service/authorization/v2/config.go | 5 +- service/cmd/start.go | 4 + ...time_pdp.go => just_in_time_authorizer.go} | 81 +++++++++--- service/internal/access/v2/plugin/granular.go | 118 ++++++++++++++++++ service/internal/access/v2/plugin/plugin.go | 40 ++++++ .../access/v2/{ => store}/policy_store.go | 2 +- service/pkg/server/options.go | 10 ++ service/pkg/server/services.go | 18 +-- .../pkg/serviceregistry/serviceregistry.go | 4 +- 11 files changed, 288 insertions(+), 33 deletions(-) rename service/internal/access/v2/{just_in_time_pdp.go => just_in_time_authorizer.go} (77%) create mode 100644 service/internal/access/v2/plugin/granular.go create mode 100644 service/internal/access/v2/plugin/plugin.go rename service/internal/access/v2/{ => store}/policy_store.go (99%) diff --git a/service/authorization/v2/authorization.go b/service/authorization/v2/authorization.go index 0e633dde9e..a8fe10d905 100644 --- a/service/authorization/v2/authorization.go +++ b/service/authorization/v2/authorization.go @@ -14,6 +14,8 @@ import ( authzV2Connect "github.com/opentdf/platform/protocol/go/authorization/v2/authorizationv2connect" otdf "github.com/opentdf/platform/sdk" "github.com/opentdf/platform/service/internal/access/v2" + "github.com/opentdf/platform/service/internal/access/v2/plugin" + policyStore "github.com/opentdf/platform/service/internal/access/v2/store" "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/pkg/cache" "github.com/opentdf/platform/service/pkg/serviceregistry" @@ -29,6 +31,9 @@ type Service struct { logger *logger.Logger trace.Tracer cache *EntitlementPolicyCache + // Config drives names and attribute prefixes of the enabled plugin PDPs. + // Any pluginPDPs available but not specified within config are disabled. + configuredPluginPDPs []plugin.PolicyDecisionPointConfig } func NewRegistration() *serviceregistry.Service[authzV2Connect.AuthorizationServiceHandler] { @@ -90,13 +95,31 @@ func NewRegistration() *serviceregistry.Service[authzV2Connect.AuthorizationServ panic(fmt.Errorf("failed to parse entitlement policy cache refresh interval [%s]: %w", authZCfg.Cache.RefreshInterval, err)) } - retriever := access.NewEntitlementPolicyRetriever(as.sdk) + retriever := policyStore.NewEntitlementPolicyRetriever(as.sdk) as.cache, err = NewEntitlementPolicyCache(context.Background(), l, retriever, cacheClient, refreshInterval) 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)) } + // If supportedregistered plugin PDPs have a name matching auth service config, + // mount the interface along with its config to the auth service struct. + for _, pluginPDP := range srp.RegisteredPluginPDPs { + for _, configuredPDP := range authZCfg.PluginPDPs { + l.Debug("plugin name", slog.String("name", pluginPDP.Name()), slog.String("configured name", configuredPDP.Name)) + if configuredPDP.Name == pluginPDP.Name() { + l.Debug("registering plugin PDP", + slog.String("name", pluginPDP.Name()), + ) + as.configuredPluginPDPs = append(as.configuredPluginPDPs, plugin.PolicyDecisionPointConfig{ + PolicyDecisionPointI: pluginPDP, + AttributePrefixes: configuredPDP.AttributePrefixes, + Name: configuredPDP.Name, + }) + } + } + } + // if err := srp.RegisterReadinessCheck("authorization", as.IsReady); err != nil { // logger.Error("failed to register authorization readiness check", slog.String("error", err.Error())) // } @@ -136,7 +159,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) + pdp, err := access.NewJustInTimeAuthorizer(ctx, as.logger, as.sdk, as.cache, nil) if err != nil { as.logger.ErrorContext(ctx, "failed to create JIT PDP", slog.Any("error", err)) return nil, connect.NewError(connect.CodeInternal, err) @@ -166,7 +189,7 @@ func (as *Service) GetDecision(ctx context.Context, req *connect.Request[authzV2 propagator := otel.GetTextMapPropagator() ctx = propagator.Extract(ctx, propagation.HeaderCarrier(req.Header())) - pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache) + pdp, err := access.NewJustInTimeAuthorizer(ctx, as.logger, as.sdk, as.cache, as.configuredPluginPDPs) if err != nil { as.logger.ErrorContext(ctx, "failed to create JIT PDP", slog.Any("error", err)) return nil, connect.NewError(connect.CodeInternal, err) @@ -204,7 +227,7 @@ func (as *Service) GetDecisionMultiResource(ctx context.Context, req *connect.Re propagator := otel.GetTextMapPropagator() ctx = propagator.Extract(ctx, propagation.HeaderCarrier(req.Header())) - pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache) + pdp, err := access.NewJustInTimeAuthorizer(ctx, as.logger, as.sdk, as.cache, as.configuredPluginPDPs) if err != nil { return nil, statusifyError(ctx, as.logger, errors.Join(errors.New("failed to create JIT PDP"), err)) } @@ -244,7 +267,7 @@ func (as *Service) GetDecisionBulk(ctx context.Context, req *connect.Request[aut propagator := otel.GetTextMapPropagator() ctx = propagator.Extract(ctx, propagation.HeaderCarrier(req.Header())) - pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache) + pdp, err := access.NewJustInTimeAuthorizer(ctx, as.logger, as.sdk, as.cache, as.configuredPluginPDPs) if err != nil { return nil, statusifyError(ctx, as.logger, errors.Join(errors.New("failed to create JIT PDP"), err)) } diff --git a/service/authorization/v2/cache.go b/service/authorization/v2/cache.go index 27baebffdb..4e3567b381 100644 --- a/service/authorization/v2/cache.go +++ b/service/authorization/v2/cache.go @@ -8,7 +8,7 @@ import ( "time" "github.com/opentdf/platform/protocol/go/policy" - "github.com/opentdf/platform/service/internal/access/v2" + policyStore "github.com/opentdf/platform/service/internal/access/v2/store" "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/pkg/cache" ) @@ -45,7 +45,7 @@ type EntitlementPolicyCache struct { cacheClient *cache.Cache // SDK-connected retriever to fetch fresh data from policy services - retriever *access.EntitlementPolicyRetriever + retriever *policyStore.EntitlementPolicyRetriever // Refresh state configuredRefreshInterval time.Duration @@ -69,7 +69,7 @@ type EntitlementPolicy struct { func NewEntitlementPolicyCache( ctx context.Context, l *logger.Logger, - retriever *access.EntitlementPolicyRetriever, + retriever *policyStore.EntitlementPolicyRetriever, cacheClient *cache.Cache, cacheRefreshInterval time.Duration, ) (*EntitlementPolicyCache, error) { diff --git a/service/authorization/v2/config.go b/service/authorization/v2/config.go index 51c2b7f601..5811f63c53 100644 --- a/service/authorization/v2/config.go +++ b/service/authorization/v2/config.go @@ -4,6 +4,8 @@ import ( "fmt" "log/slog" "time" + + "github.com/opentdf/platform/service/internal/access/v2/plugin" ) // Manage config for EntitlementPolicyCache: attributes, subject mappings, and registered resources @@ -14,7 +16,8 @@ type EntitlementPolicyCacheConfig struct { } type Config struct { - Cache EntitlementPolicyCacheConfig `mapstructure:"entitlement_policy_cache" json:"entitlement_policy_cache"` + Cache EntitlementPolicyCacheConfig `mapstructure:"entitlement_policy_cache" json:"entitlement_policy_cache"` + PluginPDPs []plugin.PolicyDecisionPointConfig `mapstructure:"plugin_pdps" json:"plugin_pdps"` } // Validate tests for a sensible configuration diff --git a/service/cmd/start.go b/service/cmd/start.go index 31d8c0e8d8..ac6a358d4a 100644 --- a/service/cmd/start.go +++ b/service/cmd/start.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/opentdf/platform/service/internal/access/v2/plugin" "github.com/opentdf/platform/service/pkg/server" "github.com/spf13/cobra" ) @@ -20,9 +21,12 @@ func start(cmd *cobra.Command, _ []string) error { configFile, _ := cmd.Flags().GetString(configFileFlag) configKey, _ := cmd.Flags().GetString(configKeyFlag) + pluginPDPs := []plugin.PolicyDecisionPoint{&plugin.GranularCustomPdp{}} + return server.Start( server.WithWaitForShutdownSignal(), server.WithConfigFile(configFile), server.WithConfigKey(configKey), + server.WithPluginPDPs(pluginPDPs...), ) } diff --git a/service/internal/access/v2/just_in_time_pdp.go b/service/internal/access/v2/just_in_time_authorizer.go similarity index 77% rename from service/internal/access/v2/just_in_time_pdp.go rename to service/internal/access/v2/just_in_time_authorizer.go index acfae78b0b..2472609bc2 100644 --- a/service/internal/access/v2/just_in_time_pdp.go +++ b/service/internal/access/v2/just_in_time_authorizer.go @@ -14,6 +14,9 @@ import ( "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/subjectmapping" otdfSDK "github.com/opentdf/platform/sdk" + "github.com/opentdf/platform/service/internal/access/v2/plugin" + policyStore "github.com/opentdf/platform/service/internal/access/v2/store" + "github.com/opentdf/platform/service/internal/subjectmappingbuiltin" "github.com/opentdf/platform/service/logger" ) @@ -23,21 +26,24 @@ var ( ErrInvalidEntityType = errors.New("access: invalid entity type") ) -type JustInTimePDP struct { +type JustInTimeAuthorizer struct { logger *logger.Logger sdk *otdfSDK.SDK // embedded PDP pdp *PolicyDecisionPoint + // plugin PDPs + pluginPDPs []plugin.PolicyDecisionPoint } -// JustInTimePDP creates a new Policy Decision Point instance with no in-memory policy and a remote connection +// JustInTimeAuthorizer creates a new Policy Decision Point instance with no in-memory policy and a remote connection // via authenticated SDK, then fetches all entitlement policy from provided store interface or policy services directly. -func NewJustInTimePDP( +func NewJustInTimeAuthorizer( ctx context.Context, l *logger.Logger, sdk *otdfSDK.SDK, - store EntitlementPolicyStore, -) (*JustInTimePDP, error) { + store policyStore.EntitlementPolicyStore, + pluginPDPs []plugin.PolicyDecisionPointConfig, +) (*JustInTimeAuthorizer, error) { var err error if sdk == nil { @@ -50,7 +56,7 @@ func NewJustInTimePDP( } } - p := &JustInTimePDP{ + p := &JustInTimeAuthorizer{ sdk: sdk, logger: l, } @@ -58,7 +64,7 @@ func NewJustInTimePDP( // If no store is provided, have EntitlementPolicyRetriever fetch from policy services if !store.IsEnabled() || !store.IsReady(ctx) { l.DebugContext(ctx, "no EntitlementPolicyStore provided or not yet ready, will retrieve directly from policy services") - store = NewEntitlementPolicyRetriever(sdk) + store = policyStore.NewEntitlementPolicyRetriever(sdk) } allAttributes, err := store.ListAllAttributes(ctx) @@ -79,6 +85,14 @@ func NewJustInTimePDP( return nil, fmt.Errorf("failed to create new policy decision point: %w", err) } p.pdp = pdp + + for _, pluginPDPConfig := range pluginPDPs { + err := pluginPDPConfig.PolicyDecisionPointI.New(ctx, l, store, pluginPDPConfig.AttributePrefixes) + if err != nil { + return nil, fmt.Errorf("failed to initialize plugin PDP: %w", err) + } + p.pluginPDPs = append(p.pluginPDPs, pluginPDPConfig.PolicyDecisionPointI) + } return p, nil } @@ -86,7 +100,7 @@ func NewJustInTimePDP( // It resolves the entity chain to get the entity representations and then calls the embedded PDP to get the decision. // The decision is returned as a slice of Decision objects, along with a global boolean indicating whether or not all // decisions are allowed. -func (p *JustInTimePDP) GetDecision( +func (p *JustInTimeAuthorizer) GetDecision( ctx context.Context, entityIdentifier *authzV2.EntityIdentifier, action *policy.Action, @@ -127,9 +141,46 @@ func (p *JustInTimePDP) GetDecision( var decisions []*Decision allPermitted := true for _, entityRep := range entityRepresentations { - d, err := p.pdp.GetDecision(ctx, entityRep, action, resources) - if err != nil { - return nil, false, fmt.Errorf("failed to get decision for entityRepresentation with original id [%s]: %w", entityRep.GetOriginalId(), err) + var d *Decision + for _, pluginPDP := range p.pluginPDPs { + // TODO: figure out API with multiple resources in one decision, but just strip off the first for POC + // TODO: remove duplicate entitlement resolution logic and refactor the regular PDP to take in entitlements instead of doing this work twice + if pluginPDP.IsValidDecisionableAction(action) && pluginPDP.IsValidDecisionableResource(resources[0]) { + // Filter all attributes down to only those that relevant to the entitlement decisioning of these specific resources + decisionableAttributes, err := getResourceDecisionableAttributes(ctx, p.pdp.logger, p.pdp.allRegisteredResourceValuesByFQN, p.pdp.allEntitleableAttributesByValueFQN /* action, */, resources) + if err != nil { + return nil, false, fmt.Errorf("error getting decisionable attributes: %w", err) + } + + // Resolve them to their entitled FQNs and the actions available on each + entitledFQNsToActions, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(decisionableAttributes, entityRep) + if err != nil { + return nil, false, fmt.Errorf("error evaluating subject mappings for entitlement: %w", err) + } + isAllowed, err := pluginPDP.GetDecision(ctx, entityRep, &entitledFQNsToActions, action, resources[0]) + if err != nil { + return nil, false, fmt.Errorf("error evaluating plugin PDP %s", pluginPDP.Name()) + } + d = &Decision{ + Access: isAllowed, + Results: []ResourceDecision{ + { + Passed: isAllowed, + ResourceID: resources[0].GetEphemeralId(), + ResourceName: resources[0].GetRegisteredResourceValueFqn(), + DataRuleResults: nil, + }, + }, + } + break + } + } + + if d == nil { + d, err = p.pdp.GetDecision(ctx, entityRep, action, resources) + if err != nil { + return nil, false, fmt.Errorf("failed to get decision for entityRepresentation with original id [%s]: %w", entityRep.GetOriginalId(), err) + } } if d == nil { return nil, false, fmt.Errorf("decision is nil: %w", err) @@ -146,7 +197,7 @@ func (p *JustInTimePDP) GetDecision( // GetEntitlements retrieves the entitlements for the provided entity chain. // It resolves the entity chain to get the entity representations and then calls the embedded PDP to get the entitlements. -func (p *JustInTimePDP) GetEntitlements( +func (p *JustInTimeAuthorizer) GetEntitlements( ctx context.Context, entityIdentifier *authzV2.EntityIdentifier, withComprehensiveHierarchy bool, @@ -198,7 +249,7 @@ func (p *JustInTimePDP) GetEntitlements( } // getMatchedSubjectMappings retrieves the subject mappings for the provided entity representations -func (p *JustInTimePDP) getMatchedSubjectMappings( +func (p *JustInTimeAuthorizer) getMatchedSubjectMappings( ctx context.Context, entityRepresentations []*entityresolutionV2.EntityRepresentation, // updated with the results, attrValue FQN to attribute and value with subject mappings @@ -236,7 +287,7 @@ func (p *JustInTimePDP) getMatchedSubjectMappings( // resolveEntitiesFromEntityChain roundtrips to ERS to resolve the provided entity chain // and optionally skips environment entities (which is expected behavior in decision flow) -func (p *JustInTimePDP) resolveEntitiesFromEntityChain( +func (p *JustInTimeAuthorizer) resolveEntitiesFromEntityChain( ctx context.Context, entityChain *entity.EntityChain, skipEnvironmentEntities bool, @@ -275,7 +326,7 @@ func (p *JustInTimePDP) resolveEntitiesFromEntityChain( // resolveEntitiesFromToken roundtrips to ERS to resolve the provided token // and optionally skips environment entities (which is expected behavior in decision flow) -func (p *JustInTimePDP) resolveEntitiesFromToken( +func (p *JustInTimeAuthorizer) resolveEntitiesFromToken( ctx context.Context, token *entity.Token, skipEnvironmentEntities bool, diff --git a/service/internal/access/v2/plugin/granular.go b/service/internal/access/v2/plugin/granular.go new file mode 100644 index 0000000000..31deff5d97 --- /dev/null +++ b/service/internal/access/v2/plugin/granular.go @@ -0,0 +1,118 @@ +package plugin + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net/mail" + "slices" + "strings" + + authzV2 "github.com/opentdf/platform/protocol/go/authorization/v2" + ersV2 "github.com/opentdf/platform/protocol/go/entityresolution/v2" + policy "github.com/opentdf/platform/protocol/go/policy" + policyStore "github.com/opentdf/platform/service/internal/access/v2/store" + "github.com/opentdf/platform/service/internal/subjectmappingbuiltin" + "github.com/opentdf/platform/service/logger" +) + +// Map of resources to ACL (mock database) +var mockACL = map[string][]string{ + "https://reg_res/granular/value/123": {"test@example.com", "test2@example.com"}, + "https://reg_res/granular/value/456": {"someone@gmail.com"}, +} +var allowedGranularActions = []string{"read", "send"} + +const ( + fieldEmailAddress = "email" + granularPluginPDPName = "granular-plugin-pdp" +) + +type GranularCustomPdp struct { + l *logger.Logger + resourceFQNPrefixes []string +} + +// Initializes a new GranularPDP +func (p *GranularCustomPdp) New(ctx context.Context, l *logger.Logger, _ policyStore.EntitlementPolicyStore, attributeFQNPrefixes []string) error { + p.resourceFQNPrefixes = attributeFQNPrefixes + p.l = l.With("component", granularPluginPDPName) + return nil +} + +func (p *GranularCustomPdp) Name() string { + return granularPluginPDPName +} + +// Granular plugin PDP is always ready +func (p *GranularCustomPdp) IsReady(_ context.Context) bool { + return true +} + +// Ensure the decision is one of the allowed decision and a valid resource, then check +// the email in the entity representation against our in-memory ACL +func (p *GranularCustomPdp) GetDecision( + ctx context.Context, + entityRepresentation *ersV2.EntityRepresentation, + _ *subjectmappingbuiltin.AttributeValueFQNsToActions, + action *policy.Action, + resource *authzV2.Resource, +) (bool, error) { + if !p.IsValidDecisionableResource(resource) { + return false, errors.New("resource is not decisionable") + } + if !p.IsValidDecisionableAction(action) { + return false, errors.New("action is not decisionable") + } + + var entityEmail string + for _, prop := range entityRepresentation.GetAdditionalProps() { + for field, value := range prop.GetFields() { + if field == fieldEmailAddress { + if e, err := mail.ParseAddress(value.GetStringValue()); err == nil { + entityEmail = e.Address + break + } + } + } + if entityEmail != "" { + break + } + } + + if entityEmail == "" { + return false, fmt.Errorf("no email found in entity representation") + } + + granularResourceFQN := resource.GetRegisteredResourceValueFqn() + for resourceName, acl := range mockACL { + if resourceName == granularResourceFQN { + if slices.Contains(acl, entityEmail) { + return true, nil + } + p.l.DebugContext(ctx, "access denied per the ACL", slog.String("email", entityEmail), slog.String("resource", resourceName)) + return false, errors.New("access denied") + } + } + + return false, nil +} + +// Ensures resource is a registered resource with an FQN matching configured prefix +func (p *GranularCustomPdp) IsValidDecisionableResource(resource *authzV2.Resource) bool { + switch resource.GetResource().(type) { + case *authzV2.Resource_RegisteredResourceValueFqn: + for _, prefix := range p.resourceFQNPrefixes { + if strings.HasPrefix(resource.GetRegisteredResourceValueFqn(), prefix) { + return true + } + } + } + return false +} + +// Check our allowed actions +func (p *GranularCustomPdp) IsValidDecisionableAction(action *policy.Action) bool { + return slices.Contains(allowedGranularActions, strings.ToLower(action.GetName())) +} diff --git a/service/internal/access/v2/plugin/plugin.go b/service/internal/access/v2/plugin/plugin.go new file mode 100644 index 0000000000..f0e0cd0428 --- /dev/null +++ b/service/internal/access/v2/plugin/plugin.go @@ -0,0 +1,40 @@ +package plugin + +import ( + "context" + + authzV2 "github.com/opentdf/platform/protocol/go/authorization/v2" + ersV2 "github.com/opentdf/platform/protocol/go/entityresolution/v2" + policy "github.com/opentdf/platform/protocol/go/policy" + policyStore "github.com/opentdf/platform/service/internal/access/v2/store" + "github.com/opentdf/platform/service/internal/subjectmappingbuiltin" + "github.com/opentdf/platform/service/logger" +) + +type PolicyDecisionPoint interface { + // Initialize a plugin PDP with a dedicated logger and access to policy + New(ctx context.Context, l *logger.Logger, store policyStore.EntitlementPolicyStore, attributeFQNPrefixes []string) error + // Make a decision based on an entity representation, platform policy entitlements, a requested action, and a relevant resource + GetDecision(ctx context.Context, entityRepresentation *ersV2.EntityRepresentation, entitlements *subjectmappingbuiltin.AttributeValueFQNsToActions, action *policy.Action, resource *authzV2.Resource) (bool, error) + // Determine if a given resource is able to be decisioned upon by this PDP implementation + IsValidDecisionableResource(resource *authzV2.Resource) bool + // Determine if a given action is able to be decisioned upon by this PDP implementation + IsValidDecisionableAction(action *policy.Action) bool + // Check any dependencies or initialization state for readiness + IsReady(ctx context.Context) bool + // Provide the name of the plugin PDP implementation + Name() string +} + +type PolicyDecisionPointConfig struct { + PolicyDecisionPointI PolicyDecisionPoint + AttributePrefixes []string `mapstructure:"resource_fqn_prefixes" json:"resource_fqn_prefixes"` + Name string `mapstructure:"name" json:"name"` +} + +// TODO: refactor so we have O(1) lookups by FQN instead of unprocessed lists with O(n) lookup +// type policyStore interface { +// AttributeAndValuesByValueFQN() +// RegisteredResourceValuesByFQN() +// // ObligationValuesByFQN() +// } diff --git a/service/internal/access/v2/policy_store.go b/service/internal/access/v2/store/policy_store.go similarity index 99% rename from service/internal/access/v2/policy_store.go rename to service/internal/access/v2/store/policy_store.go index 81181110d2..f906f3a2f2 100644 --- a/service/internal/access/v2/policy_store.go +++ b/service/internal/access/v2/store/policy_store.go @@ -1,4 +1,4 @@ -package access +package store import ( "context" diff --git a/service/pkg/server/options.go b/service/pkg/server/options.go index 5d40582126..579113c5bd 100644 --- a/service/pkg/server/options.go +++ b/service/pkg/server/options.go @@ -2,6 +2,7 @@ package server import ( "github.com/casbin/casbin/v2/persist" + "github.com/opentdf/platform/service/internal/access/v2/plugin" "github.com/opentdf/platform/service/pkg/config" "github.com/opentdf/platform/service/pkg/serviceregistry" "github.com/opentdf/platform/service/trust" @@ -23,6 +24,7 @@ type StartConfig struct { configLoaderOrder []string trustKeyManagers []trust.NamedKeyManagerFactory + pluginPDPs []plugin.PolicyDecisionPoint } // Deprecated: Use WithConfigKey @@ -143,3 +145,11 @@ func WithTrustKeyManagerFactories(factories ...trust.NamedKeyManagerFactory) Sta return c } } + +// WithPluginPDPs option allows invocation of plugin Policy Decision Points during an Access Decision. +func WithPluginPDPs(plugins ...plugin.PolicyDecisionPoint) StartOptions { + return func(c StartConfig) StartConfig { + c.pluginPDPs = append(c.pluginPDPs, plugins...) + return c + } +} diff --git a/service/pkg/server/services.go b/service/pkg/server/services.go index 1cadda5c1b..cf589cdc1a 100644 --- a/service/pkg/server/services.go +++ b/service/pkg/server/services.go @@ -15,6 +15,7 @@ import ( "github.com/opentdf/platform/service/entityresolution" entityresolutionV2 "github.com/opentdf/platform/service/entityresolution/v2" "github.com/opentdf/platform/service/health" + "github.com/opentdf/platform/service/internal/access/v2/plugin" "github.com/opentdf/platform/service/internal/server" "github.com/opentdf/platform/service/kas" logging "github.com/opentdf/platform/service/logger" @@ -119,13 +120,14 @@ func registerCoreServices(reg *serviceregistry.Registry, mode []string) ([]strin } type startServicesParams struct { - cfg *config.Config - otdf *server.OpenTDFServer - client *sdk.SDK - logger *logging.Logger - reg *serviceregistry.Registry - cacheManager *cache.Manager - keyManagerFactories []trust.NamedKeyManagerFactory + cfg *config.Config + otdf *server.OpenTDFServer + client *sdk.SDK + logger *logging.Logger + reg *serviceregistry.Registry + cacheManager *cache.Manager + keyManagerFactories []trust.NamedKeyManagerFactory + registeredPluginPDPs []plugin.PolicyDecisionPoint } // startServices iterates through the registered namespaces and starts the services @@ -142,6 +144,7 @@ func startServices(ctx context.Context, params startServicesParams) (func(), err reg := params.reg cacheManager := params.cacheManager keyManagerFactories := params.keyManagerFactories + pluginPDPs := params.registeredPluginPDPs for _, ns := range reg.GetNamespaces() { namespace, err := reg.GetNamespace(ns) @@ -225,6 +228,7 @@ func startServices(ctx context.Context, params startServicesParams) (func(), err Tracer: tracer, NewCacheClient: createCacheClient, KeyManagerFactories: keyManagerFactories, + RegisteredPluginPDPs: pluginPDPs, }) if err != nil { return func() {}, err diff --git a/service/pkg/serviceregistry/serviceregistry.go b/service/pkg/serviceregistry/serviceregistry.go index acbf95bb0a..0f77fab114 100644 --- a/service/pkg/serviceregistry/serviceregistry.go +++ b/service/pkg/serviceregistry/serviceregistry.go @@ -15,6 +15,7 @@ import ( "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" + "github.com/opentdf/platform/service/internal/access/v2/plugin" "github.com/opentdf/platform/service/internal/server" "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/pkg/cache" @@ -47,7 +48,8 @@ type RegistrationParams struct { // NewCacheClient is a function that can be used to create a new cache instance for the service NewCacheClient func(cache.Options) (*cache.Cache, error) - KeyManagerFactories []trust.NamedKeyManagerFactory + KeyManagerFactories []trust.NamedKeyManagerFactory + RegisteredPluginPDPs []plugin.PolicyDecisionPoint ////// The following functions are optional and intended to be called by the service ////// From fe46c968b344b47f275f09f1d3bdeb013491b449 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Thu, 11 Sep 2025 14:54:19 -0700 Subject: [PATCH 2/9] fixes --- service/authorization/v2/authorization.go | 36 +++++++++++------------ service/authorization/v2/config.go | 1 + service/pkg/server/start.go | 15 +++++----- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/service/authorization/v2/authorization.go b/service/authorization/v2/authorization.go index a8fe10d905..7e78dfd342 100644 --- a/service/authorization/v2/authorization.go +++ b/service/authorization/v2/authorization.go @@ -78,6 +78,24 @@ func NewRegistration() *serviceregistry.Service[authzV2Connect.AuthorizationServ } l.Debug("authorization service config", slog.Any("config", authZCfg.LogValue())) + // If supportedregistered plugin PDPs have a name matching auth service config, + // mount the interface along with its config to the auth service struct. + for _, pluginPDP := range srp.RegisteredPluginPDPs { + for _, configuredPDP := range authZCfg.PluginPDPs { + l.Debug("plugin name", slog.String("name", pluginPDP.Name()), slog.String("configured name", configuredPDP.Name)) + if configuredPDP.Name == pluginPDP.Name() { + l.Debug("registering plugin PDP", + slog.String("name", pluginPDP.Name()), + ) + as.configuredPluginPDPs = append(as.configuredPluginPDPs, plugin.PolicyDecisionPointConfig{ + PolicyDecisionPointI: pluginPDP, + AttributePrefixes: configuredPDP.AttributePrefixes, + Name: configuredPDP.Name, + }) + } + } + } + if !authZCfg.Cache.Enabled { l.Debug("entitlement policy cache is disabled") return as, nil @@ -102,24 +120,6 @@ func NewRegistration() *serviceregistry.Service[authzV2Connect.AuthorizationServ panic(fmt.Errorf("failed to create entitlement policy cache: %w", err)) } - // If supportedregistered plugin PDPs have a name matching auth service config, - // mount the interface along with its config to the auth service struct. - for _, pluginPDP := range srp.RegisteredPluginPDPs { - for _, configuredPDP := range authZCfg.PluginPDPs { - l.Debug("plugin name", slog.String("name", pluginPDP.Name()), slog.String("configured name", configuredPDP.Name)) - if configuredPDP.Name == pluginPDP.Name() { - l.Debug("registering plugin PDP", - slog.String("name", pluginPDP.Name()), - ) - as.configuredPluginPDPs = append(as.configuredPluginPDPs, plugin.PolicyDecisionPointConfig{ - PolicyDecisionPointI: pluginPDP, - AttributePrefixes: configuredPDP.AttributePrefixes, - Name: configuredPDP.Name, - }) - } - } - } - // if err := srp.RegisterReadinessCheck("authorization", as.IsReady); err != nil { // logger.Error("failed to register authorization readiness check", slog.String("error", err.Error())) // } diff --git a/service/authorization/v2/config.go b/service/authorization/v2/config.go index 5811f63c53..43048d0e83 100644 --- a/service/authorization/v2/config.go +++ b/service/authorization/v2/config.go @@ -54,5 +54,6 @@ func (c *Config) LogValue() slog.Value { slog.String("refresh_interval", c.Cache.RefreshInterval), ), ), + slog.Any("plugin_pdps", c.PluginPDPs), ) } diff --git a/service/pkg/server/start.go b/service/pkg/server/start.go index 9aae99bd16..790bc2792c 100644 --- a/service/pkg/server/start.go +++ b/service/pkg/server/start.go @@ -338,13 +338,14 @@ func Start(f ...StartOptions) error { logger.Info("starting services") gatewayCleanup, err := startServices(ctx, startServicesParams{ - cfg: cfg, - otdf: otdf, - client: client, - keyManagerFactories: startConfig.trustKeyManagers, - logger: logger, - reg: svcRegistry, - cacheManager: cacheManager, + cfg: cfg, + otdf: otdf, + client: client, + keyManagerFactories: startConfig.trustKeyManagers, + logger: logger, + reg: svcRegistry, + cacheManager: cacheManager, + registeredPluginPDPs: startConfig.pluginPDPs, }) if err != nil { logger.Error("issue starting services", slog.String("error", err.Error())) From 1e1430193b7936a985a16ea33f7c9bca033c2e83 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Thu, 11 Sep 2025 14:55:41 -0700 Subject: [PATCH 3/9] add config for granular PDP --- opentdf-dev.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/opentdf-dev.yaml b/opentdf-dev.yaml index a0a565af62..7afdef7e1d 100644 --- a/opentdf-dev.yaml +++ b/opentdf-dev.yaml @@ -51,10 +51,11 @@ services: # enabled: true # list_request_limit_default: 1000 # list_request_limit_max: 2500 - # authorization: - # entitlement_policy_cache: - # enabled: false - # refresh_interval: 30s + authorization: + plugin_pdps: + - name: 'granular-plugin-pdp' + resource_fqn_prefixes: + - 'https://reg_res/granular' server: public_hostname: localhost tls: From ccebb69d38026b319534ec444ac5fbe47dfb54f1 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Fri, 12 Sep 2025 07:34:18 -0700 Subject: [PATCH 4/9] externalize access pdp v2 --- service/authorization/v2/authorization.go | 6 +- .../authorization/v2/authorization_test.go | 2 +- service/authorization/v2/cache.go | 2 +- service/authorization/v2/config.go | 2 +- service/authorization/v2/helpers.go | 2 +- service/cmd/start.go | 2 +- service/logger/audit/getDecision.go | 6 +- .../access/v2 => pkg/access}/evaluate.go | 14 +-- .../access/v2 => pkg/access}/evaluate_test.go | 98 +++++++++---------- .../access/v2 => pkg/access}/helpers.go | 0 .../access/v2 => pkg/access}/helpers_test.go | 0 .../access}/just_in_time_authorizer.go | 8 +- .../{internal/access/v2 => pkg/access}/pdp.go | 6 +- .../access/v2 => pkg/access}/pdp_test.go | 0 .../v2 => pkg/access}/plugin/granular.go | 6 +- .../access/v2 => pkg/access}/plugin/plugin.go | 6 +- .../v2 => pkg/access}/store/policy_store.go | 0 .../resolution_actions.go} | 5 +- .../resolution_actions_test.go} | 68 ++++++------- .../access/v2 => pkg/access}/validators.go | 4 +- .../v2 => pkg/access}/validators_test.go | 0 service/pkg/server/options.go | 2 +- service/pkg/server/services.go | 2 +- .../pkg/serviceregistry/serviceregistry.go | 2 +- 24 files changed, 123 insertions(+), 120 deletions(-) rename service/{internal/access/v2 => pkg/access}/evaluate.go (96%) rename service/{internal/access/v2 => pkg/access}/evaluate_test.go (87%) rename service/{internal/access/v2 => pkg/access}/helpers.go (100%) rename service/{internal/access/v2 => pkg/access}/helpers_test.go (100%) rename service/{internal/access/v2 => pkg/access}/just_in_time_authorizer.go (97%) rename service/{internal/access/v2 => pkg/access}/pdp.go (97%) rename service/{internal/access/v2 => pkg/access}/pdp_test.go (100%) rename service/{internal/access/v2 => pkg/access}/plugin/granular.go (93%) rename service/{internal/access/v2 => pkg/access}/plugin/plugin.go (83%) rename service/{internal/access/v2 => pkg/access}/store/policy_store.go (100%) rename service/{internal/subjectmappingbuiltin/subject_mapping_builtin_actions.go => pkg/access/subject-mapping-resolution/resolution_actions.go} (93%) rename service/{internal/subjectmappingbuiltin/subject_mapping_builtin_actions_test.go => pkg/access/subject-mapping-resolution/resolution_actions_test.go} (87%) rename service/{internal/access/v2 => pkg/access}/validators.go (97%) rename service/{internal/access/v2 => pkg/access}/validators_test.go (100%) diff --git a/service/authorization/v2/authorization.go b/service/authorization/v2/authorization.go index 7e78dfd342..a21ccf1f72 100644 --- a/service/authorization/v2/authorization.go +++ b/service/authorization/v2/authorization.go @@ -13,10 +13,10 @@ import ( authzV2 "github.com/opentdf/platform/protocol/go/authorization/v2" authzV2Connect "github.com/opentdf/platform/protocol/go/authorization/v2/authorizationv2connect" otdf "github.com/opentdf/platform/sdk" - "github.com/opentdf/platform/service/internal/access/v2" - "github.com/opentdf/platform/service/internal/access/v2/plugin" - policyStore "github.com/opentdf/platform/service/internal/access/v2/store" "github.com/opentdf/platform/service/logger" + "github.com/opentdf/platform/service/pkg/access" + "github.com/opentdf/platform/service/pkg/access/plugin" + policyStore "github.com/opentdf/platform/service/pkg/access/store" "github.com/opentdf/platform/service/pkg/cache" "github.com/opentdf/platform/service/pkg/serviceregistry" "go.opentelemetry.io/otel" diff --git a/service/authorization/v2/authorization_test.go b/service/authorization/v2/authorization_test.go index abc8245524..ae6049f8e9 100644 --- a/service/authorization/v2/authorization_test.go +++ b/service/authorization/v2/authorization_test.go @@ -10,7 +10,7 @@ import ( authzV2 "github.com/opentdf/platform/protocol/go/authorization/v2" "github.com/opentdf/platform/protocol/go/entity" "github.com/opentdf/platform/protocol/go/policy" - access "github.com/opentdf/platform/service/internal/access/v2" + access "github.com/opentdf/platform/service/pkg/access" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" diff --git a/service/authorization/v2/cache.go b/service/authorization/v2/cache.go index 4e3567b381..2ce188b645 100644 --- a/service/authorization/v2/cache.go +++ b/service/authorization/v2/cache.go @@ -8,8 +8,8 @@ import ( "time" "github.com/opentdf/platform/protocol/go/policy" - policyStore "github.com/opentdf/platform/service/internal/access/v2/store" "github.com/opentdf/platform/service/logger" + policyStore "github.com/opentdf/platform/service/pkg/access/store" "github.com/opentdf/platform/service/pkg/cache" ) diff --git a/service/authorization/v2/config.go b/service/authorization/v2/config.go index 43048d0e83..fd650bc1c2 100644 --- a/service/authorization/v2/config.go +++ b/service/authorization/v2/config.go @@ -5,7 +5,7 @@ import ( "log/slog" "time" - "github.com/opentdf/platform/service/internal/access/v2/plugin" + "github.com/opentdf/platform/service/pkg/access/plugin" ) // Manage config for EntitlementPolicyCache: attributes, subject mappings, and registered resources diff --git a/service/authorization/v2/helpers.go b/service/authorization/v2/helpers.go index 0565a63e34..b9cac6bbfb 100644 --- a/service/authorization/v2/helpers.go +++ b/service/authorization/v2/helpers.go @@ -7,8 +7,8 @@ import ( "connectrpc.com/connect" authzV2 "github.com/opentdf/platform/protocol/go/authorization/v2" - "github.com/opentdf/platform/service/internal/access/v2" "github.com/opentdf/platform/service/logger" + "github.com/opentdf/platform/service/pkg/access" ) // rollupMultiResourceDecisions creates a standardized response for multi-resource decisions diff --git a/service/cmd/start.go b/service/cmd/start.go index ac6a358d4a..813dee47e6 100644 --- a/service/cmd/start.go +++ b/service/cmd/start.go @@ -1,7 +1,7 @@ package cmd import ( - "github.com/opentdf/platform/service/internal/access/v2/plugin" + "github.com/opentdf/platform/service/pkg/access/plugin" "github.com/opentdf/platform/service/pkg/server" "github.com/spf13/cobra" ) diff --git a/service/logger/audit/getDecision.go b/service/logger/audit/getDecision.go index 36a29b4dab..f1d624e3c7 100644 --- a/service/logger/audit/getDecision.go +++ b/service/logger/audit/getDecision.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "github.com/opentdf/platform/service/internal/subjectmappingbuiltin" + subjectmappingresolution "github.com/opentdf/platform/service/pkg/access/subject-mapping-resolution" ) type DecisionResult int @@ -47,7 +47,7 @@ type GetDecisionV2EventParams struct { EntityID string ActionName string Decision DecisionResult - Entitlements subjectmappingbuiltin.AttributeValueFQNsToActions + Entitlements subjectmappingresolution.AttributeValueFQNsToActions // Allow ResourceDecisions to be typed by the caller as structure is in-flight ResourceDecisions interface{} } @@ -99,7 +99,7 @@ func CreateV2GetDecisionEvent(ctx context.Context, params GetDecisionV2EventPara actorAttributes := []interface{}{ struct { - Entitlements subjectmappingbuiltin.AttributeValueFQNsToActions `json:"entitlements"` + Entitlements subjectmappingresolution.AttributeValueFQNsToActions `json:"entitlements"` }{ Entitlements: params.Entitlements, }, diff --git a/service/internal/access/v2/evaluate.go b/service/pkg/access/evaluate.go similarity index 96% rename from service/internal/access/v2/evaluate.go rename to service/pkg/access/evaluate.go index 50067158b8..a0ebcd85f0 100644 --- a/service/internal/access/v2/evaluate.go +++ b/service/pkg/access/evaluate.go @@ -11,8 +11,8 @@ import ( authz "github.com/opentdf/platform/protocol/go/authorization/v2" "github.com/opentdf/platform/protocol/go/policy" attrs "github.com/opentdf/platform/protocol/go/policy/attributes" - "github.com/opentdf/platform/service/internal/subjectmappingbuiltin" "github.com/opentdf/platform/service/logger" + subjectmappingresolution "github.com/opentdf/platform/service/pkg/access/subject-mapping-resolution" ) var ( @@ -31,7 +31,7 @@ func getResourceDecision( l *logger.Logger, accessibleAttributeValues map[string]*attrs.GetAttributeValuesByFqnsResponse_AttributeAndValue, accessibleRegisteredResourceValues map[string]*policy.RegisteredResourceValue, - entitlements subjectmappingbuiltin.AttributeValueFQNsToActions, + entitlements subjectmappingresolution.AttributeValueFQNsToActions, action *policy.Action, resource *authz.Resource, ) (*ResourceDecision, error) { @@ -111,7 +111,7 @@ func evaluateResourceAttributeValues( resourceID string, resourceName string, action *policy.Action, - entitlements subjectmappingbuiltin.AttributeValueFQNsToActions, + entitlements subjectmappingresolution.AttributeValueFQNsToActions, accessibleAttributeValues map[string]*attrs.GetAttributeValuesByFqnsResponse_AttributeAndValue, ) (*ResourceDecision, error) { // Group value FQNs by parent definition @@ -164,7 +164,7 @@ func evaluateResourceAttributeValues( func evaluateDefinition( ctx context.Context, l *logger.Logger, - entitlements subjectmappingbuiltin.AttributeValueFQNsToActions, + entitlements subjectmappingresolution.AttributeValueFQNsToActions, action *policy.Action, resourceValueFQNs []string, attrDefinition *policy.Attribute, @@ -220,7 +220,7 @@ func evaluateDefinition( func allOfRule( _ context.Context, _ *logger.Logger, - entitlements subjectmappingbuiltin.AttributeValueFQNsToActions, + entitlements subjectmappingresolution.AttributeValueFQNsToActions, action *policy.Action, resourceValueFQNs []string, ) []EntitlementFailure { @@ -260,7 +260,7 @@ func allOfRule( func anyOfRule( _ context.Context, _ *logger.Logger, - entitlements subjectmappingbuiltin.AttributeValueFQNsToActions, + entitlements subjectmappingresolution.AttributeValueFQNsToActions, action *policy.Action, resourceValueFQNs []string, ) []EntitlementFailure { @@ -311,7 +311,7 @@ func anyOfRule( func hierarchyRule( ctx context.Context, l *logger.Logger, - entitlements subjectmappingbuiltin.AttributeValueFQNsToActions, + entitlements subjectmappingresolution.AttributeValueFQNsToActions, action *policy.Action, resourceValueFQNs []string, attrDefinition *policy.Attribute, diff --git a/service/internal/access/v2/evaluate_test.go b/service/pkg/access/evaluate_test.go similarity index 87% rename from service/internal/access/v2/evaluate_test.go rename to service/pkg/access/evaluate_test.go index 791502db23..4f8de0b7bf 100644 --- a/service/internal/access/v2/evaluate_test.go +++ b/service/pkg/access/evaluate_test.go @@ -9,8 +9,8 @@ import ( authz "github.com/opentdf/platform/protocol/go/authorization/v2" "github.com/opentdf/platform/protocol/go/policy" attrs "github.com/opentdf/platform/protocol/go/policy/attributes" - "github.com/opentdf/platform/service/internal/subjectmappingbuiltin" "github.com/opentdf/platform/service/logger" + subjectmappingresolution "github.com/opentdf/platform/service/pkg/access/subject-mapping-resolution" "github.com/opentdf/platform/service/policy/actions" ) @@ -217,7 +217,7 @@ func (s *EvaluateTestSuite) TestAllOfRule() { tests := []struct { name string resourceValueFQNs []string - entitlements subjectmappingbuiltin.AttributeValueFQNsToActions + entitlements subjectmappingresolution.AttributeValueFQNsToActions expectedFailures int }{ { @@ -226,7 +226,7 @@ func (s *EvaluateTestSuite) TestAllOfRule() { projectAvengersFQN, projectJusticeLeagueFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ projectAvengersFQN: []*policy.Action{actionRead}, projectJusticeLeagueFQN: []*policy.Action{actionRead}, }, @@ -238,7 +238,7 @@ func (s *EvaluateTestSuite) TestAllOfRule() { projectJusticeLeagueFQN, projectFantasicFourFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ projectJusticeLeagueFQN: []*policy.Action{actionRead}, projectFantasicFourFQN: []*policy.Action{actionCreate}, // Wrong action }, @@ -250,7 +250,7 @@ func (s *EvaluateTestSuite) TestAllOfRule() { projectXmenFQN, projectJusticeLeagueFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ projectXmenFQN: []*policy.Action{actionCreate}, // Wrong action projectJusticeLeagueFQN: []*policy.Action{actionCreate}, // Wrong action }, @@ -262,7 +262,7 @@ func (s *EvaluateTestSuite) TestAllOfRule() { projectAvengersFQN, projectFantasicFourFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ projectAvengersFQN: []*policy.Action{actionRead}, // Missing levelLowerMidFQN entirely }, @@ -275,7 +275,7 @@ func (s *EvaluateTestSuite) TestAllOfRule() { projectJusticeLeagueFQN, projectXmenFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ projectAvengersFQN: []*policy.Action{actionRead, actionCreate}, projectJusticeLeagueFQN: []*policy.Action{actionRead}, projectXmenFQN: []*policy.Action{actionRead, actionCreate}, @@ -285,7 +285,7 @@ func (s *EvaluateTestSuite) TestAllOfRule() { { name: "empty resource list", resourceValueFQNs: []string{}, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ projectAvengersFQN: []*policy.Action{actionRead}, projectJusticeLeagueFQN: []*policy.Action{actionRead}, }, @@ -297,7 +297,7 @@ func (s *EvaluateTestSuite) TestAllOfRule() { projectAvengersFQN, projectJusticeLeagueFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{}, + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{}, expectedFailures: 2, // All resources should fail }, } @@ -344,7 +344,7 @@ func (s *EvaluateTestSuite) TestAnyOfRule() { tests := []struct { name string resourceValueFQNs []string - entitlements subjectmappingbuiltin.AttributeValueFQNsToActions + entitlements subjectmappingresolution.AttributeValueFQNsToActions expectedFailCount int }{ { @@ -353,7 +353,7 @@ func (s *EvaluateTestSuite) TestAnyOfRule() { deptFinanceFQN, deptMarketingFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ deptFinanceFQN: []*policy.Action{actionRead}, deptMarketingFQN: []*policy.Action{actionRead}, }, @@ -365,7 +365,7 @@ func (s *EvaluateTestSuite) TestAnyOfRule() { deptFinanceFQN, deptMarketingFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ deptFinanceFQN: []*policy.Action{actionRead}, deptMarketingFQN: []*policy.Action{actionCreate}, // Wrong action }, @@ -377,7 +377,7 @@ func (s *EvaluateTestSuite) TestAnyOfRule() { deptFinanceFQN, deptMarketingFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ deptFinanceFQN: []*policy.Action{actionCreate}, // Wrong action deptMarketingFQN: []*policy.Action{actionCreate}, // Wrong action }, @@ -389,7 +389,7 @@ func (s *EvaluateTestSuite) TestAnyOfRule() { deptFinanceFQN, deptMarketingFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ deptLegalFQN: []*policy.Action{actionRead}, // Wrong FQN }, expectedFailCount: 2, // Both failed so rule fails @@ -400,7 +400,7 @@ func (s *EvaluateTestSuite) TestAnyOfRule() { deptFinanceFQN, deptMarketingFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ deptFinanceFQN: []*policy.Action{actionCreate, actionRead}, // Has multiple actions including the required one }, expectedFailCount: 0, // Should pass as at least one FQN has the required action @@ -410,7 +410,7 @@ func (s *EvaluateTestSuite) TestAnyOfRule() { resourceValueFQNs: []string{ deptFinanceFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ deptFinanceFQN: []*policy.Action{actionRead}, }, expectedFailCount: 0, @@ -418,7 +418,7 @@ func (s *EvaluateTestSuite) TestAnyOfRule() { { name: "empty resource list", resourceValueFQNs: []string{}, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ deptFinanceFQN: []*policy.Action{actionRead}, deptMarketingFQN: []*policy.Action{actionRead}, }, @@ -430,7 +430,7 @@ func (s *EvaluateTestSuite) TestAnyOfRule() { deptFinanceFQN, deptMarketingFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{}, + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{}, expectedFailCount: 2, // Should fail as there are no entitlements }, } @@ -480,7 +480,7 @@ func (s *EvaluateTestSuite) TestHierarchyRule() { tests := []struct { name string resourceValueFQNs []string - entitlements subjectmappingbuiltin.AttributeValueFQNsToActions + entitlements subjectmappingresolution.AttributeValueFQNsToActions expectedFailures bool }{ { @@ -489,7 +489,7 @@ func (s *EvaluateTestSuite) TestHierarchyRule() { levelUpperMidFQN, levelMidFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelUpperMidFQN: []*policy.Action{actionRead}, // Entitled to highest value }, expectedFailures: false, @@ -499,7 +499,7 @@ func (s *EvaluateTestSuite) TestHierarchyRule() { resourceValueFQNs: []string{ levelLowerMidFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelHighestFQN: []*policy.Action{actionRead}, // Entitled to highest value }, expectedFailures: false, @@ -509,7 +509,7 @@ func (s *EvaluateTestSuite) TestHierarchyRule() { resourceValueFQNs: []string{ levelLowerMidFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelUpperMidFQN: []*policy.Action{actionRead}, // Entitled to higher value }, expectedFailures: false, @@ -519,7 +519,7 @@ func (s *EvaluateTestSuite) TestHierarchyRule() { resourceValueFQNs: []string{ levelLowestFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelUpperMidFQN: []*policy.Action{actionRead}, // higher levelMidFQN: []*policy.Action{actionRead}, // higher }, @@ -530,7 +530,7 @@ func (s *EvaluateTestSuite) TestHierarchyRule() { resourceValueFQNs: []string{ levelLowerMidFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelLowestFQN: []*policy.Action{actionRead}, // lower levelUpperMidFQN: []*policy.Action{actionRead}, // higher }, @@ -542,7 +542,7 @@ func (s *EvaluateTestSuite) TestHierarchyRule() { levelUpperMidFQN, levelMidFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelMidFQN: []*policy.Action{actionRead}, // Only entitled to lower value }, expectedFailures: true, @@ -553,7 +553,7 @@ func (s *EvaluateTestSuite) TestHierarchyRule() { levelUpperMidFQN, levelMidFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelUpperMidFQN: []*policy.Action{actionCreate}, // Wrong action }, expectedFailures: true, @@ -565,7 +565,7 @@ func (s *EvaluateTestSuite) TestHierarchyRule() { levelHighestFQN, // This is highest levelLowerMidFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelHighestFQN: []*policy.Action{actionRead}, }, expectedFailures: false, @@ -575,7 +575,7 @@ func (s *EvaluateTestSuite) TestHierarchyRule() { resourceValueFQNs: []string{ levelLowestFQN, // Lowest in hierarchy (index 4) }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelHighestFQN: []*policy.Action{actionRead}, // Highest in hierarchy (index 0) }, expectedFailures: false, // Should pass with the fix @@ -586,7 +586,7 @@ func (s *EvaluateTestSuite) TestHierarchyRule() { levelLowerMidFQN, // Lower in hierarchy (index 3) levelLowestFQN, // Lowest in hierarchy (index 4) }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ // No entitlement for exact matches levelHighestFQN: []*policy.Action{actionRead}, // Much higher in hierarchy (index 0) levelUpperMidFQN: []*policy.Action{actionRead}, // Higher in hierarchy (index 1) @@ -599,7 +599,7 @@ func (s *EvaluateTestSuite) TestHierarchyRule() { levelMidFQN, // Middle in hierarchy (index 2) levelLowerMidFQN, // Lower in hierarchy (index 3) }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelUpperMidFQN: []*policy.Action{actionCreate}, // Higher but wrong action levelHighestFQN: []*policy.Action{actionCreate}, // Highest but wrong action }, @@ -608,7 +608,7 @@ func (s *EvaluateTestSuite) TestHierarchyRule() { { name: "empty resource list", resourceValueFQNs: []string{}, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelUpperMidFQN: []*policy.Action{actionRead}, }, expectedFailures: false, // No resources to check, should pass @@ -636,7 +636,7 @@ func (s *EvaluateTestSuite) TestEvaluateDefinition() { name string definition *policy.Attribute resourceValues []string - entitlements subjectmappingbuiltin.AttributeValueFQNsToActions + entitlements subjectmappingresolution.AttributeValueFQNsToActions expectPass bool expectError bool }{ @@ -647,7 +647,7 @@ func (s *EvaluateTestSuite) TestEvaluateDefinition() { levelMidFQN, levelLowerMidFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelMidFQN: []*policy.Action{actionRead}, levelLowerMidFQN: []*policy.Action{actionRead}, }, @@ -661,7 +661,7 @@ func (s *EvaluateTestSuite) TestEvaluateDefinition() { deptFinanceFQN, deptMarketingFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ deptFinanceFQN: []*policy.Action{actionRead}, }, expectPass: true, @@ -674,7 +674,7 @@ func (s *EvaluateTestSuite) TestEvaluateDefinition() { levelUpperMidFQN, levelMidFQN, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelUpperMidFQN: []*policy.Action{actionRead}, }, expectPass: true, @@ -690,7 +690,7 @@ func (s *EvaluateTestSuite) TestEvaluateDefinition() { }, }, resourceValues: []string{levelMidFQN}, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{}, + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{}, expectPass: false, expectError: true, }, @@ -716,7 +716,7 @@ func (s *EvaluateTestSuite) TestEvaluateResourceAttributeValues() { tests := []struct { name string resourceAttrs *authz.Resource_AttributeValues - entitlements subjectmappingbuiltin.AttributeValueFQNsToActions + entitlements subjectmappingresolution.AttributeValueFQNsToActions expectAccessible bool expectError bool }{ @@ -728,7 +728,7 @@ func (s *EvaluateTestSuite) TestEvaluateResourceAttributeValues() { deptFinanceFQN, }, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelMidFQN: []*policy.Action{actionRead}, deptFinanceFQN: []*policy.Action{actionRead}, }, @@ -743,7 +743,7 @@ func (s *EvaluateTestSuite) TestEvaluateResourceAttributeValues() { deptFinanceFQN, }, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelMidFQN: []*policy.Action{actionRead}, deptFinanceFQN: []*policy.Action{actionCreate}, // Wrong action }, @@ -758,7 +758,7 @@ func (s *EvaluateTestSuite) TestEvaluateResourceAttributeValues() { "https://namespace.com/attr/department/value/unknown", // This FQN doesn't exist in accessibleAttributeValues }, }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelMidFQN: []*policy.Action{actionRead}, }, expectAccessible: false, @@ -807,7 +807,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { tests := []struct { name string resource *authz.Resource - entitlements subjectmappingbuiltin.AttributeValueFQNsToActions + entitlements subjectmappingresolution.AttributeValueFQNsToActions expectError bool expectPass bool }{ @@ -821,7 +821,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { }, EphemeralId: "test-attr-values-id-1", }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelMidFQN: []*policy.Action{actionRead}, }, expectError: false, @@ -835,7 +835,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { }, EphemeralId: "test-reg-res-id-1", }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelHighestFQN: []*policy.Action{actionRead}, }, expectError: false, @@ -849,7 +849,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { }, EphemeralId: "test-reg-res-id-2", }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ projectAvengersFQN: []*policy.Action{actionRead}, projectJusticeLeagueFQN: []*policy.Action{actionRead}, }, @@ -864,7 +864,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { }, EphemeralId: "test-reg-res-id-3", }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ // Missing projectJusticeLeagueFQN projectAvengersFQN: []*policy.Action{actionRead}, }, @@ -879,7 +879,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { }, EphemeralId: "test-reg-res-id-4", }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ // Wrong action levelHighestFQN: []*policy.Action{actionCreate}, }, @@ -894,14 +894,14 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { }, EphemeralId: "test-reg-res-id-5", }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{}, + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{}, expectError: true, expectPass: false, }, { name: "invalid nil resource", resource: nil, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{}, + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{}, expectError: true, }, { @@ -912,7 +912,7 @@ func (s *EvaluateTestSuite) TestGetResourceDecision() { }, EphemeralId: "test-reg-res-id-6", }, - entitlements: subjectmappingbuiltin.AttributeValueFQNsToActions{ + entitlements: subjectmappingresolution.AttributeValueFQNsToActions{ levelHighestFQN: []*policy.Action{actionRead}, }, expectError: false, diff --git a/service/internal/access/v2/helpers.go b/service/pkg/access/helpers.go similarity index 100% rename from service/internal/access/v2/helpers.go rename to service/pkg/access/helpers.go diff --git a/service/internal/access/v2/helpers_test.go b/service/pkg/access/helpers_test.go similarity index 100% rename from service/internal/access/v2/helpers_test.go rename to service/pkg/access/helpers_test.go diff --git a/service/internal/access/v2/just_in_time_authorizer.go b/service/pkg/access/just_in_time_authorizer.go similarity index 97% rename from service/internal/access/v2/just_in_time_authorizer.go rename to service/pkg/access/just_in_time_authorizer.go index 2472609bc2..877f9ede64 100644 --- a/service/internal/access/v2/just_in_time_authorizer.go +++ b/service/pkg/access/just_in_time_authorizer.go @@ -14,9 +14,9 @@ import ( "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/subjectmapping" otdfSDK "github.com/opentdf/platform/sdk" - "github.com/opentdf/platform/service/internal/access/v2/plugin" - policyStore "github.com/opentdf/platform/service/internal/access/v2/store" - "github.com/opentdf/platform/service/internal/subjectmappingbuiltin" + "github.com/opentdf/platform/service/pkg/access/plugin" + policyStore "github.com/opentdf/platform/service/pkg/access/store" + subjectmappingresolution "github.com/opentdf/platform/service/pkg/access/subject-mapping-resolution" "github.com/opentdf/platform/service/logger" ) @@ -153,7 +153,7 @@ func (p *JustInTimeAuthorizer) GetDecision( } // Resolve them to their entitled FQNs and the actions available on each - entitledFQNsToActions, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(decisionableAttributes, entityRep) + entitledFQNsToActions, err := subjectmappingresolution.EvaluateSubjectMappingsWithActions(decisionableAttributes, entityRep) if err != nil { return nil, false, fmt.Errorf("error evaluating subject mappings for entitlement: %w", err) } diff --git a/service/internal/access/v2/pdp.go b/service/pkg/access/pdp.go similarity index 97% rename from service/internal/access/v2/pdp.go rename to service/pkg/access/pdp.go index 0d5225f6c6..b07cf49ad9 100644 --- a/service/internal/access/v2/pdp.go +++ b/service/pkg/access/pdp.go @@ -13,9 +13,9 @@ import ( entityresolutionV2 "github.com/opentdf/platform/protocol/go/entityresolution/v2" "github.com/opentdf/platform/protocol/go/policy" attrs "github.com/opentdf/platform/protocol/go/policy/attributes" - "github.com/opentdf/platform/service/internal/subjectmappingbuiltin" "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/logger/audit" + subjectmappingresolution "github.com/opentdf/platform/service/pkg/access/subject-mapping-resolution" ) // Decision represents the overall access decision for an entity. @@ -184,7 +184,7 @@ func (p *PolicyDecisionPoint) GetDecision( l.DebugContext(ctx, "filtered to only entitlements relevant to decisioning", slog.Int("decisionable_attribute_values_count", len(decisionableAttributes))) // Resolve them to their entitled FQNs and the actions available on each - entitledFQNsToActions, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(decisionableAttributes, entityRepresentation) + entitledFQNsToActions, err := subjectmappingresolution.EvaluateSubjectMappingsWithActions(decisionableAttributes, entityRepresentation) if err != nil { return nil, fmt.Errorf("error evaluating subject mappings for entitlement: %w", err) } @@ -338,7 +338,7 @@ func (p *PolicyDecisionPoint) GetEntitlements( } // Resolve them to their entitled FQNs and the actions available on each - entityIDsToFQNsToActions, err := subjectmappingbuiltin.EvaluateSubjectMappingMultipleEntitiesWithActions(entitleableAttributes, entityRepresentations) + entityIDsToFQNsToActions, err := subjectmappingresolution.EvaluateSubjectMappingMultipleEntitiesWithActions(entitleableAttributes, entityRepresentations) if err != nil { return nil, fmt.Errorf("error evaluating subject mappings for entitlement: %w", err) } diff --git a/service/internal/access/v2/pdp_test.go b/service/pkg/access/pdp_test.go similarity index 100% rename from service/internal/access/v2/pdp_test.go rename to service/pkg/access/pdp_test.go diff --git a/service/internal/access/v2/plugin/granular.go b/service/pkg/access/plugin/granular.go similarity index 93% rename from service/internal/access/v2/plugin/granular.go rename to service/pkg/access/plugin/granular.go index 31deff5d97..e24b81dc91 100644 --- a/service/internal/access/v2/plugin/granular.go +++ b/service/pkg/access/plugin/granular.go @@ -12,9 +12,9 @@ import ( authzV2 "github.com/opentdf/platform/protocol/go/authorization/v2" ersV2 "github.com/opentdf/platform/protocol/go/entityresolution/v2" policy "github.com/opentdf/platform/protocol/go/policy" - policyStore "github.com/opentdf/platform/service/internal/access/v2/store" - "github.com/opentdf/platform/service/internal/subjectmappingbuiltin" "github.com/opentdf/platform/service/logger" + policyStore "github.com/opentdf/platform/service/pkg/access/store" + subjectmappingresolution "github.com/opentdf/platform/service/pkg/access/subject-mapping-resolution" ) // Map of resources to ACL (mock database) @@ -55,7 +55,7 @@ func (p *GranularCustomPdp) IsReady(_ context.Context) bool { func (p *GranularCustomPdp) GetDecision( ctx context.Context, entityRepresentation *ersV2.EntityRepresentation, - _ *subjectmappingbuiltin.AttributeValueFQNsToActions, + _ *subjectmappingresolution.AttributeValueFQNsToActions, action *policy.Action, resource *authzV2.Resource, ) (bool, error) { diff --git a/service/internal/access/v2/plugin/plugin.go b/service/pkg/access/plugin/plugin.go similarity index 83% rename from service/internal/access/v2/plugin/plugin.go rename to service/pkg/access/plugin/plugin.go index f0e0cd0428..65cd38ce2b 100644 --- a/service/internal/access/v2/plugin/plugin.go +++ b/service/pkg/access/plugin/plugin.go @@ -6,16 +6,16 @@ import ( authzV2 "github.com/opentdf/platform/protocol/go/authorization/v2" ersV2 "github.com/opentdf/platform/protocol/go/entityresolution/v2" policy "github.com/opentdf/platform/protocol/go/policy" - policyStore "github.com/opentdf/platform/service/internal/access/v2/store" - "github.com/opentdf/platform/service/internal/subjectmappingbuiltin" "github.com/opentdf/platform/service/logger" + policyStore "github.com/opentdf/platform/service/pkg/access/store" + subjectmappingresolution "github.com/opentdf/platform/service/pkg/access/subject-mapping-resolution" ) type PolicyDecisionPoint interface { // Initialize a plugin PDP with a dedicated logger and access to policy New(ctx context.Context, l *logger.Logger, store policyStore.EntitlementPolicyStore, attributeFQNPrefixes []string) error // Make a decision based on an entity representation, platform policy entitlements, a requested action, and a relevant resource - GetDecision(ctx context.Context, entityRepresentation *ersV2.EntityRepresentation, entitlements *subjectmappingbuiltin.AttributeValueFQNsToActions, action *policy.Action, resource *authzV2.Resource) (bool, error) + GetDecision(ctx context.Context, entityRepresentation *ersV2.EntityRepresentation, entitlements *subjectmappingresolution.AttributeValueFQNsToActions, action *policy.Action, resource *authzV2.Resource) (bool, error) // Determine if a given resource is able to be decisioned upon by this PDP implementation IsValidDecisionableResource(resource *authzV2.Resource) bool // Determine if a given action is able to be decisioned upon by this PDP implementation diff --git a/service/internal/access/v2/store/policy_store.go b/service/pkg/access/store/policy_store.go similarity index 100% rename from service/internal/access/v2/store/policy_store.go rename to service/pkg/access/store/policy_store.go diff --git a/service/internal/subjectmappingbuiltin/subject_mapping_builtin_actions.go b/service/pkg/access/subject-mapping-resolution/resolution_actions.go similarity index 93% rename from service/internal/subjectmappingbuiltin/subject_mapping_builtin_actions.go rename to service/pkg/access/subject-mapping-resolution/resolution_actions.go index 491d1bb8f7..5cf07e8483 100644 --- a/service/internal/subjectmappingbuiltin/subject_mapping_builtin_actions.go +++ b/service/pkg/access/subject-mapping-resolution/resolution_actions.go @@ -1,4 +1,4 @@ -package subjectmappingbuiltin +package subjectmappingresolution import ( "fmt" @@ -10,6 +10,7 @@ import ( entityresolutionV2 "github.com/opentdf/platform/protocol/go/entityresolution/v2" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/attributes" + "github.com/opentdf/platform/service/internal/subjectmappingbuiltin" ) type AttributeValueFQNsToActions map[string][]*policy.Action @@ -51,7 +52,7 @@ func EvaluateSubjectMappingsWithActions( for _, subjectMapping := range attributeAndValue.GetValue().GetSubjectMappings() { subjectMappingResult := true for _, subjectSet := range subjectMapping.GetSubjectConditionSet().GetSubjectSets() { - subjectSetConditionResult, err := EvaluateSubjectSet(subjectSet, flattenedEntity) + subjectSetConditionResult, err := subjectmappingbuiltin.EvaluateSubjectSet(subjectSet, flattenedEntity) if err != nil { return nil, err } diff --git a/service/internal/subjectmappingbuiltin/subject_mapping_builtin_actions_test.go b/service/pkg/access/subject-mapping-resolution/resolution_actions_test.go similarity index 87% rename from service/internal/subjectmappingbuiltin/subject_mapping_builtin_actions_test.go rename to service/pkg/access/subject-mapping-resolution/resolution_actions_test.go index 2b5a86c1a4..bb279e66af 100644 --- a/service/internal/subjectmappingbuiltin/subject_mapping_builtin_actions_test.go +++ b/service/pkg/access/subject-mapping-resolution/resolution_actions_test.go @@ -1,4 +1,4 @@ -package subjectmappingbuiltin_test +package subjectmappingresolution import ( "testing" @@ -6,8 +6,6 @@ import ( entityresolutionV2 "github.com/opentdf/platform/protocol/go/entityresolution/v2" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/attributes" - "github.com/opentdf/platform/service/internal/subjectmappingbuiltin" - "github.com/opentdf/platform/service/policy/actions" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/structpb" @@ -18,6 +16,10 @@ var ( classConfFQN = "https://example.com/attr/class/value/conf" classRestrictedFQN = "https://example.com/attr/class/value/restricted" + actionNameRead = "read" + actionNameCreate = "create" + actionNameDelete = "delete" + departmentEngineeringSM = &policy.SubjectMapping{ SubjectConditionSet: &policy.SubjectConditionSet{ SubjectSets: []*policy.SubjectSet{ @@ -39,10 +41,10 @@ var ( }, Actions: []*policy.Action{ { - Name: actions.ActionNameRead, + Name: actionNameRead, }, { - Name: actions.ActionNameCreate, + Name: actionNameCreate, }, }, } @@ -68,7 +70,7 @@ var ( }, Actions: []*policy.Action{ { - Name: actions.ActionNameRead, + Name: actionNameRead, }, }, } @@ -99,7 +101,7 @@ var ( }, Actions: []*policy.Action{ { - Name: actions.ActionNameRead, + Name: actionNameRead, }, }, } @@ -143,7 +145,7 @@ func TestEvaluateSubjectMappingMultipleEntitiesWithActions_SingleEntity(t *testi ), } - result, err := subjectmappingbuiltin.EvaluateSubjectMappingMultipleEntitiesWithActions(attributeMappings, []*entityresolutionV2.EntityRepresentation{engineeringEntity}) + result, err := EvaluateSubjectMappingMultipleEntitiesWithActions(attributeMappings, []*entityresolutionV2.EntityRepresentation{engineeringEntity}) require.NoError(t, err) assert.Len(t, result, 1) @@ -161,8 +163,8 @@ func TestEvaluateSubjectMappingMultipleEntitiesWithActions_SingleEntity(t *testi for _, action := range actionsList { actionNames = append(actionNames, action.GetName()) } - assert.Contains(t, actionNames, actions.ActionNameRead) - assert.Contains(t, actionNames, actions.ActionNameCreate) + assert.Contains(t, actionNames, actionNameRead) + assert.Contains(t, actionNames, actionNameCreate) } func TestEvaluateSubjectMappingMultipleEntitiesWithActions_MultipleEntities(t *testing.T) { @@ -184,7 +186,7 @@ func TestEvaluateSubjectMappingMultipleEntitiesWithActions_MultipleEntities(t *t } // Execute function - result, err := subjectmappingbuiltin.EvaluateSubjectMappingMultipleEntitiesWithActions( + result, err := EvaluateSubjectMappingMultipleEntitiesWithActions( attributeMappings, []*entityresolutionV2.EntityRepresentation{engineeringEntity, salesEntity}, ) @@ -204,8 +206,8 @@ func TestEvaluateSubjectMappingMultipleEntitiesWithActions_MultipleEntities(t *t for _, action := range engActions { engActionNames = append(engActionNames, action.GetName()) } - assert.Contains(t, engActionNames, actions.ActionNameRead) - assert.Contains(t, engActionNames, actions.ActionNameCreate) + assert.Contains(t, engActionNames, actionNameRead) + assert.Contains(t, engActionNames, actionNameCreate) // Check sales entity entitlements salesEntitlements, exists := result["sales-entity"] @@ -214,7 +216,7 @@ func TestEvaluateSubjectMappingMultipleEntitiesWithActions_MultipleEntities(t *t assert.True(t, exists) // Sales entity should only have read access assert.Len(t, salesActions, 1) - assert.Equal(t, actions.ActionNameRead, salesActions[0].GetName()) + assert.Equal(t, actionNameRead, salesActions[0].GetName()) } func TestEvaluateSubjectMappingMultipleEntitiesWithActions_NoMatchingEntities(t *testing.T) { @@ -232,7 +234,7 @@ func TestEvaluateSubjectMappingMultipleEntitiesWithActions_NoMatchingEntities(t } // Execute function - result, err := subjectmappingbuiltin.EvaluateSubjectMappingMultipleEntitiesWithActions( + result, err := EvaluateSubjectMappingMultipleEntitiesWithActions( attributeMappings, []*entityresolutionV2.EntityRepresentation{marketingEntity}, ) @@ -266,7 +268,7 @@ func TestEvaluateSubjectMappingMultipleEntitiesWithActions_MultipleAttributes(t } // Execute function - result, err := subjectmappingbuiltin.EvaluateSubjectMappingMultipleEntitiesWithActions( + result, err := EvaluateSubjectMappingMultipleEntitiesWithActions( attributeMappings, []*entityresolutionV2.EntityRepresentation{engineeringEntity}, ) @@ -288,15 +290,15 @@ func TestEvaluateSubjectMappingMultipleEntitiesWithActions_MultipleAttributes(t for _, action := range confActions { actionNames = append(actionNames, action.GetName()) } - assert.Contains(t, actionNames, actions.ActionNameRead) - assert.Contains(t, actionNames, actions.ActionNameCreate) + assert.Contains(t, actionNames, actionNameRead) + assert.Contains(t, actionNames, actionNameCreate) // Check group-based entitlements (Internal) internalActions, exists := entitlements[classIntFQN] assert.True(t, exists) assert.NotEmpty(t, internalActions) assert.Len(t, internalActions, 1) - assert.Equal(t, actions.ActionNameRead, internalActions[0].GetName()) + assert.Equal(t, actionNameRead, internalActions[0].GetName()) } func TestEvaluateSubjectMappingsWithActions_OneGoodResolution(t *testing.T) { @@ -356,7 +358,7 @@ func TestEvaluateSubjectMappingsWithActions_OneGoodResolution(t *testing.T) { ), } // Execute function - entitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, entity) + entitlements, err := EvaluateSubjectMappingsWithActions(attributeMappings, entity) require.NoError(t, err) assert.Len(t, entitlements, 1) @@ -422,7 +424,7 @@ func TestEvaluateSubjectMappingsWithActions_MultipleMatchingSubjectMappings(t *t } // Execute function - entitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, multiMatchEntity) + entitlements, err := EvaluateSubjectMappingsWithActions(attributeMappings, multiMatchEntity) // Validate results require.NoError(t, err) @@ -438,8 +440,8 @@ func TestEvaluateSubjectMappingsWithActions_MultipleMatchingSubjectMappings(t *t actionNameSet[action.GetName()] = true } assert.Len(t, actionNameSet, 3) - assert.True(t, actionNameSet[actions.ActionNameRead]) - assert.True(t, actionNameSet[actions.ActionNameCreate]) + assert.True(t, actionNameSet[actionNameRead]) + assert.True(t, actionNameSet[actionNameCreate]) assert.True(t, actionNameSet["custom_action"]) // Check internal actions @@ -447,7 +449,7 @@ func TestEvaluateSubjectMappingsWithActions_MultipleMatchingSubjectMappings(t *t assert.True(t, exists) assert.Len(t, internalActions, 1) - assert.Equal(t, actions.ActionNameRead, internalActions[0].GetName()) + assert.Equal(t, actionNameRead, internalActions[0].GetName()) } func TestEvaluateSubjectMappingsWithActions_NoMatchingSubjectMappings(t *testing.T) { @@ -470,7 +472,7 @@ func TestEvaluateSubjectMappingsWithActions_NoMatchingSubjectMappings(t *testing } // Execute function - entitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, marketingEntity) + entitlements, err := EvaluateSubjectMappingsWithActions(attributeMappings, marketingEntity) // Validate results require.NoError(t, err) @@ -515,10 +517,10 @@ func TestEvaluateSubjectMappingsWithActions_ComplexCondition_MultipleConditionGr }, Actions: []*policy.Action{ { - Name: actions.ActionNameRead, + Name: actionNameRead, }, { - Name: actions.ActionNameDelete, + Name: actionNameDelete, }, }, } @@ -556,7 +558,7 @@ func TestEvaluateSubjectMappingsWithActions_ComplexCondition_MultipleConditionGr } // Test senior engineer - seniorEntitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, seniorEngEntity) + seniorEntitlements, err := EvaluateSubjectMappingsWithActions(attributeMappings, seniorEngEntity) require.NoError(t, err) assert.Empty(t, seniorEntitlements) seniorActions, exists := seniorEntitlements[classRestrictedFQN] @@ -564,7 +566,7 @@ func TestEvaluateSubjectMappingsWithActions_ComplexCondition_MultipleConditionGr assert.Empty(t, seniorActions) // Test principal engineer with admin - adminEntitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, principalEngWithAdmin) + adminEntitlements, err := EvaluateSubjectMappingsWithActions(attributeMappings, principalEngWithAdmin) require.NoError(t, err) assert.Len(t, adminEntitlements, 1) seniorWithAdminActions, exists := adminEntitlements[classRestrictedFQN] @@ -574,11 +576,11 @@ func TestEvaluateSubjectMappingsWithActions_ComplexCondition_MultipleConditionGr for _, action := range seniorWithAdminActions { actionNames = append(actionNames, action.GetName()) } - assert.Contains(t, actionNames, actions.ActionNameRead) - assert.Contains(t, actionNames, actions.ActionNameDelete) + assert.Contains(t, actionNames, actionNameRead) + assert.Contains(t, actionNames, actionNameDelete) // Test senior engineer with admin in a different index - adminEntitlementsBadIndex, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, seniorEngWithAdminEntityInBadIndex) + adminEntitlementsBadIndex, err := EvaluateSubjectMappingsWithActions(attributeMappings, seniorEngWithAdminEntityInBadIndex) require.NoError(t, err) assert.Empty(t, adminEntitlementsBadIndex) adminActionsBadIndex, exists := adminEntitlementsBadIndex[classRestrictedFQN] @@ -586,7 +588,7 @@ func TestEvaluateSubjectMappingsWithActions_ComplexCondition_MultipleConditionGr assert.Empty(t, adminActionsBadIndex) // Test non-engineering admin - nonEngAdminEntitlements, err := subjectmappingbuiltin.EvaluateSubjectMappingsWithActions(attributeMappings, nonEngAdminEntity) + nonEngAdminEntitlements, err := EvaluateSubjectMappingsWithActions(attributeMappings, nonEngAdminEntity) require.NoError(t, err) assert.Empty(t, nonEngAdminEntitlements) nonEngAdminActions, exists := nonEngAdminEntitlements[classRestrictedFQN] diff --git a/service/internal/access/v2/validators.go b/service/pkg/access/validators.go similarity index 97% rename from service/internal/access/v2/validators.go rename to service/pkg/access/validators.go index e49c9d6509..d3c99330a5 100644 --- a/service/internal/access/v2/validators.go +++ b/service/pkg/access/validators.go @@ -10,7 +10,7 @@ import ( entityresolutionV2 "github.com/opentdf/platform/protocol/go/entityresolution/v2" "github.com/opentdf/platform/protocol/go/policy" attrs "github.com/opentdf/platform/protocol/go/policy/attributes" - "github.com/opentdf/platform/service/internal/subjectmappingbuiltin" + subjectmappingresolution "github.com/opentdf/platform/service/pkg/access/subject-mapping-resolution" ) var ( @@ -176,7 +176,7 @@ func validateEntityRepresentations(entityRepresentations []*entityresolutionV2.E // - resource: must not be nil func validateGetResourceDecision( accessibleAttributeValues map[string]*attrs.GetAttributeValuesByFqnsResponse_AttributeAndValue, - entitlements subjectmappingbuiltin.AttributeValueFQNsToActions, + entitlements subjectmappingresolution.AttributeValueFQNsToActions, action *policy.Action, resource *authzV2.Resource, ) error { diff --git a/service/internal/access/v2/validators_test.go b/service/pkg/access/validators_test.go similarity index 100% rename from service/internal/access/v2/validators_test.go rename to service/pkg/access/validators_test.go diff --git a/service/pkg/server/options.go b/service/pkg/server/options.go index 579113c5bd..5969c11791 100644 --- a/service/pkg/server/options.go +++ b/service/pkg/server/options.go @@ -2,7 +2,7 @@ package server import ( "github.com/casbin/casbin/v2/persist" - "github.com/opentdf/platform/service/internal/access/v2/plugin" + "github.com/opentdf/platform/service/pkg/access/plugin" "github.com/opentdf/platform/service/pkg/config" "github.com/opentdf/platform/service/pkg/serviceregistry" "github.com/opentdf/platform/service/trust" diff --git a/service/pkg/server/services.go b/service/pkg/server/services.go index cf589cdc1a..ccb0b1c634 100644 --- a/service/pkg/server/services.go +++ b/service/pkg/server/services.go @@ -15,10 +15,10 @@ import ( "github.com/opentdf/platform/service/entityresolution" entityresolutionV2 "github.com/opentdf/platform/service/entityresolution/v2" "github.com/opentdf/platform/service/health" - "github.com/opentdf/platform/service/internal/access/v2/plugin" "github.com/opentdf/platform/service/internal/server" "github.com/opentdf/platform/service/kas" logging "github.com/opentdf/platform/service/logger" + "github.com/opentdf/platform/service/pkg/access/plugin" "github.com/opentdf/platform/service/pkg/cache" "github.com/opentdf/platform/service/pkg/config" "github.com/opentdf/platform/service/pkg/db" diff --git a/service/pkg/serviceregistry/serviceregistry.go b/service/pkg/serviceregistry/serviceregistry.go index 0f77fab114..d212b245e1 100644 --- a/service/pkg/serviceregistry/serviceregistry.go +++ b/service/pkg/serviceregistry/serviceregistry.go @@ -15,9 +15,9 @@ import ( "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" - "github.com/opentdf/platform/service/internal/access/v2/plugin" "github.com/opentdf/platform/service/internal/server" "github.com/opentdf/platform/service/logger" + "github.com/opentdf/platform/service/pkg/access/plugin" "github.com/opentdf/platform/service/pkg/cache" "github.com/opentdf/platform/service/pkg/config" "github.com/opentdf/platform/service/pkg/db" From 5edcae7d640ddc060cc63a4c1556e9d9bd5719f8 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Fri, 12 Sep 2025 08:10:42 -0700 Subject: [PATCH 5/9] do not error if encountering registered resource not found in memory loaded from policy --- service/pkg/access/helpers.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/service/pkg/access/helpers.go b/service/pkg/access/helpers.go index 574ff98181..502f505377 100644 --- a/service/pkg/access/helpers.go +++ b/service/pkg/access/helpers.go @@ -191,7 +191,7 @@ func mergeDeduplicatedActions(actionsSet map[string]*policy.Action, actionsToMer func getResourceDecisionableAttributes( ctx context.Context, - logger *logger.Logger, + l *logger.Logger, accessibleRegisteredResourceValues map[string]*policy.RegisteredResourceValue, entitleableAttributesByValueFQN map[string]*attrs.GetAttributeValuesByFqnsResponse_AttributeAndValue, // action *policy.Action, @@ -214,7 +214,11 @@ func getResourceDecisionableAttributes( regResValueFQN := strings.ToLower(resource.GetRegisteredResourceValueFqn()) regResValue, found := accessibleRegisteredResourceValues[regResValueFQN] if !found { - return nil, fmt.Errorf("resource registered resource value FQN not found in memory [%s]: %w", regResValueFQN, ErrInvalidResource) + l.DebugContext(ctx, + "resource registered resource value FQN not found in memory - possible custom PDP resource", + slog.String("registered_resource_value_fqn", regResValueFQN), + ) + // return nil, fmt.Errorf("resource registered resource value FQN not found in memory [%s]: %w", regResValueFQN, ErrInvalidResource) } for _, aav := range regResValue.GetActionAttributeValues() { @@ -249,7 +253,7 @@ func getResourceDecisionableAttributes( } decisionableAttributes[attrValueFQN] = attributeAndValue - err := populateHigherValuesIfHierarchy(ctx, logger, attrValueFQN, attributeAndValue.GetAttribute(), entitleableAttributesByValueFQN, decisionableAttributes) + err := populateHigherValuesIfHierarchy(ctx, l, attrValueFQN, attributeAndValue.GetAttribute(), entitleableAttributesByValueFQN, decisionableAttributes) if err != nil { return nil, fmt.Errorf("error populating higher hierarchy attribute values: %w", err) } From 60054db3c1592657c83747a911bab19ffc0319e4 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Fri, 12 Sep 2025 09:32:08 -0700 Subject: [PATCH 6/9] wrap error --- service/pkg/access/just_in_time_authorizer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/pkg/access/just_in_time_authorizer.go b/service/pkg/access/just_in_time_authorizer.go index 877f9ede64..bb2e468d20 100644 --- a/service/pkg/access/just_in_time_authorizer.go +++ b/service/pkg/access/just_in_time_authorizer.go @@ -159,7 +159,7 @@ func (p *JustInTimeAuthorizer) GetDecision( } isAllowed, err := pluginPDP.GetDecision(ctx, entityRep, &entitledFQNsToActions, action, resources[0]) if err != nil { - return nil, false, fmt.Errorf("error evaluating plugin PDP %s", pluginPDP.Name()) + return nil, false, fmt.Errorf("error evaluating plugin PDP %s: %w", pluginPDP.Name(), err) } d = &Decision{ Access: isAllowed, From 3e4008f9bb441f0b78b704e9391308a6c9552dfb Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Fri, 12 Sep 2025 11:10:46 -0700 Subject: [PATCH 7/9] allow dynamic attribute values --- service/pkg/access/helpers.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/service/pkg/access/helpers.go b/service/pkg/access/helpers.go index 502f505377..7da18f5608 100644 --- a/service/pkg/access/helpers.go +++ b/service/pkg/access/helpers.go @@ -249,7 +249,11 @@ func getResourceDecisionableAttributes( attributeAndValue, ok := entitleableAttributesByValueFQN[attrValueFQN] if !ok { - return nil, fmt.Errorf("resource attribute value FQN not found in memory [%s]: %w", attrValueFQN, ErrInvalidResource) + l.DebugContext(ctx, + "resource attribute value FQN not found in memory - possible custom PDP resource", + slog.String("attribute_value_fqn", attrValueFQN), + ) + // return nil, fmt.Errorf("resource attribute value FQN not found in memory [%s]: %w", attrValueFQN, ErrInvalidResource) } decisionableAttributes[attrValueFQN] = attributeAndValue From 48371b6d78eb2d6b5e07ee5cf1aace79bf3a8c7c Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Fri, 12 Sep 2025 11:42:40 -0700 Subject: [PATCH 8/9] tweak --- service/pkg/access/helpers.go | 1 + 1 file changed, 1 insertion(+) diff --git a/service/pkg/access/helpers.go b/service/pkg/access/helpers.go index 7da18f5608..72cf7c5170 100644 --- a/service/pkg/access/helpers.go +++ b/service/pkg/access/helpers.go @@ -253,6 +253,7 @@ func getResourceDecisionableAttributes( "resource attribute value FQN not found in memory - possible custom PDP resource", slog.String("attribute_value_fqn", attrValueFQN), ) + continue // return nil, fmt.Errorf("resource attribute value FQN not found in memory [%s]: %w", attrValueFQN, ErrInvalidResource) } From 70c9188755cad6ebfa14f51423f6cb8f7b505be9 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Fri, 12 Sep 2025 12:22:30 -0700 Subject: [PATCH 9/9] move to entity interface instead --- service/cmd/start.go | 4 - service/pkg/access/just_in_time_authorizer.go | 9 +- service/pkg/access/plugin/granular.go | 118 ------------------ service/pkg/access/plugin/plugin.go | 40 +++++- 4 files changed, 47 insertions(+), 124 deletions(-) delete mode 100644 service/pkg/access/plugin/granular.go diff --git a/service/cmd/start.go b/service/cmd/start.go index 813dee47e6..31d8c0e8d8 100644 --- a/service/cmd/start.go +++ b/service/cmd/start.go @@ -1,7 +1,6 @@ package cmd import ( - "github.com/opentdf/platform/service/pkg/access/plugin" "github.com/opentdf/platform/service/pkg/server" "github.com/spf13/cobra" ) @@ -21,12 +20,9 @@ func start(cmd *cobra.Command, _ []string) error { configFile, _ := cmd.Flags().GetString(configFileFlag) configKey, _ := cmd.Flags().GetString(configKeyFlag) - pluginPDPs := []plugin.PolicyDecisionPoint{&plugin.GranularCustomPdp{}} - return server.Start( server.WithWaitForShutdownSignal(), server.WithConfigFile(configFile), server.WithConfigKey(configKey), - server.WithPluginPDPs(pluginPDPs...), ) } diff --git a/service/pkg/access/just_in_time_authorizer.go b/service/pkg/access/just_in_time_authorizer.go index bb2e468d20..3dbfc5b3d0 100644 --- a/service/pkg/access/just_in_time_authorizer.go +++ b/service/pkg/access/just_in_time_authorizer.go @@ -157,7 +157,14 @@ func (p *JustInTimeAuthorizer) GetDecision( if err != nil { return nil, false, fmt.Errorf("error evaluating subject mappings for entitlement: %w", err) } - isAllowed, err := pluginPDP.GetDecision(ctx, entityRep, &entitledFQNsToActions, action, resources[0]) + + resolvedEntity := plugin.NewEntity( + entityRep, + &entitledFQNsToActions, + entityIdentifier, + ) + + isAllowed, err := pluginPDP.GetDecision(ctx, resolvedEntity, action, resources[0]) if err != nil { return nil, false, fmt.Errorf("error evaluating plugin PDP %s: %w", pluginPDP.Name(), err) } diff --git a/service/pkg/access/plugin/granular.go b/service/pkg/access/plugin/granular.go deleted file mode 100644 index e24b81dc91..0000000000 --- a/service/pkg/access/plugin/granular.go +++ /dev/null @@ -1,118 +0,0 @@ -package plugin - -import ( - "context" - "errors" - "fmt" - "log/slog" - "net/mail" - "slices" - "strings" - - authzV2 "github.com/opentdf/platform/protocol/go/authorization/v2" - ersV2 "github.com/opentdf/platform/protocol/go/entityresolution/v2" - policy "github.com/opentdf/platform/protocol/go/policy" - "github.com/opentdf/platform/service/logger" - policyStore "github.com/opentdf/platform/service/pkg/access/store" - subjectmappingresolution "github.com/opentdf/platform/service/pkg/access/subject-mapping-resolution" -) - -// Map of resources to ACL (mock database) -var mockACL = map[string][]string{ - "https://reg_res/granular/value/123": {"test@example.com", "test2@example.com"}, - "https://reg_res/granular/value/456": {"someone@gmail.com"}, -} -var allowedGranularActions = []string{"read", "send"} - -const ( - fieldEmailAddress = "email" - granularPluginPDPName = "granular-plugin-pdp" -) - -type GranularCustomPdp struct { - l *logger.Logger - resourceFQNPrefixes []string -} - -// Initializes a new GranularPDP -func (p *GranularCustomPdp) New(ctx context.Context, l *logger.Logger, _ policyStore.EntitlementPolicyStore, attributeFQNPrefixes []string) error { - p.resourceFQNPrefixes = attributeFQNPrefixes - p.l = l.With("component", granularPluginPDPName) - return nil -} - -func (p *GranularCustomPdp) Name() string { - return granularPluginPDPName -} - -// Granular plugin PDP is always ready -func (p *GranularCustomPdp) IsReady(_ context.Context) bool { - return true -} - -// Ensure the decision is one of the allowed decision and a valid resource, then check -// the email in the entity representation against our in-memory ACL -func (p *GranularCustomPdp) GetDecision( - ctx context.Context, - entityRepresentation *ersV2.EntityRepresentation, - _ *subjectmappingresolution.AttributeValueFQNsToActions, - action *policy.Action, - resource *authzV2.Resource, -) (bool, error) { - if !p.IsValidDecisionableResource(resource) { - return false, errors.New("resource is not decisionable") - } - if !p.IsValidDecisionableAction(action) { - return false, errors.New("action is not decisionable") - } - - var entityEmail string - for _, prop := range entityRepresentation.GetAdditionalProps() { - for field, value := range prop.GetFields() { - if field == fieldEmailAddress { - if e, err := mail.ParseAddress(value.GetStringValue()); err == nil { - entityEmail = e.Address - break - } - } - } - if entityEmail != "" { - break - } - } - - if entityEmail == "" { - return false, fmt.Errorf("no email found in entity representation") - } - - granularResourceFQN := resource.GetRegisteredResourceValueFqn() - for resourceName, acl := range mockACL { - if resourceName == granularResourceFQN { - if slices.Contains(acl, entityEmail) { - return true, nil - } - p.l.DebugContext(ctx, "access denied per the ACL", slog.String("email", entityEmail), slog.String("resource", resourceName)) - return false, errors.New("access denied") - } - } - - return false, nil -} - -// Ensures resource is a registered resource with an FQN matching configured prefix -func (p *GranularCustomPdp) IsValidDecisionableResource(resource *authzV2.Resource) bool { - switch resource.GetResource().(type) { - case *authzV2.Resource_RegisteredResourceValueFqn: - for _, prefix := range p.resourceFQNPrefixes { - if strings.HasPrefix(resource.GetRegisteredResourceValueFqn(), prefix) { - return true - } - } - } - return false -} - -// Check our allowed actions -func (p *GranularCustomPdp) IsValidDecisionableAction(action *policy.Action) bool { - return slices.Contains(allowedGranularActions, strings.ToLower(action.GetName())) -} diff --git a/service/pkg/access/plugin/plugin.go b/service/pkg/access/plugin/plugin.go index 65cd38ce2b..267b361915 100644 --- a/service/pkg/access/plugin/plugin.go +++ b/service/pkg/access/plugin/plugin.go @@ -15,7 +15,7 @@ type PolicyDecisionPoint interface { // Initialize a plugin PDP with a dedicated logger and access to policy New(ctx context.Context, l *logger.Logger, store policyStore.EntitlementPolicyStore, attributeFQNPrefixes []string) error // Make a decision based on an entity representation, platform policy entitlements, a requested action, and a relevant resource - GetDecision(ctx context.Context, entityRepresentation *ersV2.EntityRepresentation, entitlements *subjectmappingresolution.AttributeValueFQNsToActions, action *policy.Action, resource *authzV2.Resource) (bool, error) + GetDecision(ctx context.Context, entity EntityI, action *policy.Action, resource *authzV2.Resource) (bool, error) // Determine if a given resource is able to be decisioned upon by this PDP implementation IsValidDecisionableResource(resource *authzV2.Resource) bool // Determine if a given action is able to be decisioned upon by this PDP implementation @@ -32,6 +32,44 @@ type PolicyDecisionPointConfig struct { Name string `mapstructure:"name" json:"name"` } +type EntityI interface { + EntityRepresentation() *ersV2.EntityRepresentation + Entitlements() *subjectmappingresolution.AttributeValueFQNsToActions + OriginalEntity() *authzV2.EntityIdentifier +} + +type Entity struct { + entityRepresentation *ersV2.EntityRepresentation + entitlements *subjectmappingresolution.AttributeValueFQNsToActions + originalEntity *authzV2.EntityIdentifier +} + +// TODO: take in only the EntityIdentifier, an SDK, and Policy (attrs/SMs/RegisteredResources) +// then do the work to resolve each of the pieces via an ERS roundtrip and SM resolution +func NewEntity( + entityRepresentation *ersV2.EntityRepresentation, + entitlements *subjectmappingresolution.AttributeValueFQNsToActions, + originalEntity *authzV2.EntityIdentifier, +) *Entity { + return &Entity{ + entityRepresentation: entityRepresentation, + entitlements: entitlements, + originalEntity: originalEntity, + } +} + +func (e *Entity) EntityRepresentation() *ersV2.EntityRepresentation { + return e.entityRepresentation +} + +func (e *Entity) Entitlements() *subjectmappingresolution.AttributeValueFQNsToActions { + return e.entitlements +} + +func (e *Entity) OriginalEntity() *authzV2.EntityIdentifier { + return e.originalEntity +} + // TODO: refactor so we have O(1) lookups by FQN instead of unprocessed lists with O(n) lookup // type policyStore interface { // AttributeAndValuesByValueFQN()