mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-06 21:49:08 +02:00
[management] Resolve agent network permissions per submodule (#7030)
## Describe your changes Agent Network gates providers, policies, guardrails, budgets, usage, access logs, and settings behind the single `agent_network` permission module, so access is all-or-nothing: a future delegated role cannot be scoped to a subset of the area (for example usage-only visibility). This introduces dotted submodules (`agent_network.providers`, `.policies`, `.guardrails`, `.budgets`, `.usage`, `.logs`, `.settings`) and resolves grants with a cascade: exact module first, then its parent, then the role's `AutoAllowNew` default. The agent network manager now validates each operation against its matching submodule. `usage` (aggregated counters, overview) is deliberately separate from `logs` (request-level entries, which can contain captured prompts). No role definitions change. No built-in role carries an explicit `agent_network` entry, so every role resolves the submodules exactly as it resolved the parent module before — pinned by a test that compares each built-in role's answer on every submodule against its answer on `agent_network`. Role additions that use these submodules come separately.
This commit is contained in:
@@ -157,14 +157,14 @@ func NewManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) GetAllProviders(ctx context.Context, accountID, userID string) ([]*types.Provider, error) {
|
func (m *managerImpl) GetAllProviders(ctx context.Context, accountID, userID string) ([]*types.Provider, error) {
|
||||||
if err := m.requirePermission(ctx, accountID, userID, operations.Read); err != nil {
|
if err := m.requirePermission(ctx, accountID, userID, modules.AgentNetworkProviders, operations.Read); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return m.store.GetAccountAgentNetworkProviders(ctx, store.LockingStrengthNone, accountID)
|
return m.store.GetAccountAgentNetworkProviders(ctx, store.LockingStrengthNone, accountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) GetProvider(ctx context.Context, accountID, userID, providerID string) (*types.Provider, error) {
|
func (m *managerImpl) GetProvider(ctx context.Context, accountID, userID, providerID string) (*types.Provider, error) {
|
||||||
if err := m.requirePermission(ctx, accountID, userID, operations.Read); err != nil {
|
if err := m.requirePermission(ctx, accountID, userID, modules.AgentNetworkProviders, operations.Read); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return m.store.GetAgentNetworkProviderByID(ctx, store.LockingStrengthNone, accountID, providerID)
|
return m.store.GetAgentNetworkProviderByID(ctx, store.LockingStrengthNone, accountID, providerID)
|
||||||
@@ -175,9 +175,14 @@ func (m *managerImpl) GetProvider(ctx context.Context, accountID, userID, provid
|
|||||||
// been created yet; otherwise it is ignored (the cluster is pinned on
|
// been created yet; otherwise it is ignored (the cluster is pinned on
|
||||||
// Settings and every provider in the account routes through it).
|
// Settings and every provider in the account routes through it).
|
||||||
func (m *managerImpl) CreateProvider(ctx context.Context, userID string, provider *types.Provider, bootstrapCluster string) (*types.Provider, error) {
|
func (m *managerImpl) CreateProvider(ctx context.Context, userID string, provider *types.Provider, bootstrapCluster string) (*types.Provider, error) {
|
||||||
if err := m.requirePermission(ctx, provider.AccountID, userID, operations.Create); err != nil {
|
if err := m.requirePermission(ctx, provider.AccountID, userID, modules.AgentNetworkProviders, operations.Create); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(bootstrapCluster) != "" {
|
||||||
|
if err := m.requireSettingsBootstrapPermission(ctx, provider.AccountID, userID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// An empty api_key would silently produce a synthesised service
|
// An empty api_key would silently produce a synthesised service
|
||||||
// that 401s on every upstream request. Surface the misconfiguration
|
// that 401s on every upstream request. Surface the misconfiguration
|
||||||
@@ -218,7 +223,7 @@ func (m *managerImpl) CreateProvider(ctx context.Context, userID string, provide
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) UpdateProvider(ctx context.Context, userID string, provider *types.Provider) (*types.Provider, error) {
|
func (m *managerImpl) UpdateProvider(ctx context.Context, userID string, provider *types.Provider) (*types.Provider, error) {
|
||||||
if err := m.requirePermission(ctx, provider.AccountID, userID, operations.Update); err != nil {
|
if err := m.requirePermission(ctx, provider.AccountID, userID, modules.AgentNetworkProviders, operations.Update); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,7 +262,7 @@ func (m *managerImpl) UpdateProvider(ctx context.Context, userID string, provide
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) DeleteProvider(ctx context.Context, accountID, userID, providerID string) error {
|
func (m *managerImpl) DeleteProvider(ctx context.Context, accountID, userID, providerID string) error {
|
||||||
if err := m.requirePermission(ctx, accountID, userID, operations.Delete); err != nil {
|
if err := m.requirePermission(ctx, accountID, userID, modules.AgentNetworkProviders, operations.Delete); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,21 +311,21 @@ func pluralize(n int, singular, plural string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) GetAllPolicies(ctx context.Context, accountID, userID string) ([]*types.Policy, error) {
|
func (m *managerImpl) GetAllPolicies(ctx context.Context, accountID, userID string) ([]*types.Policy, error) {
|
||||||
if err := m.requirePermission(ctx, accountID, userID, operations.Read); err != nil {
|
if err := m.requirePermission(ctx, accountID, userID, modules.AgentNetworkPolicies, operations.Read); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return m.store.GetAccountAgentNetworkPolicies(ctx, store.LockingStrengthNone, accountID)
|
return m.store.GetAccountAgentNetworkPolicies(ctx, store.LockingStrengthNone, accountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) GetPolicy(ctx context.Context, accountID, userID, policyID string) (*types.Policy, error) {
|
func (m *managerImpl) GetPolicy(ctx context.Context, accountID, userID, policyID string) (*types.Policy, error) {
|
||||||
if err := m.requirePermission(ctx, accountID, userID, operations.Read); err != nil {
|
if err := m.requirePermission(ctx, accountID, userID, modules.AgentNetworkPolicies, operations.Read); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return m.store.GetAgentNetworkPolicyByID(ctx, store.LockingStrengthNone, accountID, policyID)
|
return m.store.GetAgentNetworkPolicyByID(ctx, store.LockingStrengthNone, accountID, policyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) CreatePolicy(ctx context.Context, userID string, policy *types.Policy) (*types.Policy, error) {
|
func (m *managerImpl) CreatePolicy(ctx context.Context, userID string, policy *types.Policy) (*types.Policy, error) {
|
||||||
if err := m.requirePermission(ctx, policy.AccountID, userID, operations.Create); err != nil {
|
if err := m.requirePermission(ctx, policy.AccountID, userID, modules.AgentNetworkPolicies, operations.Create); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +351,7 @@ func (m *managerImpl) CreatePolicy(ctx context.Context, userID string, policy *t
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) UpdatePolicy(ctx context.Context, userID string, policy *types.Policy) (*types.Policy, error) {
|
func (m *managerImpl) UpdatePolicy(ctx context.Context, userID string, policy *types.Policy) (*types.Policy, error) {
|
||||||
if err := m.requirePermission(ctx, policy.AccountID, userID, operations.Update); err != nil {
|
if err := m.requirePermission(ctx, policy.AccountID, userID, modules.AgentNetworkPolicies, operations.Update); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +378,7 @@ func (m *managerImpl) UpdatePolicy(ctx context.Context, userID string, policy *t
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) DeletePolicy(ctx context.Context, accountID, userID, policyID string) error {
|
func (m *managerImpl) DeletePolicy(ctx context.Context, accountID, userID, policyID string) error {
|
||||||
if err := m.requirePermission(ctx, accountID, userID, operations.Delete); err != nil {
|
if err := m.requirePermission(ctx, accountID, userID, modules.AgentNetworkPolicies, operations.Delete); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,21 +398,21 @@ func (m *managerImpl) DeletePolicy(ctx context.Context, accountID, userID, polic
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) GetAllGuardrails(ctx context.Context, accountID, userID string) ([]*types.Guardrail, error) {
|
func (m *managerImpl) GetAllGuardrails(ctx context.Context, accountID, userID string) ([]*types.Guardrail, error) {
|
||||||
if err := m.requirePermission(ctx, accountID, userID, operations.Read); err != nil {
|
if err := m.requirePermission(ctx, accountID, userID, modules.AgentNetworkGuardrails, operations.Read); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return m.store.GetAccountAgentNetworkGuardrails(ctx, store.LockingStrengthNone, accountID)
|
return m.store.GetAccountAgentNetworkGuardrails(ctx, store.LockingStrengthNone, accountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) GetGuardrail(ctx context.Context, accountID, userID, guardrailID string) (*types.Guardrail, error) {
|
func (m *managerImpl) GetGuardrail(ctx context.Context, accountID, userID, guardrailID string) (*types.Guardrail, error) {
|
||||||
if err := m.requirePermission(ctx, accountID, userID, operations.Read); err != nil {
|
if err := m.requirePermission(ctx, accountID, userID, modules.AgentNetworkGuardrails, operations.Read); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return m.store.GetAgentNetworkGuardrailByID(ctx, store.LockingStrengthNone, accountID, guardrailID)
|
return m.store.GetAgentNetworkGuardrailByID(ctx, store.LockingStrengthNone, accountID, guardrailID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) CreateGuardrail(ctx context.Context, userID string, guardrail *types.Guardrail) (*types.Guardrail, error) {
|
func (m *managerImpl) CreateGuardrail(ctx context.Context, userID string, guardrail *types.Guardrail) (*types.Guardrail, error) {
|
||||||
if err := m.requirePermission(ctx, guardrail.AccountID, userID, operations.Create); err != nil {
|
if err := m.requirePermission(ctx, guardrail.AccountID, userID, modules.AgentNetworkGuardrails, operations.Create); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,7 +434,7 @@ func (m *managerImpl) CreateGuardrail(ctx context.Context, userID string, guardr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) UpdateGuardrail(ctx context.Context, userID string, guardrail *types.Guardrail) (*types.Guardrail, error) {
|
func (m *managerImpl) UpdateGuardrail(ctx context.Context, userID string, guardrail *types.Guardrail) (*types.Guardrail, error) {
|
||||||
if err := m.requirePermission(ctx, guardrail.AccountID, userID, operations.Update); err != nil {
|
if err := m.requirePermission(ctx, guardrail.AccountID, userID, modules.AgentNetworkGuardrails, operations.Update); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,7 +457,7 @@ func (m *managerImpl) UpdateGuardrail(ctx context.Context, userID string, guardr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) DeleteGuardrail(ctx context.Context, accountID, userID, guardrailID string) error {
|
func (m *managerImpl) DeleteGuardrail(ctx context.Context, accountID, userID, guardrailID string) error {
|
||||||
if err := m.requirePermission(ctx, accountID, userID, operations.Delete); err != nil {
|
if err := m.requirePermission(ctx, accountID, userID, modules.AgentNetworkGuardrails, operations.Delete); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,7 +478,7 @@ func (m *managerImpl) DeleteGuardrail(ctx context.Context, accountID, userID, gu
|
|||||||
|
|
||||||
// GetAllBudgetRules returns every account-level budget rule for the account.
|
// GetAllBudgetRules returns every account-level budget rule for the account.
|
||||||
func (m *managerImpl) GetAllBudgetRules(ctx context.Context, accountID, userID string) ([]*types.AccountBudgetRule, error) {
|
func (m *managerImpl) GetAllBudgetRules(ctx context.Context, accountID, userID string) ([]*types.AccountBudgetRule, error) {
|
||||||
if err := m.requirePermission(ctx, accountID, userID, operations.Read); err != nil {
|
if err := m.requirePermission(ctx, accountID, userID, modules.AgentNetworkBudgets, operations.Read); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return m.store.GetAccountAgentNetworkBudgetRules(ctx, store.LockingStrengthNone, accountID)
|
return m.store.GetAccountAgentNetworkBudgetRules(ctx, store.LockingStrengthNone, accountID)
|
||||||
@@ -481,7 +486,7 @@ func (m *managerImpl) GetAllBudgetRules(ctx context.Context, accountID, userID s
|
|||||||
|
|
||||||
// GetBudgetRule returns a single account-level budget rule.
|
// GetBudgetRule returns a single account-level budget rule.
|
||||||
func (m *managerImpl) GetBudgetRule(ctx context.Context, accountID, userID, ruleID string) (*types.AccountBudgetRule, error) {
|
func (m *managerImpl) GetBudgetRule(ctx context.Context, accountID, userID, ruleID string) (*types.AccountBudgetRule, error) {
|
||||||
if err := m.requirePermission(ctx, accountID, userID, operations.Read); err != nil {
|
if err := m.requirePermission(ctx, accountID, userID, modules.AgentNetworkBudgets, operations.Read); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return m.store.GetAgentNetworkBudgetRuleByID(ctx, store.LockingStrengthNone, accountID, ruleID)
|
return m.store.GetAgentNetworkBudgetRuleByID(ctx, store.LockingStrengthNone, accountID, ruleID)
|
||||||
@@ -491,7 +496,7 @@ func (m *managerImpl) GetBudgetRule(ctx context.Context, accountID, userID, rule
|
|||||||
// enforced at request time (CheckLLMPolicyLimits), not baked into the synth
|
// enforced at request time (CheckLLMPolicyLimits), not baked into the synth
|
||||||
// proxy config, so no reconcile is needed.
|
// proxy config, so no reconcile is needed.
|
||||||
func (m *managerImpl) CreateBudgetRule(ctx context.Context, userID string, rule *types.AccountBudgetRule) (*types.AccountBudgetRule, error) {
|
func (m *managerImpl) CreateBudgetRule(ctx context.Context, userID string, rule *types.AccountBudgetRule) (*types.AccountBudgetRule, error) {
|
||||||
if err := m.requirePermission(ctx, rule.AccountID, userID, operations.Create); err != nil {
|
if err := m.requirePermission(ctx, rule.AccountID, userID, modules.AgentNetworkBudgets, operations.Create); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,7 +518,7 @@ func (m *managerImpl) CreateBudgetRule(ctx context.Context, userID string, rule
|
|||||||
|
|
||||||
// UpdateBudgetRule updates an existing account-level budget rule.
|
// UpdateBudgetRule updates an existing account-level budget rule.
|
||||||
func (m *managerImpl) UpdateBudgetRule(ctx context.Context, userID string, rule *types.AccountBudgetRule) (*types.AccountBudgetRule, error) {
|
func (m *managerImpl) UpdateBudgetRule(ctx context.Context, userID string, rule *types.AccountBudgetRule) (*types.AccountBudgetRule, error) {
|
||||||
if err := m.requirePermission(ctx, rule.AccountID, userID, operations.Update); err != nil {
|
if err := m.requirePermission(ctx, rule.AccountID, userID, modules.AgentNetworkBudgets, operations.Update); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,7 +541,7 @@ func (m *managerImpl) UpdateBudgetRule(ctx context.Context, userID string, rule
|
|||||||
|
|
||||||
// DeleteBudgetRule removes an account-level budget rule.
|
// DeleteBudgetRule removes an account-level budget rule.
|
||||||
func (m *managerImpl) DeleteBudgetRule(ctx context.Context, accountID, userID, ruleID string) error {
|
func (m *managerImpl) DeleteBudgetRule(ctx context.Context, accountID, userID, ruleID string) error {
|
||||||
if err := m.requirePermission(ctx, accountID, userID, operations.Delete); err != nil {
|
if err := m.requirePermission(ctx, accountID, userID, modules.AgentNetworkBudgets, operations.Delete); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,7 +566,7 @@ func (m *managerImpl) DeleteBudgetRule(ctx context.Context, accountID, userID, r
|
|||||||
// gating, access-log emission), a reconcile is triggered so the proxy and peer
|
// gating, access-log emission), a reconcile is triggered so the proxy and peer
|
||||||
// network maps converge on the new state.
|
// network maps converge on the new state.
|
||||||
func (m *managerImpl) UpdateSettings(ctx context.Context, userID string, settings *types.Settings) (*types.Settings, error) {
|
func (m *managerImpl) UpdateSettings(ctx context.Context, userID string, settings *types.Settings) (*types.Settings, error) {
|
||||||
if err := m.requirePermission(ctx, settings.AccountID, userID, operations.Update); err != nil {
|
if err := m.requirePermission(ctx, settings.AccountID, userID, modules.AgentNetworkSettings, operations.Update); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,7 +620,7 @@ func (m *managerImpl) validateProviderRefs(ctx context.Context, accountID string
|
|||||||
// Returns the underlying status.NotFound when no row has been
|
// Returns the underlying status.NotFound when no row has been
|
||||||
// bootstrapped yet (i.e. the account has no providers).
|
// bootstrapped yet (i.e. the account has no providers).
|
||||||
func (m *managerImpl) GetSettings(ctx context.Context, accountID, userID string) (*types.Settings, error) {
|
func (m *managerImpl) GetSettings(ctx context.Context, accountID, userID string) (*types.Settings, error) {
|
||||||
if err := m.requirePermission(ctx, accountID, userID, operations.Read); err != nil {
|
if err := m.requirePermission(ctx, accountID, userID, modules.AgentNetworkSettings, operations.Read); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return m.store.GetAgentNetworkSettings(ctx, store.LockingStrengthNone, accountID)
|
return m.store.GetAgentNetworkSettings(ctx, store.LockingStrengthNone, accountID)
|
||||||
@@ -627,6 +632,22 @@ func (m *managerImpl) GetSettings(ctx context.Context, accountID, userID string)
|
|||||||
// the subdomain is picked from the curated wordlist avoiding
|
// the subdomain is picked from the curated wordlist avoiding
|
||||||
// collisions on the same cluster. Idempotent: if a row already exists
|
// collisions on the same cluster. Idempotent: if a row already exists
|
||||||
// it is returned untouched and the hint is ignored.
|
// it is returned untouched and the hint is ignored.
|
||||||
|
// requireSettingsBootstrapPermission gates the one-time settings bootstrap a
|
||||||
|
// first provider create performs. Pinning the account's cluster and subdomain
|
||||||
|
// is a settings write, so it needs the settings permission on top of the
|
||||||
|
// provider one. No-op once the settings row exists.
|
||||||
|
func (m *managerImpl) requireSettingsBootstrapPermission(ctx context.Context, accountID, userID string) error {
|
||||||
|
_, err := m.store.GetAgentNetworkSettings(ctx, store.LockingStrengthNone, accountID)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var sErr *status.Error
|
||||||
|
if !errors.As(err, &sErr) || sErr.Type() != status.NotFound {
|
||||||
|
return fmt.Errorf("get agent network settings: %w", err)
|
||||||
|
}
|
||||||
|
return m.requirePermission(ctx, accountID, userID, modules.AgentNetworkSettings, operations.Create)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *managerImpl) bootstrapSettingsIfNeeded(ctx context.Context, accountID, providerCluster string) (*types.Settings, error) {
|
func (m *managerImpl) bootstrapSettingsIfNeeded(ctx context.Context, accountID, providerCluster string) (*types.Settings, error) {
|
||||||
if accountID == "" {
|
if accountID == "" {
|
||||||
return nil, fmt.Errorf("bootstrap settings: account id is required")
|
return nil, fmt.Errorf("bootstrap settings: account id is required")
|
||||||
@@ -685,7 +706,7 @@ func (m *managerImpl) bootstrapSettingsIfNeeded(ctx context.Context, accountID,
|
|||||||
// counter view; permission gate is the same Read role that gates
|
// counter view; permission gate is the same Read role that gates
|
||||||
// every other agent-network surface.
|
// every other agent-network surface.
|
||||||
func (m *managerImpl) ListConsumption(ctx context.Context, accountID, userID string) ([]*types.Consumption, error) {
|
func (m *managerImpl) ListConsumption(ctx context.Context, accountID, userID string) ([]*types.Consumption, error) {
|
||||||
if err := m.requirePermission(ctx, accountID, userID, operations.Read); err != nil {
|
if err := m.requirePermission(ctx, accountID, userID, modules.AgentNetworkUsage, operations.Read); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return m.store.ListAgentNetworkConsumption(ctx, store.LockingStrengthNone, accountID)
|
return m.store.ListAgentNetworkConsumption(ctx, store.LockingStrengthNone, accountID)
|
||||||
@@ -694,7 +715,7 @@ func (m *managerImpl) ListConsumption(ctx context.Context, accountID, userID str
|
|||||||
// ListAccessLogs returns a paginated, server-side-filtered page of
|
// ListAccessLogs returns a paginated, server-side-filtered page of
|
||||||
// agent-network access logs plus the total count matching the filter.
|
// agent-network access logs plus the total count matching the filter.
|
||||||
func (m *managerImpl) ListAccessLogs(ctx context.Context, accountID, userID string, filter types.AgentNetworkAccessLogFilter) ([]*types.AgentNetworkAccessLog, int64, error) {
|
func (m *managerImpl) ListAccessLogs(ctx context.Context, accountID, userID string, filter types.AgentNetworkAccessLogFilter) ([]*types.AgentNetworkAccessLog, int64, error) {
|
||||||
if err := m.requirePermission(ctx, accountID, userID, operations.Read); err != nil {
|
if err := m.requirePermission(ctx, accountID, userID, modules.AgentNetworkLogs, operations.Read); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
return m.store.GetAgentNetworkAccessLogs(ctx, store.LockingStrengthNone, accountID, filter)
|
return m.store.GetAgentNetworkAccessLogs(ctx, store.LockingStrengthNone, accountID, filter)
|
||||||
@@ -704,7 +725,7 @@ func (m *managerImpl) ListAccessLogs(ctx context.Context, accountID, userID stri
|
|||||||
// agent-network access logs grouped by session, plus the total number of
|
// agent-network access logs grouped by session, plus the total number of
|
||||||
// sessions matching the filter.
|
// sessions matching the filter.
|
||||||
func (m *managerImpl) ListAccessLogSessions(ctx context.Context, accountID, userID string, filter types.AgentNetworkAccessLogFilter) ([]*types.AgentNetworkAccessLogSession, int64, error) {
|
func (m *managerImpl) ListAccessLogSessions(ctx context.Context, accountID, userID string, filter types.AgentNetworkAccessLogFilter) ([]*types.AgentNetworkAccessLogSession, int64, error) {
|
||||||
if err := m.requirePermission(ctx, accountID, userID, operations.Read); err != nil {
|
if err := m.requirePermission(ctx, accountID, userID, modules.AgentNetworkLogs, operations.Read); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
return m.store.GetAgentNetworkAccessLogSessions(ctx, store.LockingStrengthNone, accountID, filter)
|
return m.store.GetAgentNetworkAccessLogSessions(ctx, store.LockingStrengthNone, accountID, filter)
|
||||||
@@ -713,7 +734,7 @@ func (m *managerImpl) ListAccessLogSessions(ctx context.Context, accountID, user
|
|||||||
// GetUsageOverview returns the filtered usage rows aggregated into time buckets
|
// GetUsageOverview returns the filtered usage rows aggregated into time buckets
|
||||||
// at the requested granularity, oldest-first.
|
// at the requested granularity, oldest-first.
|
||||||
func (m *managerImpl) GetUsageOverview(ctx context.Context, accountID, userID string, filter types.AgentNetworkAccessLogFilter, granularity types.UsageGranularity) ([]*types.AgentNetworkUsageBucket, error) {
|
func (m *managerImpl) GetUsageOverview(ctx context.Context, accountID, userID string, filter types.AgentNetworkAccessLogFilter, granularity types.UsageGranularity) ([]*types.AgentNetworkUsageBucket, error) {
|
||||||
if err := m.requirePermission(ctx, accountID, userID, operations.Read); err != nil {
|
if err := m.requirePermission(ctx, accountID, userID, modules.AgentNetworkUsage, operations.Read); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
rows, err := m.store.GetAgentNetworkUsageRows(ctx, store.LockingStrengthNone, accountID, filter)
|
rows, err := m.store.GetAgentNetworkUsageRows(ctx, store.LockingStrengthNone, accountID, filter)
|
||||||
@@ -787,8 +808,8 @@ func (m *managerImpl) RecordConsumption(ctx context.Context, accountID string, k
|
|||||||
return m.store.IncrementAgentNetworkConsumption(ctx, accountID, kind, dimID, windowSeconds, windowStart, tokensIn, tokensOut, costUSD)
|
return m.store.IncrementAgentNetworkConsumption(ctx, accountID, kind, dimID, windowSeconds, windowStart, tokensIn, tokensOut, costUSD)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) requirePermission(ctx context.Context, accountID, userID string, op operations.Operation) error {
|
func (m *managerImpl) requirePermission(ctx context.Context, accountID, userID string, module modules.Module, op operations.Operation) error {
|
||||||
ok, _, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.AgentNetwork, op)
|
ok, _, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, module, op)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.NewPermissionValidationError(err)
|
return status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package agentnetwork
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
|
||||||
|
"github.com/netbirdio/netbird/management/server/account"
|
||||||
|
"github.com/netbirdio/netbird/management/server/permissions"
|
||||||
|
"github.com/netbirdio/netbird/management/server/permissions/modules"
|
||||||
|
"github.com/netbirdio/netbird/management/server/permissions/operations"
|
||||||
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
|
nbtypes "github.com/netbirdio/netbird/management/server/types"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// bootstrapFixture wires a real sqlite store to a gomock permissions manager
|
||||||
|
// so tests can grant the provider permission while denying (or never
|
||||||
|
// expecting) the settings one.
|
||||||
|
type bootstrapFixture struct {
|
||||||
|
manager Manager
|
||||||
|
store store.Store
|
||||||
|
perms *permissions.MockManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBootstrapFixture(t *testing.T) *bootstrapFixture {
|
||||||
|
t.Helper()
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("sqlite store not properly supported on Windows yet")
|
||||||
|
}
|
||||||
|
t.Setenv("NETBIRD_STORE_ENGINE", string(nbtypes.SqliteStoreEngine))
|
||||||
|
|
||||||
|
st, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
|
||||||
|
require.NoError(t, err, "test store setup must succeed")
|
||||||
|
t.Cleanup(cleanUp)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
perms := permissions.NewMockManager(ctrl)
|
||||||
|
|
||||||
|
accounts := account.NewMockManager(ctrl)
|
||||||
|
accounts.EXPECT().StoreEvent(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
accounts.EXPECT().UpdateAccountPeers(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
accounts.EXPECT().BufferUpdateAccountPeers(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
|
||||||
|
return &bootstrapFixture{
|
||||||
|
manager: NewManager(st, perms, accounts, nil),
|
||||||
|
store: st,
|
||||||
|
perms: perms,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *bootstrapFixture) expectPermission(accountID, userID string, module modules.Module, op operations.Operation, allowed bool) {
|
||||||
|
f.perms.EXPECT().
|
||||||
|
ValidateUserPermissions(gomock.Any(), accountID, userID, module, op).
|
||||||
|
Return(allowed, context.Background(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBootstrapProvider(accountID string) *types.Provider {
|
||||||
|
p := types.NewProvider(accountID)
|
||||||
|
p.Name = "openai"
|
||||||
|
p.UpstreamURL = "https://api.openai.com"
|
||||||
|
p.APIKey = "sk-test"
|
||||||
|
p.Enabled = true
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCreateProviderBootstrapRequiresSettingsPermission pins the gate on the
|
||||||
|
// one-time settings bootstrap: creating the first provider with a
|
||||||
|
// bootstrap_cluster pins the account's cluster and subdomain, which is a
|
||||||
|
// settings write and must not ride on the providers permission alone.
|
||||||
|
func TestCreateProviderBootstrapRequiresSettingsPermission(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("denied without settings permission", func(t *testing.T) {
|
||||||
|
f := newBootstrapFixture(t)
|
||||||
|
f.expectPermission("account1", "user1", modules.AgentNetworkProviders, operations.Create, true)
|
||||||
|
f.expectPermission("account1", "user1", modules.AgentNetworkSettings, operations.Create, false)
|
||||||
|
|
||||||
|
_, err := f.manager.CreateProvider(ctx, "user1", newBootstrapProvider("account1"), "cluster1.example.com")
|
||||||
|
require.Error(t, err, "bootstrap without settings permission must fail")
|
||||||
|
var sErr *status.Error
|
||||||
|
require.ErrorAs(t, err, &sErr)
|
||||||
|
assert.Equal(t, status.PermissionDenied, sErr.Type(), "denial should surface as permission denied")
|
||||||
|
|
||||||
|
providers, err := f.store.GetAccountAgentNetworkProviders(ctx, store.LockingStrengthNone, "account1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, providers, "provider must not be persisted when bootstrap is denied")
|
||||||
|
_, err = f.store.GetAgentNetworkSettings(ctx, store.LockingStrengthNone, "account1")
|
||||||
|
assert.Error(t, err, "settings row must not be created when bootstrap is denied")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("allowed with settings permission", func(t *testing.T) {
|
||||||
|
f := newBootstrapFixture(t)
|
||||||
|
f.expectPermission("account1", "user1", modules.AgentNetworkProviders, operations.Create, true)
|
||||||
|
f.expectPermission("account1", "user1", modules.AgentNetworkSettings, operations.Create, true)
|
||||||
|
|
||||||
|
created, err := f.manager.CreateProvider(ctx, "user1", newBootstrapProvider("account1"), "cluster1.example.com")
|
||||||
|
require.NoError(t, err, "bootstrap with both permissions must succeed")
|
||||||
|
require.NotNil(t, created)
|
||||||
|
|
||||||
|
settings, err := f.store.GetAgentNetworkSettings(ctx, store.LockingStrengthNone, "account1")
|
||||||
|
require.NoError(t, err, "bootstrap must create the settings row")
|
||||||
|
assert.Equal(t, "cluster1.example.com", settings.Cluster, "settings should pin the bootstrap cluster")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("existing settings need no settings permission", func(t *testing.T) {
|
||||||
|
f := newBootstrapFixture(t)
|
||||||
|
require.NoError(t, f.store.SaveAgentNetworkSettings(ctx, &types.Settings{
|
||||||
|
AccountID: "account1",
|
||||||
|
Cluster: "cluster1.example.com",
|
||||||
|
Subdomain: "existing",
|
||||||
|
}), "pre-existing settings row setup must succeed")
|
||||||
|
|
||||||
|
// Only the providers permission may be consulted: gomock fails the
|
||||||
|
// test on any unexpected settings-permission call.
|
||||||
|
f.expectPermission("account1", "user1", modules.AgentNetworkProviders, operations.Create, true)
|
||||||
|
|
||||||
|
_, err := f.manager.CreateProvider(ctx, "user1", newBootstrapProvider("account1"), "cluster1.example.com")
|
||||||
|
require.NoError(t, err, "create with existing settings must not require the settings permission")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no bootstrap cluster needs no settings permission", func(t *testing.T) {
|
||||||
|
f := newBootstrapFixture(t)
|
||||||
|
f.expectPermission("account1", "user1", modules.AgentNetworkProviders, operations.Create, true)
|
||||||
|
|
||||||
|
_, err := f.manager.CreateProvider(ctx, "user1", newBootstrapProvider("account1"), "")
|
||||||
|
require.NoError(t, err, "create without bootstrap must not require the settings permission")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -82,6 +82,9 @@ func (m *managerImpl) ValidateUserPermissions(
|
|||||||
return m.ValidateRoleModuleAccess(ctx, accountID, role, module, operation), ctxEnriched, nil
|
return m.ValidateRoleModuleAccess(ctx, accountID, role, module, operation), ctxEnriched, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateRoleModuleAccess resolves an operation against the role's explicit
|
||||||
|
// grant for the module, then the grant for its parent module when the module
|
||||||
|
// is a dotted submodule, and finally the role's AutoAllowNew default.
|
||||||
func (m *managerImpl) ValidateRoleModuleAccess(
|
func (m *managerImpl) ValidateRoleModuleAccess(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
accountID string,
|
accountID string,
|
||||||
@@ -89,7 +92,7 @@ func (m *managerImpl) ValidateRoleModuleAccess(
|
|||||||
module modules.Module,
|
module modules.Module,
|
||||||
operation operations.Operation,
|
operation operations.Operation,
|
||||||
) bool {
|
) bool {
|
||||||
if permissions, ok := role.Permissions[module]; ok {
|
if permissions, ok := lookupModulePermissions(role, module); ok {
|
||||||
if allowed, exists := permissions[operation]; exists {
|
if allowed, exists := permissions[operation]; exists {
|
||||||
return allowed
|
return allowed
|
||||||
}
|
}
|
||||||
@@ -100,6 +103,21 @@ func (m *managerImpl) ValidateRoleModuleAccess(
|
|||||||
return role.AutoAllowNew[operation]
|
return role.AutoAllowNew[operation]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lookupModulePermissions returns the role's explicit permission set for the
|
||||||
|
// module, falling back to the parent module's set for dotted submodules. The
|
||||||
|
// second return reports whether any explicit set was found.
|
||||||
|
func lookupModulePermissions(role roles.RolePermissions, module modules.Module) (map[operations.Operation]bool, bool) {
|
||||||
|
if permissions, ok := role.Permissions[module]; ok {
|
||||||
|
return permissions, true
|
||||||
|
}
|
||||||
|
if parent, hasParent := module.Parent(); hasParent {
|
||||||
|
if permissions, ok := role.Permissions[parent]; ok {
|
||||||
|
return permissions, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
func (m *managerImpl) ValidateAccountAccess(ctx context.Context, accountID string, user *types.User, allowOwnerAndAdmin bool) (context.Context, error) {
|
func (m *managerImpl) ValidateAccountAccess(ctx context.Context, accountID string, user *types.User, allowOwnerAndAdmin bool) (context.Context, error) {
|
||||||
if user.AccountID != accountID {
|
if user.AccountID != accountID {
|
||||||
return ctx, status.NewUserNotPartOfAccountError()
|
return ctx, status.NewUserNotPartOfAccountError()
|
||||||
@@ -119,7 +137,7 @@ func (m *managerImpl) GetPermissionsByRole(ctx context.Context, role types.UserR
|
|||||||
permissions := roles.Permissions{}
|
permissions := roles.Permissions{}
|
||||||
|
|
||||||
for k := range modules.All {
|
for k := range modules.All {
|
||||||
if rolePermissions, ok := roleMap.Permissions[k]; ok {
|
if rolePermissions, ok := lookupModulePermissions(roleMap, k); ok {
|
||||||
permissions[k] = rolePermissions
|
permissions[k] = rolePermissions
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package permissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/permissions/modules"
|
||||||
|
"github.com/netbirdio/netbird/management/server/permissions/operations"
|
||||||
|
"github.com/netbirdio/netbird/management/server/permissions/roles"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateRoleModuleAccessSubmoduleCascade(t *testing.T) {
|
||||||
|
manager := NewManager(nil)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
fullAccess := map[operations.Operation]bool{
|
||||||
|
operations.Read: true,
|
||||||
|
operations.Create: true,
|
||||||
|
operations.Update: true,
|
||||||
|
operations.Delete: true,
|
||||||
|
}
|
||||||
|
readOnly := map[operations.Operation]bool{
|
||||||
|
operations.Read: true,
|
||||||
|
operations.Create: false,
|
||||||
|
operations.Update: false,
|
||||||
|
operations.Delete: false,
|
||||||
|
}
|
||||||
|
denyAll := map[operations.Operation]bool{
|
||||||
|
operations.Read: false,
|
||||||
|
operations.Create: false,
|
||||||
|
operations.Update: false,
|
||||||
|
operations.Delete: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("parent grant covers submodules", func(t *testing.T) {
|
||||||
|
role := roles.RolePermissions{
|
||||||
|
AutoAllowNew: denyAll,
|
||||||
|
Permissions: roles.Permissions{modules.AgentNetwork: fullAccess},
|
||||||
|
}
|
||||||
|
assert.True(t, manager.ValidateRoleModuleAccess(ctx, "account", role, modules.AgentNetworkProviders, operations.Create),
|
||||||
|
"parent full grant should allow create on a submodule")
|
||||||
|
assert.True(t, manager.ValidateRoleModuleAccess(ctx, "account", role, modules.AgentNetworkLogs, operations.Read),
|
||||||
|
"parent full grant should allow read on a submodule")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("submodule grant does not leak to parent or siblings", func(t *testing.T) {
|
||||||
|
role := roles.RolePermissions{
|
||||||
|
AutoAllowNew: denyAll,
|
||||||
|
Permissions: roles.Permissions{modules.AgentNetworkUsage: readOnly},
|
||||||
|
}
|
||||||
|
assert.True(t, manager.ValidateRoleModuleAccess(ctx, "account", role, modules.AgentNetworkUsage, operations.Read),
|
||||||
|
"explicit submodule read should be allowed")
|
||||||
|
assert.False(t, manager.ValidateRoleModuleAccess(ctx, "account", role, modules.AgentNetworkUsage, operations.Create),
|
||||||
|
"read-only submodule grant should not allow create")
|
||||||
|
assert.False(t, manager.ValidateRoleModuleAccess(ctx, "account", role, modules.AgentNetwork, operations.Read),
|
||||||
|
"submodule grant should not grant the parent module")
|
||||||
|
assert.False(t, manager.ValidateRoleModuleAccess(ctx, "account", role, modules.AgentNetworkProviders, operations.Read),
|
||||||
|
"submodule grant should not grant a sibling submodule")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("explicit submodule entry wins over parent grant", func(t *testing.T) {
|
||||||
|
role := roles.RolePermissions{
|
||||||
|
AutoAllowNew: denyAll,
|
||||||
|
Permissions: roles.Permissions{
|
||||||
|
modules.AgentNetwork: fullAccess,
|
||||||
|
modules.AgentNetworkLogs: denyAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.False(t, manager.ValidateRoleModuleAccess(ctx, "account", role, modules.AgentNetworkLogs, operations.Read),
|
||||||
|
"explicit submodule deny should override the parent grant")
|
||||||
|
assert.True(t, manager.ValidateRoleModuleAccess(ctx, "account", role, modules.AgentNetworkUsage, operations.Read),
|
||||||
|
"sibling submodules should still resolve through the parent grant")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("auto allow applies when neither submodule nor parent is granted", func(t *testing.T) {
|
||||||
|
role := roles.RolePermissions{
|
||||||
|
AutoAllowNew: readOnly,
|
||||||
|
}
|
||||||
|
assert.True(t, manager.ValidateRoleModuleAccess(ctx, "account", role, modules.AgentNetworkProviders, operations.Read),
|
||||||
|
"auto-allow read should apply to submodules")
|
||||||
|
assert.False(t, manager.ValidateRoleModuleAccess(ctx, "account", role, modules.AgentNetworkProviders, operations.Delete),
|
||||||
|
"auto-allow should not grant unlisted operations")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExistingRolesKeepAgentNetworkBehaviorOnSubmodules pins the behavior the
|
||||||
|
// submodule split must not change: every built-in role resolves the new
|
||||||
|
// submodules exactly as it resolved the agent_network module before.
|
||||||
|
func TestExistingRolesKeepAgentNetworkBehaviorOnSubmodules(t *testing.T) {
|
||||||
|
manager := NewManager(nil)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
submodules := []modules.Module{
|
||||||
|
modules.AgentNetworkProviders,
|
||||||
|
modules.AgentNetworkPolicies,
|
||||||
|
modules.AgentNetworkGuardrails,
|
||||||
|
modules.AgentNetworkBudgets,
|
||||||
|
modules.AgentNetworkUsage,
|
||||||
|
modules.AgentNetworkLogs,
|
||||||
|
modules.AgentNetworkSettings,
|
||||||
|
}
|
||||||
|
allOperations := []operations.Operation{operations.Read, operations.Create, operations.Update, operations.Delete}
|
||||||
|
|
||||||
|
for _, role := range []types.UserRole{types.UserRoleOwner, types.UserRoleAdmin, types.UserRoleAuditor, types.UserRoleNetworkAdmin, types.UserRoleUser} {
|
||||||
|
rolePermissions, ok := roles.RolesMap[role]
|
||||||
|
require.True(t, ok, "role %s must exist in RolesMap", role)
|
||||||
|
|
||||||
|
for _, sub := range submodules {
|
||||||
|
for _, op := range allOperations {
|
||||||
|
expected := manager.ValidateRoleModuleAccess(ctx, "account", rolePermissions, modules.AgentNetwork, op)
|
||||||
|
actual := manager.ValidateRoleModuleAccess(ctx, "account", rolePermissions, sub, op)
|
||||||
|
assert.Equal(t, expected, actual, "role %s: %s on %s should match the agent_network module", role, op, sub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPermissionsByRoleIncludesSubmodules(t *testing.T) {
|
||||||
|
manager := NewManager(nil)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
permissions, err := manager.GetPermissionsByRole(ctx, types.UserRoleAuditor)
|
||||||
|
require.NoError(t, err, "auditor role must resolve")
|
||||||
|
|
||||||
|
usage, ok := permissions[modules.AgentNetworkUsage]
|
||||||
|
require.True(t, ok, "permissions map should contain the usage submodule")
|
||||||
|
assert.True(t, usage[operations.Read], "auditor should read the usage submodule")
|
||||||
|
assert.False(t, usage[operations.Update], "auditor should not update the usage submodule")
|
||||||
|
|
||||||
|
adminPermissions, err := manager.GetPermissionsByRole(ctx, types.UserRoleAdmin)
|
||||||
|
require.NoError(t, err, "admin role must resolve")
|
||||||
|
providers, ok := adminPermissions[modules.AgentNetworkProviders]
|
||||||
|
require.True(t, ok, "permissions map should contain the providers submodule")
|
||||||
|
assert.True(t, providers[operations.Delete], "admin should delete on the providers submodule")
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package modules
|
package modules
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
type Module string
|
type Module string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -20,6 +22,17 @@ const (
|
|||||||
IdentityProviders Module = "identity_providers"
|
IdentityProviders Module = "identity_providers"
|
||||||
Services Module = "services"
|
Services Module = "services"
|
||||||
AgentNetwork Module = "agent_network"
|
AgentNetwork Module = "agent_network"
|
||||||
|
|
||||||
|
// Agent Network submodules. A role may grant one of these directly
|
||||||
|
// or grant the AgentNetwork parent, which covers all of them (see
|
||||||
|
// permissions.Manager cascade resolution).
|
||||||
|
AgentNetworkProviders Module = "agent_network.providers"
|
||||||
|
AgentNetworkPolicies Module = "agent_network.policies"
|
||||||
|
AgentNetworkGuardrails Module = "agent_network.guardrails"
|
||||||
|
AgentNetworkBudgets Module = "agent_network.budgets"
|
||||||
|
AgentNetworkUsage Module = "agent_network.usage"
|
||||||
|
AgentNetworkLogs Module = "agent_network.logs"
|
||||||
|
AgentNetworkSettings Module = "agent_network.settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var All = map[Module]struct{}{
|
var All = map[Module]struct{}{
|
||||||
@@ -40,4 +53,21 @@ var All = map[Module]struct{}{
|
|||||||
IdentityProviders: {},
|
IdentityProviders: {},
|
||||||
Services: {},
|
Services: {},
|
||||||
AgentNetwork: {},
|
AgentNetwork: {},
|
||||||
|
|
||||||
|
AgentNetworkProviders: {},
|
||||||
|
AgentNetworkPolicies: {},
|
||||||
|
AgentNetworkGuardrails: {},
|
||||||
|
AgentNetworkBudgets: {},
|
||||||
|
AgentNetworkUsage: {},
|
||||||
|
AgentNetworkLogs: {},
|
||||||
|
AgentNetworkSettings: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent returns the module owning a dotted submodule name and true, or the
|
||||||
|
// module itself and false when it has no parent.
|
||||||
|
func (m Module) Parent() (Module, bool) {
|
||||||
|
if i := strings.IndexByte(string(m), '.'); i > 0 {
|
||||||
|
return Module(string(m)[:i]), true
|
||||||
|
}
|
||||||
|
return m, false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user