Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add authorization scriptlet #1412

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cmd/incusd/api_1.0.go
Original file line number Diff line number Diff line change
Expand Up @@ -1012,5 +1012,14 @@ func doApi10UpdateTriggers(d *Daemon, nodeChanged, clusterChanged map[string]str
}
}

// Setup the authorization scriptlet.
value, ok = clusterChanged["authorization.scriptlet"]
if ok {
err := d.setupAuthorizationScriptlet(value)
if err != nil {
return err
}
}

return nil
}
41 changes: 41 additions & 0 deletions cmd/incusd/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -1438,6 +1438,7 @@ func (d *Daemon) init() error {
syslogSocketEnabled := d.localConfig.SyslogSocket()
openfgaAPIURL, openfgaAPIToken, openfgaStoreID := d.globalConfig.OpenFGA()
instancePlacementScriptlet := d.globalConfig.InstancesPlacementScriptlet()
authorizationScriptlet := d.globalConfig.AuthorizationScriptlet()

d.endpoints.NetworkUpdateTrustedProxy(d.globalConfig.HTTPSTrustedProxy())
d.globalConfigMu.Unlock()
Expand Down Expand Up @@ -1474,6 +1475,14 @@ func (d *Daemon) init() error {
}
}

// Setup the authorization scriptlet.
if authorizationScriptlet != "" {
err = d.setupAuthorizationScriptlet(authorizationScriptlet)
if err != nil {
return err
}
}

// Setup BGP listener.
d.bgp = bgp.NewServer()
if bgpAddress != "" && bgpASN != 0 && bgpRouterID != "" {
Expand Down Expand Up @@ -2205,6 +2214,38 @@ func (d *Daemon) setupOpenFGA(apiURL string, apiToken string, storeID string) er
return nil
}

// Setup authorization scriptlet.
func (d *Daemon) setupAuthorizationScriptlet(scriptlet string) error {
err := scriptletLoad.AuthorizationSet(scriptlet)
if err != nil {
return fmt.Errorf("Failed saving authorization scriptlet: %w", err)
}

if scriptlet == "" {
// Reset to default authorizer.
d.authorizer, err = auth.LoadAuthorizer(d.shutdownCtx, auth.DriverTLS, logger.Log, d.clientCerts)
if err != nil {
return err
}

return nil
}

// Fail if not using the default tls or scriptlet authorizer.
switch d.authorizer.(type) {
case *auth.TLS, *auth.Scriptlet:
d.authorizer, err = auth.LoadAuthorizer(d.shutdownCtx, auth.DriverScriptlet, logger.Log, d.clientCerts)
if err != nil {
return err
}

default:
return errors.New("Attempting to setup scriptlet authorization while another authorizer is already set")
}

return nil
}

// Syslog listener.
func (d *Daemon) setupSyslogSocket(enable bool) error {
// Always cancel the context to ensure that no goroutines leak.
Expand Down
1 change: 1 addition & 0 deletions doc/.wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ BGP
bibi
BitLocker
bool
boolean
bootable
BPF
BMC
Expand Down
4 changes: 4 additions & 0 deletions doc/api-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2660,3 +2660,7 @@ The following configuration options have been added:
## `storage_live_migration`

This adds support for virtual-machines live-migration between storage pools.

## `authorization_scriptlet`

This adds the ability to define a scriptlet in a new configuration key, `authorization.scriptlet`, managing authorization on the Incus cluster.
23 changes: 21 additions & 2 deletions doc/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ When interacting with Incus over the Unix socket, members of the `incus-admin` g
Those who are only members of the `incus` group will instead be restricted to a single project tied to their user.

When interacting with Incus over the network (see {ref}`server-expose` for instructions), it is possible to further authenticate and restrict user access.
There are two supported authorization methods:
There are three supported authorization methods:

- {ref}`authorization-tls`
- {ref}`authorization-openfga`
- {ref}`authorization-scriptlet`

(authorization-tls)=
## TLS authorization
Expand All @@ -20,7 +21,7 @@ To restrict access, use [`incus config trust edit <fingerprint>`](incus_config_t
Set the `restricted` key to `true` and specify a list of projects to restrict the client to.
If the list of projects is empty, the client will not be allowed access to any of them.

This authorization method is always used if a client authenticates with TLS, regardless of whether another authorization method is configured.
This authorization method is also used if a client authenticates with TLS even if {ref}`OpenFGA authorization <authorization-openfga>` is configured.

(authorization-openfga)=
## Open Fine-Grained Authorization (OpenFGA)
Expand Down Expand Up @@ -76,3 +77,21 @@ The full Incus OpenFGA authorization model is defined in `internal/server/auth/d
language: none
---
```

(authorization-scriptlet)=
## Scriptlet authorization

Incus supports defining a scriptlet to manage fine-grained authorization, allowing to write precise authorization rules with no dependency on external tools.

To use scriptlet authorization, you can write a scriptlet in the `authorization.scriptlet` server configuration option implementing a function `authorize`, which takes three arguments:

- `details`, the authorization request details
- `object`, the object on which the user requests authorization
- `entitlement`, the authorization level asked by the user

This function must return a boolean indicating whether the user has access or not to the given object with the given entitlement.

Additionally, two optional functions can be defined so that users can be listed through the access API:

- `get_instance_access`, with two arguments (`project_name` and `instance_name`), returning a list of users able to access a given instance
- `get_project_access`, with one argument (`project_name`), returning a list of users able to access a given project
7 changes: 7 additions & 0 deletions doc/config_options.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2421,6 +2421,13 @@ The events can be any combination of `lifecycle`, `logging`, and `network-acl`.

<!-- config group server-loki end -->
<!-- config group server-miscellaneous start -->
```{config:option} authorization.scriptlet server-miscellaneous
:scope: "global"
:shortdesc: "Authorization scriptlet"
:type: "string"
When using scriptlet-based authorization, this option stores the scriptlet.
```

```{config:option} backups.compression_algorithm server-miscellaneous
:defaultdesc: "`gzip`"
:scope: "global"
Expand Down
8 changes: 6 additions & 2 deletions internal/server/auth/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@ const (

// DriverOpenFGA provides fine-grained authorization. It is compatible with any authentication method.
DriverOpenFGA string = "openfga"

// DriverScriptlet provides scriptlet-based authorization. It is compatible with any authentication method.
DriverScriptlet string = "scriptlet"
)

// ErrUnknownDriver is the "Unknown driver" error.
var ErrUnknownDriver = fmt.Errorf("Unknown driver")

var authorizers = map[string]func() authorizer{
DriverTLS: func() authorizer { return &tls{} },
DriverOpenFGA: func() authorizer { return &fga{} },
DriverTLS: func() authorizer { return &TLS{} },
DriverOpenFGA: func() authorizer { return &FGA{} },
DriverScriptlet: func() authorizer { return &Scriptlet{} },
}

type authorizer interface {
Expand Down
42 changes: 42 additions & 0 deletions internal/server/auth/common/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package common

// RequestDetails is a type representing an authorization request.
type RequestDetails struct {
UserName string
Protocol string
ForwardedUsername string
ForwardedProtocol string
IsAllProjectsRequest bool
ProjectName string
}

// IsInternalOrUnix tells whether a given request has been initiated locally.
func (r *RequestDetails) IsInternalOrUnix() bool {
if r.Protocol == "unix" {
return true
}

if r.Protocol == "cluster" && (r.ForwardedProtocol == "unix" || r.ForwardedProtocol == "cluster" || r.ForwardedProtocol == "") {
return true
}

return false
}

// Username extracts the request username.
func (r *RequestDetails) Username() string {
if r.Protocol == "cluster" && r.ForwardedUsername != "" {
return r.ForwardedUsername
}

return r.UserName
}

// AuthenticationProtocol extracts the request protocol.
func (r *RequestDetails) AuthenticationProtocol() string {
if r.Protocol == "cluster" {
return r.ForwardedProtocol
}

return r.Protocol
}
54 changes: 9 additions & 45 deletions internal/server/auth/driver_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
"net/url"

"github.com/lxc/incus/v6/internal/server/auth/common"
"github.com/lxc/incus/v6/internal/server/request"
"github.com/lxc/incus/v6/shared/logger"
"github.com/lxc/incus/v6/shared/util"
Expand All @@ -28,44 +29,7 @@ func (c *commonAuthorizer) init(driverName string, l logger.Logger) error {
return nil
}

type requestDetails struct {
userName string
protocol string
forwardedUsername string
forwardedProtocol string
isAllProjectsRequest bool
projectName string
}

func (r *requestDetails) isInternalOrUnix() bool {
if r.protocol == "unix" {
return true
}

if r.protocol == "cluster" && (r.forwardedProtocol == "unix" || r.forwardedProtocol == "cluster" || r.forwardedProtocol == "") {
return true
}

return false
}

func (r *requestDetails) username() string {
if r.protocol == "cluster" && r.forwardedUsername != "" {
return r.forwardedUsername
}

return r.userName
}

func (r *requestDetails) authenticationProtocol() string {
if r.protocol == "cluster" {
return r.forwardedProtocol
}

return r.protocol
}

func (c *commonAuthorizer) requestDetails(r *http.Request) (*requestDetails, error) {
func (c *commonAuthorizer) requestDetails(r *http.Request) (*common.RequestDetails, error) {
if r == nil {
return nil, fmt.Errorf("Cannot inspect nil request")
} else if r.URL == nil {
Expand Down Expand Up @@ -115,13 +79,13 @@ func (c *commonAuthorizer) requestDetails(r *http.Request) (*requestDetails, err
return nil, fmt.Errorf("Failed to parse request query parameters: %w", err)
}

return &requestDetails{
userName: username,
protocol: protocol,
forwardedUsername: forwardedUsername,
forwardedProtocol: forwardedProtocol,
isAllProjectsRequest: util.IsTrue(values.Get("all-projects")),
projectName: request.ProjectParam(r),
return &common.RequestDetails{
UserName: username,
Protocol: protocol,
ForwardedUsername: forwardedUsername,
ForwardedProtocol: forwardedProtocol,
IsAllProjectsRequest: util.IsTrue(values.Get("all-projects")),
ProjectName: request.ProjectParam(r),
}, nil
}

Expand Down
Loading