Skip to content

Add authorization scriptlet #1412

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

Merged
merged 14 commits into from
Dec 8, 2024
Merged
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 @@ -1477,6 +1477,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 @@ -1513,6 +1514,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 @@ -2244,6 +2253,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
4 changes: 4 additions & 0 deletions doc/api-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2668,3 +2668,7 @@ This adds support to take screenshots of the current VGA console of a VM.
## `image_import_alias`

Adds a new `X-Incus-aliases` HTTP header to set aliases while uploading an image.

## `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 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 @@ -64,3 +65,21 @@ Users that you do not trust with root access to the host should not be granted t
The remaining relations may be granted.
However, you must apply appropriate {ref}`project-restrictions`.
```

(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`, an object with attributes `Username` (the user name or certificate fingerprint), `Protocol` (the authentication protocol), `IsAllProjectsRequest` (whether the request is made on all projects) and `ProjectName` (the project name)
- `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
9 changes: 9 additions & 0 deletions internal/server/auth/common/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package common

// RequestDetails is a type representing an authorization request.
type RequestDetails struct {
Username string
Protocol string
IsAllProjectsRequest bool
ProjectName string
}
47 changes: 29 additions & 18 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 @@ -29,40 +30,47 @@ func (c *commonAuthorizer) init(driverName string, l logger.Logger) error {
}

type requestDetails struct {
userName string
protocol string
forwardedUsername string
forwardedProtocol string
isAllProjectsRequest bool
projectName string
common.RequestDetails

forwardedUsername string
forwardedProtocol string
}

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

if r.protocol == "cluster" && (r.forwardedProtocol == "unix" || r.forwardedProtocol == "cluster" || r.forwardedProtocol == "") {
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 != "" {
if r.Protocol == "cluster" && r.forwardedUsername != "" {
return r.forwardedUsername
}

return r.userName
return r.Username
}

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

return r.protocol
return r.Protocol
}

func (r *requestDetails) actualDetails() *common.RequestDetails {
return &common.RequestDetails{
Username: r.username(),
Protocol: r.authenticationProtocol(),
IsAllProjectsRequest: r.IsAllProjectsRequest,
ProjectName: r.ProjectName,
}
}

func (c *commonAuthorizer) requestDetails(r *http.Request) (*requestDetails, error) {
Expand Down Expand Up @@ -116,12 +124,15 @@ func (c *commonAuthorizer) requestDetails(r *http.Request) (*requestDetails, err
}

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

forwardedUsername: forwardedUsername,
forwardedProtocol: forwardedProtocol,
}, nil
}

Expand Down
Loading
Loading