mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-04 19:55:09 -04:00
## 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.
135 lines
5.7 KiB
Go
135 lines
5.7 KiB
Go
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")
|
|
})
|
|
}
|