mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-26 08:39:06 +02:00
Merge main into embedded-vnc
# Conflicts: # client/internal/engine.go
This commit is contained in:
@@ -102,11 +102,20 @@ func TestAgentNetwork_UpdateSettings_PreservesImmutableAndTogglesCollection(t *t
|
||||
require.NotEmpty(t, before.Subdomain, "subdomain pinned at bootstrap")
|
||||
assert.False(t, before.EnablePromptCollection, "prompt collection defaults off")
|
||||
|
||||
// Attempt to flip toggles AND smuggle a different cluster/subdomain — the
|
||||
// immutable fields must be ignored.
|
||||
// A cluster different from the one pinned at bootstrap must be rejected
|
||||
// outright — never silently swapped or ignored.
|
||||
_, err = mgr.UpdateSettings(ctx, adminUserID, &agenttypes.Settings{
|
||||
AccountID: accountID,
|
||||
Cluster: "attacker.cluster",
|
||||
EnableLogCollection: true,
|
||||
})
|
||||
require.Error(t, err, "UpdateSettings with a mismatched cluster must fail")
|
||||
|
||||
// Flipping the toggles works with the pinned cluster echoed back (and
|
||||
// with it omitted); the subdomain is never taken from the request.
|
||||
updated, err := mgr.UpdateSettings(ctx, adminUserID, &agenttypes.Settings{
|
||||
AccountID: accountID,
|
||||
Cluster: "attacker.cluster",
|
||||
Cluster: clusterAddr,
|
||||
Subdomain: "evil",
|
||||
EnableLogCollection: true,
|
||||
EnablePromptCollection: true,
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
agentNetworkTypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
"github.com/rs/xid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
@@ -744,6 +746,14 @@ func validateDeleteGroup(ctx context.Context, transaction store.Store, group *ty
|
||||
return &GroupLinkError{"network router", linkedRouter.ID}
|
||||
}
|
||||
|
||||
if isLinked, linkedService := isGroupLinkedToReverseProxyService(ctx, transaction, group.AccountID, group.ID); isLinked {
|
||||
return &GroupLinkError{"reverse proxy service", linkedService.Domain}
|
||||
}
|
||||
|
||||
if isLinked, linkedPolicy := isGroupLinkedToAgentNetworkPolicy(ctx, transaction, group.AccountID, group.ID); isLinked {
|
||||
return &GroupLinkError{"agent network policy", linkedPolicy.Name}
|
||||
}
|
||||
|
||||
return checkGroupLinkedToSettings(ctx, transaction, group)
|
||||
}
|
||||
|
||||
@@ -875,6 +885,46 @@ func isGroupLinkedToNetworkRouter(ctx context.Context, transaction store.Store,
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// isGroupLinkedToReverseProxyService checks if a group is used as an access group
|
||||
// of a private reverse proxy service or as a bearer-auth distribution group.
|
||||
func isGroupLinkedToReverseProxyService(ctx context.Context, transaction store.Store, accountID string, groupID string) (bool, *service.Service) {
|
||||
services, err := transaction.GetAccountServices(ctx, store.LockingStrengthNone, accountID)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("error retrieving reverse proxy services while checking group linkage: %v", err)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, svc := range services {
|
||||
if svc.Private && slices.Contains(svc.AccessGroups, groupID) {
|
||||
return true, svc
|
||||
}
|
||||
if svc.Auth.BearerAuth != nil && svc.Auth.BearerAuth.Enabled && slices.Contains(svc.Auth.BearerAuth.DistributionGroups, groupID) {
|
||||
return true, svc
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// isGroupLinkedToAgentNetworkPolicy checks if a group is used as a source group by any
|
||||
// agent network policy in the account.
|
||||
func isGroupLinkedToAgentNetworkPolicy(ctx context.Context, transaction store.Store, accountID string, groupID string) (bool, *agentNetworkTypes.Policy) {
|
||||
policies, err := transaction.GetAccountAgentNetworkPolicies(ctx, store.LockingStrengthNone, accountID)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("error retrieving agent network policies while checking group linkage: %v", err)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, policy := range policies {
|
||||
if policy == nil {
|
||||
continue
|
||||
}
|
||||
if slices.Contains(policy.SourceGroups, groupID) {
|
||||
return true, policy
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// areGroupChangesAffectPeers checks if any changes to the specified groups will affect peers.
|
||||
// It fetches each collection once and checks all groupIDs against them in memory.
|
||||
func areGroupChangesAffectPeers(ctx context.Context, transaction store.Store, accountID string, groupIDs []string) (bool, error) {
|
||||
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
agentNetworkTypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
|
||||
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
"github.com/netbirdio/netbird/management/server/groups"
|
||||
"github.com/netbirdio/netbird/management/server/networks"
|
||||
"github.com/netbirdio/netbird/management/server/networks/resources"
|
||||
@@ -125,6 +127,21 @@ func TestDefaultAccountManager_DeleteGroup(t *testing.T) {
|
||||
"grp-for-integration",
|
||||
"only service users with admin power can delete integration group",
|
||||
},
|
||||
{
|
||||
"agent network policy",
|
||||
"grp-for-agent-network-policy",
|
||||
"agent network policy",
|
||||
},
|
||||
{
|
||||
"reverse proxy private service access group",
|
||||
"grp-for-rp-private",
|
||||
"reverse proxy service",
|
||||
},
|
||||
{
|
||||
"reverse proxy bearer distribution group",
|
||||
"grp-for-rp-bearer",
|
||||
"reverse proxy service",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
@@ -218,6 +235,17 @@ func TestDefaultAccountManager_DeleteGroups(t *testing.T) {
|
||||
groupIDs: []string{"grp-for-integration"},
|
||||
expectedReasons: []string{"only service users with admin power can delete integration group"},
|
||||
},
|
||||
{
|
||||
name: "agent network policy",
|
||||
groupIDs: []string{"grp-for-agent-network-policy"},
|
||||
expectedReasons: []string{"agent network policy"},
|
||||
},
|
||||
{
|
||||
name: "reverse proxy services",
|
||||
groupIDs: []string{"grp-for-rp-private", "grp-for-rp-bearer"},
|
||||
expectedReasons: []string{"reverse proxy service", "reverse proxy service"},
|
||||
expectedNotDeleted: []string{"grp-for-rp-private", "grp-for-rp-bearer"},
|
||||
},
|
||||
{
|
||||
name: "successfully delete multiple groups",
|
||||
groupIDs: []string{"group-1", "group-2"},
|
||||
@@ -285,6 +313,65 @@ func TestDefaultAccountManager_DeleteGroups(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAccountManager_DeleteGroupUnlinkedFromReverseProxyService(t *testing.T) {
|
||||
am, _, err := createManager(t)
|
||||
require.NoError(t, err, "Failed to create account manager")
|
||||
|
||||
_, account, err := initTestGroupAccount(am)
|
||||
require.NoError(t, err, "Failed to init testing account")
|
||||
|
||||
deletableGroups := []*types.Group{
|
||||
{
|
||||
ID: "grp-rp-bearer-disabled",
|
||||
AccountID: account.Id,
|
||||
Name: "Group only in a disabled bearer auth",
|
||||
Issued: types.GroupIssuedAPI,
|
||||
Peers: make([]string, 0),
|
||||
},
|
||||
{
|
||||
ID: "grp-rp-nonprivate-access",
|
||||
AccountID: account.Id,
|
||||
Name: "Group only in a non-private service's access groups",
|
||||
Issued: types.GroupIssuedAPI,
|
||||
Peers: make([]string, 0),
|
||||
},
|
||||
}
|
||||
for _, group := range deletableGroups {
|
||||
require.NoError(t, am.CreateGroup(context.Background(), account.Id, groupAdminUserID, group))
|
||||
}
|
||||
|
||||
// Disabled bearer auth and stale access groups on a non-private service
|
||||
// are inert configuration and must not block group deletion.
|
||||
services := []*rpservice.Service{
|
||||
{
|
||||
ID: "rp-svc-bearer-disabled",
|
||||
AccountID: account.Id,
|
||||
Domain: "bearer-disabled.services.example.com",
|
||||
Auth: rpservice.AuthConfig{
|
||||
BearerAuth: &rpservice.BearerAuthConfig{
|
||||
Enabled: false,
|
||||
DistributionGroups: []string{"grp-rp-bearer-disabled"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "rp-svc-nonprivate-access",
|
||||
AccountID: account.Id,
|
||||
Domain: "nonprivate.services.example.com",
|
||||
Private: false,
|
||||
AccessGroups: []string{"grp-rp-nonprivate-access"},
|
||||
},
|
||||
}
|
||||
for _, svc := range services {
|
||||
require.NoError(t, am.Store.CreateService(context.Background(), svc))
|
||||
}
|
||||
|
||||
for _, group := range deletableGroups {
|
||||
err = am.DeleteGroup(context.Background(), account.Id, groupAdminUserID, group.ID)
|
||||
assert.NoError(t, err, "group %s is not referenced by an active reverse proxy gate and should be deletable", group.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAccountManager_DeleteGroupLinkedToFlowGroup(t *testing.T) {
|
||||
am, _, err := createManager(t)
|
||||
require.NoError(t, err)
|
||||
@@ -406,6 +493,30 @@ func initTestGroupAccount(am *DefaultAccountManager) (*DefaultAccountManager, *t
|
||||
Peers: make([]string, 0),
|
||||
}
|
||||
|
||||
groupForAgentNetworkPolicy := &types.Group{
|
||||
ID: "grp-for-agent-network-policy",
|
||||
AccountID: "account-id",
|
||||
Name: "Group for agent network policies",
|
||||
Issued: types.GroupIssuedAPI,
|
||||
Peers: make([]string, 0),
|
||||
}
|
||||
|
||||
groupForRPPrivate := &types.Group{
|
||||
ID: "grp-for-rp-private",
|
||||
AccountID: "account-id",
|
||||
Name: "Group for private reverse proxy service",
|
||||
Issued: types.GroupIssuedAPI,
|
||||
Peers: make([]string, 0),
|
||||
}
|
||||
|
||||
groupForRPBearer := &types.Group{
|
||||
ID: "grp-for-rp-bearer",
|
||||
AccountID: "account-id",
|
||||
Name: "Group for bearer reverse proxy service",
|
||||
Issued: types.GroupIssuedAPI,
|
||||
Peers: make([]string, 0),
|
||||
}
|
||||
|
||||
routeResource := &route.Route{
|
||||
ID: "example route",
|
||||
Groups: []string{groupForRoute.ID},
|
||||
@@ -461,6 +572,66 @@ func initTestGroupAccount(am *DefaultAccountManager) (*DefaultAccountManager, *t
|
||||
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForSetupKeys)
|
||||
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForUsers)
|
||||
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForIntegration)
|
||||
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForAgentNetworkPolicy)
|
||||
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForRPPrivate)
|
||||
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForRPBearer)
|
||||
|
||||
agentNetworkPolicy := &agentNetworkTypes.Policy{
|
||||
ID: "example agent network policy",
|
||||
AccountID: accountID,
|
||||
Name: "Example agent network policy",
|
||||
Enabled: true,
|
||||
SourceGroups: []string{groupForAgentNetworkPolicy.ID},
|
||||
}
|
||||
if err := am.Store.SaveAgentNetworkPolicy(context.Background(), agentNetworkPolicy); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// The decoy services are created first so the linkage check has to scan
|
||||
// past services that do not reference the groups under test.
|
||||
rpServices := []*rpservice.Service{
|
||||
{
|
||||
ID: "rp-svc-private-decoy",
|
||||
AccountID: accountID,
|
||||
Domain: "private-decoy.services.example.com",
|
||||
Private: true,
|
||||
AccessGroups: []string{"unrelated-group"},
|
||||
},
|
||||
{
|
||||
ID: "rp-svc-bearer-decoy",
|
||||
AccountID: accountID,
|
||||
Domain: "bearer-decoy.services.example.com",
|
||||
Auth: rpservice.AuthConfig{
|
||||
BearerAuth: &rpservice.BearerAuthConfig{
|
||||
Enabled: true,
|
||||
DistributionGroups: []string{"unrelated-group"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "rp-svc-private",
|
||||
AccountID: accountID,
|
||||
Domain: "private.services.example.com",
|
||||
Private: true,
|
||||
AccessGroups: []string{groupForRPPrivate.ID},
|
||||
},
|
||||
{
|
||||
ID: "rp-svc-bearer",
|
||||
AccountID: accountID,
|
||||
Domain: "bearer.services.example.com",
|
||||
Auth: rpservice.AuthConfig{
|
||||
BearerAuth: &rpservice.BearerAuthConfig{
|
||||
Enabled: true,
|
||||
DistributionGroups: []string{groupForRPBearer.ID},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, svc := range rpServices {
|
||||
if err := am.Store.CreateService(context.Background(), svc); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
acc, err := am.Store.GetAccount(context.Background(), account.Id)
|
||||
if err != nil {
|
||||
|
||||
@@ -82,6 +82,9 @@ func (m *managerImpl) ValidateUserPermissions(
|
||||
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(
|
||||
ctx context.Context,
|
||||
accountID string,
|
||||
@@ -89,7 +92,7 @@ func (m *managerImpl) ValidateRoleModuleAccess(
|
||||
module modules.Module,
|
||||
operation operations.Operation,
|
||||
) bool {
|
||||
if permissions, ok := role.Permissions[module]; ok {
|
||||
if permissions, ok := lookupModulePermissions(role, module); ok {
|
||||
if allowed, exists := permissions[operation]; exists {
|
||||
return allowed
|
||||
}
|
||||
@@ -100,6 +103,21 @@ func (m *managerImpl) ValidateRoleModuleAccess(
|
||||
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) {
|
||||
if user.AccountID != accountID {
|
||||
return ctx, status.NewUserNotPartOfAccountError()
|
||||
@@ -119,7 +137,7 @@ func (m *managerImpl) GetPermissionsByRole(ctx context.Context, role types.UserR
|
||||
permissions := roles.Permissions{}
|
||||
|
||||
for k := range modules.All {
|
||||
if rolePermissions, ok := roleMap.Permissions[k]; ok {
|
||||
if rolePermissions, ok := lookupModulePermissions(roleMap, k); ok {
|
||||
permissions[k] = rolePermissions
|
||||
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
|
||||
|
||||
import "strings"
|
||||
|
||||
type Module string
|
||||
|
||||
const (
|
||||
@@ -20,6 +22,17 @@ const (
|
||||
IdentityProviders Module = "identity_providers"
|
||||
Services Module = "services"
|
||||
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{}{
|
||||
@@ -40,4 +53,21 @@ var All = map[Module]struct{}{
|
||||
IdentityProviders: {},
|
||||
Services: {},
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1685,14 +1685,34 @@ func (a *Account) injectPrivateServicePolicies(svc *service.Service, proxyPeers
|
||||
if len(proxyPeers) == 0 {
|
||||
return
|
||||
}
|
||||
// A service's AccessGroups can name groups that no longer exist — persisted
|
||||
// services and the agent-network synthesiser both carry the ids verbatim from
|
||||
// their own state. An unresolvable source authorises nothing, so drop it here
|
||||
// rather than let the network-map assembly resolve it to a nil group.
|
||||
sources := a.existingGroupIDs(svc.AccessGroups)
|
||||
if len(sources) == 0 {
|
||||
return
|
||||
}
|
||||
for _, proxyPeer := range proxyPeers {
|
||||
a.Policies = append(a.Policies, a.createPrivateServicePolicy(svc, proxyPeer))
|
||||
a.Policies = append(a.Policies, a.createPrivateServicePolicy(svc, proxyPeer, sources))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Account) createPrivateServicePolicy(svc *service.Service, proxyPeer *nbpeer.Peer) *Policy {
|
||||
// existingGroupIDs returns the subset of groupIDs that resolve to a group in the account,
|
||||
// preserving the input order.
|
||||
func (a *Account) existingGroupIDs(groupIDs []string) []string {
|
||||
out := make([]string, 0, len(groupIDs))
|
||||
for _, groupID := range groupIDs {
|
||||
if _, ok := a.Groups[groupID]; ok {
|
||||
out = append(out, groupID)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (a *Account) createPrivateServicePolicy(svc *service.Service, proxyPeer *nbpeer.Peer, accessGroups []string) *Policy {
|
||||
policyID := fmt.Sprintf("private-access-%s-%s", svc.ID, proxyPeer.ID)
|
||||
sources := append([]string(nil), svc.AccessGroups...)
|
||||
sources := append([]string(nil), accessGroups...)
|
||||
return &Policy{
|
||||
ID: policyID,
|
||||
Name: fmt.Sprintf("Private Access to %s", svc.Name),
|
||||
|
||||
@@ -68,7 +68,7 @@ type ProxyAccessTokenGenerated struct {
|
||||
// CreateNewProxyAccessToken generates a new proxy access token.
|
||||
// Returns the token with hashed value stored and plain token for one-time display.
|
||||
func CreateNewProxyAccessToken(name string, expiresIn time.Duration, accountID *string, createdBy string) (*ProxyAccessTokenGenerated, error) {
|
||||
hashedToken, plainToken, err := generateProxyToken()
|
||||
hashedToken, plainToken, err := GenerateProxyToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -94,7 +94,10 @@ func CreateNewProxyAccessToken(name string, expiresIn time.Duration, accountID *
|
||||
}, nil
|
||||
}
|
||||
|
||||
func generateProxyToken() (HashedProxyToken, PlainProxyToken, error) {
|
||||
// GenerateProxyToken generates a new random proxy token, returning its SHA-256
|
||||
// hash (for storage) and the one-time plaintext. Exported so external modules
|
||||
// can mint tokens in the canonical proxy-token format.
|
||||
func GenerateProxyToken() (HashedProxyToken, PlainProxyToken, error) {
|
||||
secret, err := b.Random(ProxyTokenSecretLength)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -123,6 +124,22 @@ func TestCreateNewProxyAccessToken(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGenerateProxyToken(t *testing.T) {
|
||||
hashed, plain, err := GenerateProxyToken()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := plain.Validate(); err != nil {
|
||||
t.Errorf("generated token failed Validate(): %v", err)
|
||||
}
|
||||
if plain.Hash() != hashed {
|
||||
t.Error("returned hashed token does not match Hash(plain)")
|
||||
}
|
||||
if !strings.HasPrefix(string(plain), ProxyTokenPrefix) {
|
||||
t.Errorf("token %q missing prefix %q", plain, ProxyTokenPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyAccessToken_IsExpired(t *testing.T) {
|
||||
past := time.Now().Add(-1 * time.Hour)
|
||||
future := time.Now().Add(1 * time.Hour)
|
||||
|
||||
Reference in New Issue
Block a user