mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-01 20:41:28 +02:00
Delegating Agent Network today means handing out full account admin, and regular users cannot see their own usage or how to connect a local tool. Add two roles on top of the existing agent_network permission submodules. agent_network_admin owns the whole area (providers, policies, guardrails, budgets, usage, logs, settings) with read-only users, groups, peers, and account info needed to build policies, and nothing else in the account. usage_viewer is the regular User baseline plus read on the aggregated usage and cost overview: no provider configuration, no policies, no request-level logs, which can contain captured prompts. billing_admin gets a proper permission-map entry with the User baseline so role resolution stops failing with role-not-found; its plan and invoice permissions stay enforced cloud-side. Add the self-service endpoints behind the "My Agent Network" view, available to every authenticated user because both answers are scoped strictly to the caller. GET /api/agent-network/me/setup returns the account endpoint plus the providers and models the caller's own groups authorize, computed with the same rules the proxy enforces: policy filtering as in policy selection, model allowlist union intersected with declared models, orphan and disabled providers omitted. Not set up and no access are deliberately indistinguishable, and the response carries display metadata only. GET /api/agent-network/me/consumption returns the caller's own user-dimension counters.
141 lines
5.3 KiB
Go
141 lines
5.3 KiB
Go
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"
|
|
)
|
|
|
|
var allOps = []operations.Operation{operations.Read, operations.Create, operations.Update, operations.Delete}
|
|
|
|
// TestAgentNetworkAdminRole pins the delegated-admin contract: full control
|
|
// over the whole agent_network area (parent grant cascades to every
|
|
// submodule), read-only on the account objects needed to build policies,
|
|
// and nothing else in the account.
|
|
func TestAgentNetworkAdminRole(t *testing.T) {
|
|
manager := NewManager(nil)
|
|
ctx := context.Background()
|
|
|
|
role, ok := roles.RolesMap[types.UserRoleAgentNetworkAdmin]
|
|
require.True(t, ok, "agent_network_admin must exist in RolesMap")
|
|
|
|
agentNetworkModules := []modules.Module{
|
|
modules.AgentNetwork,
|
|
modules.AgentNetworkProviders,
|
|
modules.AgentNetworkPolicies,
|
|
modules.AgentNetworkGuardrails,
|
|
modules.AgentNetworkBudgets,
|
|
modules.AgentNetworkUsage,
|
|
modules.AgentNetworkLogs,
|
|
modules.AgentNetworkSettings,
|
|
}
|
|
for _, m := range agentNetworkModules {
|
|
for _, op := range allOps {
|
|
assert.True(t, manager.ValidateRoleModuleAccess(ctx, "account", role, m, op),
|
|
"agent_network_admin must have %s on %s", op, m)
|
|
}
|
|
}
|
|
|
|
// Settings read rides along because GET /api/accounts (which the
|
|
// dashboard needs to boot) validates it, like network_admin.
|
|
for _, m := range []modules.Module{modules.Users, modules.Groups, modules.Peers, modules.Accounts, modules.Settings} {
|
|
assert.True(t, manager.ValidateRoleModuleAccess(ctx, "account", role, m, operations.Read),
|
|
"agent_network_admin must read %s to build policies and load the dashboard", m)
|
|
for _, op := range []operations.Operation{operations.Create, operations.Update, operations.Delete} {
|
|
assert.False(t, manager.ValidateRoleModuleAccess(ctx, "account", role, m, op),
|
|
"agent_network_admin must not have %s on %s", op, m)
|
|
}
|
|
}
|
|
|
|
for _, m := range []modules.Module{modules.Networks, modules.Dns, modules.SetupKeys, modules.Routes} {
|
|
for _, op := range allOps {
|
|
assert.False(t, manager.ValidateRoleModuleAccess(ctx, "account", role, m, op),
|
|
"agent_network_admin must not have %s on %s", op, m)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestUsageViewerRole pins the least-privilege cost role: read on the
|
|
// aggregated usage overview plus read-only on the resources its filters
|
|
// and display columns resolve against (users, groups, peers, the provider
|
|
// list) — no policies, no request-level logs (which can contain captured
|
|
// prompts), nothing else in the account.
|
|
func TestUsageViewerRole(t *testing.T) {
|
|
manager := NewManager(nil)
|
|
ctx := context.Background()
|
|
|
|
role, ok := roles.RolesMap[types.UserRoleUsageViewer]
|
|
require.True(t, ok, "usage_viewer must exist in RolesMap")
|
|
|
|
readOnly := []modules.Module{
|
|
modules.AgentNetworkUsage,
|
|
modules.AgentNetworkProviders,
|
|
modules.Users,
|
|
modules.Groups,
|
|
modules.Peers,
|
|
}
|
|
for _, m := range readOnly {
|
|
assert.True(t, manager.ValidateRoleModuleAccess(ctx, "account", role, m, operations.Read),
|
|
"usage_viewer must read %s for the usage view and its filters", m)
|
|
for _, op := range []operations.Operation{operations.Create, operations.Update, operations.Delete} {
|
|
assert.False(t, manager.ValidateRoleModuleAccess(ctx, "account", role, m, op),
|
|
"usage_viewer must not have %s on %s", op, m)
|
|
}
|
|
}
|
|
|
|
denied := []modules.Module{
|
|
modules.AgentNetwork,
|
|
modules.AgentNetworkPolicies,
|
|
modules.AgentNetworkGuardrails,
|
|
modules.AgentNetworkBudgets,
|
|
modules.AgentNetworkLogs,
|
|
modules.AgentNetworkSettings,
|
|
modules.Networks,
|
|
modules.SetupKeys,
|
|
}
|
|
for _, m := range denied {
|
|
for _, op := range allOps {
|
|
assert.False(t, manager.ValidateRoleModuleAccess(ctx, "account", role, m, op),
|
|
"usage_viewer must not have %s on %s", op, m)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBillingAdminRoleResolves pins that billing_admin has a proper entry
|
|
// in the permission map. Its plan/seat/invoice permissions are enforced
|
|
// outside this map; management-side it carries the regular User baseline
|
|
// instead of failing role resolution.
|
|
func TestBillingAdminRoleResolves(t *testing.T) {
|
|
manager := NewManager(nil)
|
|
ctx := context.Background()
|
|
|
|
role, ok := roles.RolesMap[types.UserRoleBillingAdmin]
|
|
require.True(t, ok, "billing_admin must exist in RolesMap")
|
|
|
|
permissions, err := manager.GetPermissionsByRole(ctx, types.UserRoleBillingAdmin)
|
|
require.NoError(t, err, "billing_admin role must resolve")
|
|
require.NotEmpty(t, permissions)
|
|
|
|
for _, m := range []modules.Module{modules.AgentNetwork, modules.Networks, modules.Users, modules.Peers} {
|
|
for _, op := range allOps {
|
|
assert.False(t, manager.ValidateRoleModuleAccess(ctx, "account", role, m, op),
|
|
"billing_admin must not have %s on %s", op, m)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestNewRolesParse pins the API role strings, which are permanent once
|
|
// released.
|
|
func TestNewRolesParse(t *testing.T) {
|
|
assert.Equal(t, types.UserRoleAgentNetworkAdmin, types.StrRoleToUserRole("agent_network_admin"))
|
|
assert.Equal(t, types.UserRoleUsageViewer, types.StrRoleToUserRole("usage_viewer"))
|
|
assert.Equal(t, types.UserRoleBillingAdmin, types.StrRoleToUserRole("billing_admin"))
|
|
}
|