Skip to content

Commit

Permalink
Auth: Allow listing unmanaged networks with fine-grained auth (#14447)
Browse files Browse the repository at this point in the history
Fixes issue where fine-grained authorization was filtering out unmanaged
networks (because they aren't in the database). To do this I added a new
entitlement at the server level: `can_view_unmanaged_networks`.
Restricted TLS clients are granted this by default to not break existing
behaviour.

Opening this now but will mark as draft until I add some tests.

Closes #14085
  • Loading branch information
tomponline authored Nov 13, 2024
2 parents 7d821b5 + 152584e commit 75aea99
Show file tree
Hide file tree
Showing 7 changed files with 58 additions and 18 deletions.
3 changes: 3 additions & 0 deletions doc/metadata.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6331,6 +6331,9 @@ container or containers that use it. This allows using the `zfs` command in the
`can_view_warnings`
: Grants permission to view warnings.

`can_view_unmanaged_networks`
: Grants permission to view unmanaged networks on the LXD host machines.


<!-- entity group server end -->
<!-- entity group storage_bucket start -->
Expand Down
3 changes: 3 additions & 0 deletions lxd/auth/drivers/openfga_model.openfga
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ type server

# Grants permission to view warnings.
define can_view_warnings: [identity, service_account, group#member] or admin or viewer

# Grants permission to view unmanaged networks on the LXD host machines.
define can_view_unmanaged_networks: [identity, service_account, group#member] or admin or viewer
type certificate
relations
define server: [server]
Expand Down
2 changes: 1 addition & 1 deletion lxd/auth/drivers/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func (t *tls) allowProjectUnspecificEntityType(entitlement auth.Entitlement, ent
switch entityType {
case entity.TypeServer:
// Restricted TLS certificates have the following entitlements on server.
return shared.ValueInSlice(entitlement, []auth.Entitlement{auth.EntitlementCanViewResources, auth.EntitlementCanViewMetrics})
return shared.ValueInSlice(entitlement, []auth.Entitlement{auth.EntitlementCanViewResources, auth.EntitlementCanViewMetrics, auth.EntitlementCanViewUnmanagedNetworks})
case entity.TypeIdentity:
// If the entity URL refers to the identity that made the request, then the second path argument of the URL is
// the identifier of the identity. This line allows the caller to view their own identity and no one else's.
Expand Down
5 changes: 5 additions & 0 deletions lxd/auth/entitlements_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions lxd/metadata/configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -7167,6 +7167,10 @@
{
"name": "can_view_warnings",
"description": "Grants permission to view warnings."
},
{
"name": "can_view_unmanaged_networks",
"description": "Grants permission to view unmanaged networks on the LXD host machines."
}
]
},
Expand Down
52 changes: 36 additions & 16 deletions lxd/networks.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,20 +241,36 @@ func networksGet(d *Daemon, r *http.Request) response.Response {

recursion := util.IsRecursionRequest(r)

var networkNames []string
// networks holds the network names of the managed and unmanaged networks. They are in two different slices so that
// we can perform access control checks differently.
var networks [2][]string
const (
managed = iota
unmanaged
)

err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
// Get list of managed networks (that may or may not have network interfaces on the host).
networkNames, err = tx.GetNetworks(ctx, effectiveProjectName)
networks[managed], err = tx.GetNetworks(ctx, effectiveProjectName)

return err
})
if err != nil {
return response.InternalError(err)
}

// Get list of actual network interfaces on the host as well if the effective project is Default.
// Get list of actual network interfaces on the host if the effective project is default and the caller has permission.
var getUnmanagedNetworks bool
if effectiveProjectName == api.ProjectDefaultName {
err := s.Authorizer.CheckPermission(r.Context(), entity.ServerURL(), auth.EntitlementCanViewUnmanagedNetworks)
if err == nil {
getUnmanagedNetworks = true
} else if !auth.IsDeniedError(err) {
return response.SmartError(err)
}
}

if getUnmanagedNetworks {
ifaces, err := net.Interfaces()
if err != nil {
return response.InternalError(err)
Expand All @@ -267,33 +283,37 @@ func networksGet(d *Daemon, r *http.Request) response.Response {
}

// Append to the list of networks if a managed network of same name doesn't exist.
if !shared.ValueInSlice(iface.Name, networkNames) {
networkNames = append(networkNames, iface.Name)
if !shared.ValueInSlice(iface.Name, networks[managed]) {
networks[unmanaged] = append(networks[unmanaged], iface.Name)
}
}
}

// Permission checker works for managed networks only, since they are present in the database.
userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), auth.EntitlementCanView, entity.TypeNetwork)
if err != nil {
return response.InternalError(err)
}

resultString := []string{}
resultMap := []api.Network{}
for _, networkName := range networkNames {
if !userHasPermission(entity.NetworkURL(requestProjectName, networkName)) {
continue
}

if !recursion {
resultString = append(resultString, fmt.Sprintf("/%s/networks/%s", version.APIVersion, networkName))
} else {
net, err := doNetworkGet(s, r, s.ServerClustered, requestProjectName, reqProject.Config, networkName)
if err != nil {
for kind, networkNames := range networks {
for _, networkName := range networkNames {
// Filter out managed networks that the caller doesn't have permission to view.
if kind == managed && !userHasPermission(entity.NetworkURL(requestProjectName, networkName)) {
continue
}

resultMap = append(resultMap, net)
if !recursion {
resultString = append(resultString, fmt.Sprintf("/%s/networks/%s", version.APIVersion, networkName))
} else {
net, err := doNetworkGet(s, r, s.ServerClustered, requestProjectName, reqProject.Config, networkName)
if err != nil {
continue
}

resultMap = append(resultMap, net)
}
}
}

Expand Down
7 changes: 6 additions & 1 deletion test/suites/auth.sh
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ effective_permissions: []"
echo "${list_output}" | grep -Fq 'project,/1.0/projects/default,"can_create_image_aliases,can_create_images,can_create_instances,..."'

list_output="$(lxc auth permission list entity_type=server --format csv --max-entitlements 0)"
echo "${list_output}" | grep -Fq 'server,/1.0,"admin,can_create_groups,can_create_identities,can_create_identity_provider_groups,can_create_projects,can_create_storage_pools,can_delete_groups,can_delete_identities,can_delete_identity_provider_groups,can_delete_projects,can_delete_storage_pools,can_edit,can_edit_groups,can_edit_identities,can_edit_identity_provider_groups,can_edit_projects,can_edit_storage_pools,can_override_cluster_target_restriction,can_view_groups,can_view_identities,can_view_identity_provider_groups,can_view_metrics,can_view_permissions,can_view_privileged_events,can_view_projects,can_view_resources,can_view_warnings,permission_manager,project_manager,storage_pool_manager,viewer"'
echo "${list_output}" | grep -Fq 'server,/1.0,"admin,can_create_groups,can_create_identities,can_create_identity_provider_groups,can_create_projects,can_create_storage_pools,can_delete_groups,can_delete_identities,can_delete_identity_provider_groups,can_delete_projects,can_delete_storage_pools,can_edit,can_edit_groups,can_edit_identities,can_edit_identity_provider_groups,can_edit_projects,can_edit_storage_pools,can_override_cluster_target_restriction,can_view_groups,can_view_identities,can_view_identity_provider_groups,can_view_metrics,can_view_permissions,can_view_privileged_events,can_view_projects,can_view_resources,can_view_unmanaged_networks,can_view_warnings,permission_manager,project_manager,storage_pool_manager,viewer"'

list_output="$(lxc auth permission list entity_type=project --format csv --max-entitlements 0)"
echo "${list_output}" | grep -Fq 'project,/1.0/projects/default,"can_create_image_aliases,can_create_images,can_create_instances,can_create_network_acls,can_create_network_zones,can_create_networks,can_create_profiles,can_create_storage_buckets,can_create_storage_volumes,can_delete,can_delete_image_aliases,can_delete_images,can_delete_instances,can_delete_network_acls,can_delete_network_zones,can_delete_networks,can_delete_profiles,can_delete_storage_buckets,can_delete_storage_volumes,can_edit,can_edit_image_aliases,can_edit_images,can_edit_instances,can_edit_network_acls,can_edit_network_zones,can_edit_networks,can_edit_profiles,can_edit_storage_buckets,can_edit_storage_volumes,can_operate_instances,can_view,can_view_events,can_view_image_aliases,can_view_images,can_view_instances,can_view_metrics,can_view_network_acls,can_view_network_zones,can_view_networks,can_view_operations,can_view_profiles,can_view_storage_buckets,can_view_storage_volumes,image_alias_manager,image_manager,instance_manager,network_acl_manager,network_manager,network_zone_manager,operator,profile_manager,storage_bucket_manager,storage_volume_manager,viewer"'
Expand Down Expand Up @@ -448,6 +448,11 @@ user_is_server_admin() {
lxc_remote storage set "${remote}:test-pool" rsync.compression=true
lxc_remote storage show "${remote}:test-pool" | grep -Fq 'rsync.compression:'
lxc_remote storage delete "${remote}:test-pool"

# Should be able to view all managed and unmanaged networks
host_networks="$(ip a | grep -P '^\d+:' | cut -d' ' -f2 | tr -d ':' | grep -vP '^veth.*' | sort)"
lxd_networks="$(lxc_remote query "${remote}:/1.0/networks?recursion=1" | jq -r '.[].name' | sort)"
[ "${host_networks}" = "${lxd_networks}" ]
}

user_is_server_operator() {
Expand Down

0 comments on commit 75aea99

Please sign in to comment.