Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow authorisation policy to be configured.
Browse files Browse the repository at this point in the history
simonwo committed Jan 23, 2024
1 parent 74198c6 commit 45a9e5b
Showing 8 changed files with 282 additions and 7 deletions.
184 changes: 184 additions & 0 deletions docs/docs/dev/auth_flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# Authorization and authentication flow

Bacalhau authenticates and authorizes users in a multi-step flow.

## Roles

- **Auth server** is a set of API endpoints that are trusted to make auth
decisions. This is something built into the requester node and doesn't need to
be a separate service, but could also be implemented as an external service if
desired.
- **User agent** is a tool that acts on behalf of the user, running in a trusted
way locally to them. The user agent submits API calls to the requester node on
their behalf – so the CLI, Web UI and SDK are all user agents. We use the term
"user agent" to differentiate from a "client", which in the OAuth sense means
a third-party service that the user does not have complete trust in.

## Policies

Bacalhau implements flexible authentication and authorization using policies
which are written using a machine-executable policy format called Rego.

- Each **authentication policy** receives authentication credentials as input
and outputs JWT access tokens that will supplied to future API calls.
- Each **authorization policy** receives access tokens as input and outputs
decisions about allowable access to APIs and job submission.

These two policies work together to define the entire authentication and
authorization scheme.

## 1. Retrieve list of supported authentication methods

User agents make a request to their configured auth server to retrieve a list of
authentication methods, keyed by name.

```bash
curl -sL -X GET 'https://bootstrap.production.bacalhau.org/api/v1/auth'
```
```json
{
"clientkey": {
"type": "challenge",
"params": {
"nOnce": "9qn4v93qb4vq9hff",
"minBits": 2048,
},
},
"password": {
"type": "ask",
"params": {
"$schema": ...
},
},
"microsoft": {
"type": "external",
"params": {
"base": "https://login.microsoft.com/?abc=...",
"returnQueryParam": "redirect",
},
}
}
```

Each authentication method object describes:

* a type of authentication, identified by a specific key
* parameters to be used in running the authentication method, specific to that
type

### `challenge` authentication

This method is used to identify users via a private key that they hold. The
authentication response contains a `InputPhrase` that the user should sign and
return to the endpoint.

```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bacalhau.org/auth/challenge",
"type": "object",
"properties": {
"InputPhrase": { "type": "string", "pattern": "[A-Za-z0-9]+" },
}
}
```

## 2. Run the authn flow and submit the result for an access token

The user agent decides which authentication method to use (e.g. by asking the
user, or by knowing it has an appropriate key) and operates the flow.

Once all the data for the method has been successfully collected, the user agent
POSTs the data to the auth endpoint for the method. The endpoint is the base
auth endpoint plus the name of the method, e.g. `/api/v1/auth/<method>`. So to
submit data for a "userpass" method, the user agent would POST to
`/api/v1/auth/userpass`.

## 3. Auth server checks the authn data against a policy

The auth server processes the request by inputting the auth credentials into a
auth policy. If the auth policy finds the passed data acceptable, it returns a
signed JWT that the user can use as an access token.

The signed JWT is returned to the user agent. The user agent takes appropriate
steps to keep the access token secret.

In principle, the auth policy can return any JWT it wishes, which will be
interpreted later in the API auth policy – it is up to the authn policy and the
authz policy to work together to apply auth. The policy to run is identified by
the `Node.Auth.Methods` variable, which is a map of method names to policy
paths.

However, the default authn and authz policies make decisions using namespaces.
Here, the authn policy returns a set of namespaces with associated access
permissions, and the authz policy controls access based on them.

In this default case, the JWT includes the fields:

### `iss` (issuer)

The node ID of the auth server.

### `sub` (subject)

A network-unique user ID, derived from the auth credentials. The `sub` does not
need to identify the same user across different authentication methods, but
should ideally be the same if the user logs in via the same auth method again.

### `ist` (issued at)

The timestamp when the token was issued.

### `exp` (expires at)

The timestamp after which the token is no longer valid.

### `ns` (namespaces)

A map of namespaces to permission bits.

The key in the map is an namespace name that the user has some level of access
of. Namespace names are ephemeral – i.e. there does not need to be a persistent
or coordinated store of namespaces shared across the whole cluster. Instead, the
**format** of namespace names is an interface for the network operator to
decide.

For example, the default policy will just give the user access to a namespace
identifier by the `sub` field (e.g. their username). But in principle, more
complex setups involving groups could be used.

Namespace names can contain a `*`, which by convention will match any set of
characters, like a filesystem glob. But it is up to the various auth policies to
actually implement this. So a JWT claim containing `"*"` would give default
permissions for all namespaces.

The value in the map is an unsigned integer encoding permission bits. If the
following bits are set:

- `0b00000001`: user can describe jobs in the namespace
- `0b00000010`: user can create jobs in the namespace
- `0b00000100`: user can download results from the namespace
- `0b00001000`: user can cancel jobs in the namespace

## 4. Make an API request and include the token

The user agent includes an `Authorization` header with the access token it
wishes to use passed as a bearer token:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX3459…

Note that the `Authorization` header is strictly optional – access for
unauthorized users is controlled using the policy, and may be allowed. The API
call is allowed to proceed if the authorization policy returns a positive
decision.

The requester node executes the API authorization policy and passes details of
the API call. The default policy is one where the namespaces of the token are
checked if present, and non-namespaced APIs require a valid signed token.

As above, custom policies are allowed. The policy to execute is defined by the
`Node.Auth.AccessPolicyPath` config variable. For non-namespaced APIs, such as
node APIs, the policy may make a blanket decision simply using whether the user
has an authorization token or not, or may choose to make a decision depending on
the type of authorization. For namespaced APIs, such as job APIs, the policy
should examine the namespaces in the JWT token and respond accordingly.
49 changes: 49 additions & 0 deletions docs/docs/running-node/auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Authentication and authorization

Bacalhau includes a flexible auth system that supports multiple methods of auth
that are appropriate for different deployment environments.

## By default

With no specific authentication configuration supplied, Bacalhau runs in
"anonymous mode" – which allows unidentified users limited control over the
system. "Anonymous mode" is only appropriate for testing or evaluation setups.

In anonymous mode, Bacalhau will allow:

- Users identified by a self-generated private key to submit any job, cancel
their own jobs, read job lists and describing jobs.
- Users not identified by any key to access other read-only endpoints, such as
node or agent information.

## Restricting anonymous access

Bacalhau auth is controlled by policies. Configuring the auth system is done by
supplying a different policy file.

Restricting API access to only users that have authenticated requires specifying
a new **authorization policy**. You can download a policy that restricts
anonymous access and install it by using:

curl -sL https://raw.githubusercontent.com/bacalhau-project/bacalhau/main/pkg/authz/policies/policy_ns_anon.rego -o ~/.bacalhau/no-anon.rego
bacalhau config set Node.Auth.AccessPolicyPath ~/.bacalhau/no-anon.rego

Once the node is restarted, accessing the node APIs will require the user to be
authenticated, but by default will still allow users with a self-generated key
to authenticate themselves.

Restricting the list of keys that can authenticate to only a known set requires
specifying a new **authentication policy**. You can download a policy that
restricts key-based access and install it by using:

curl -sL https://raw.githubusercontent.com/bacalhau-project/bacalhau/main/pkg/authn/challenge/challenge_ns_no_anon.rego -o ~/.bacalhau/challenge_ns_no_anon.rego
bacalhau config set Node.Auth.Methods.ClientKey.Type challenge
bacalhau config set Node.Auth.Methods.ClientKey.PolicyPath ~/.bacalhau/challenge_ns_no_anon.rego

Then, modify the `allowed_clients` variable in `challange_ns_no_anon.rego` to
include acceptable client IDs, found by running `bacalhau id`.

bacalhau id | jq -rc .ClientID

Once the node is restarted, only keys in the allowed list will be able to access
any API.
7 changes: 6 additions & 1 deletion pkg/authz/policies/policy_ns_anon.rego
Original file line number Diff line number Diff line change
@@ -26,12 +26,17 @@ allow if {
namespace_readable(job_namespace_perms)
}

# Allow reading all other endpoints
# Allow reading all other endpoints, inclduing by users who don't have a token
allow if {
input.http.path != job_endpoint
input.http.method in http_safe_methods
}

# Allow posting to auth endpoints, neccessary to get a token in the first place
allow if {
input.http.path[2] == "auth"
}


# The permissions the access token grants on the job namespace
job_namespace_perms := token_namespaces[job_namespace]
6 changes: 5 additions & 1 deletion pkg/authz/policy.go
Original file line number Diff line number Diff line change
@@ -87,6 +87,10 @@ func (authorizer *policyAuthorizer) Authorize(req *http.Request) (Authorization,
return Authorization{Approved: approved}, err
}

// AlwaysAllowPolicy is a policy that will always permit access, irrespective of
// the passed in data, which is useful for testing.
var AlwaysAllowPolicy = lo.Must(policy.FromFS(policies, "policies/policy_test_allow.rego"))

// AlwaysAllow is an authorizer that will always permit access, irrespective of
// the passed in data, which is useful for testing.
var AlwaysAllow = NewPolicyAuthorizer(lo.Must(policy.FromFS(policies, "policies/policy_test_allow.rego")))
var AlwaysAllow = NewPolicyAuthorizer(AlwaysAllowPolicy)
32 changes: 28 additions & 4 deletions pkg/config/types/auth.go
Original file line number Diff line number Diff line change
@@ -12,9 +12,33 @@ type AuthenticatorConfig struct {
PolicyPath string `yaml:"PolicyPath,omitempty"`
}

// AuthConfig is config that controls the authentication and authorization
// process for servers. It is not used for clients.
// AuthConfig is config that controls user authentication and authorization.
type AuthConfig struct {
TokensPath string `yaml:"TokensPath"`
Methods map[string]AuthenticatorConfig `yaml:"Methods"`
// TokensPath is the location where a state file of tokens will be stored.
// By default it will be local to the Bacalhau repo, but can be any location
// in the host filesystem. Tokens are sensitive and should be stored in a
// location that is only readable to the current user.
TokensPath string `yaml:"TokensPath"`

// Methods maps "method names" to authenticator implementations. A method
// name is a human-readable string chosen by the person configuring the
// system that is shown to users to help them pick the authentication method
// they want to use. There can be multiple usages of the same Authenticator
// *type* but with different configs and parameters, each identified with a
// unique method name.
//
// For example, if an implementation wants to allow users to log in with
// Github or Bitbucket, they might both use an authenticator implementation
// of type "oidc", and each would appear once on this provider with key /
// method name "github" and "bitbucket".
//
// By default, only a single authentication method that accepts
// authentication via client keys will be enabled.
Methods map[string]AuthenticatorConfig `yaml:"Methods"`

// AccessPolicyPath is the path to a file or directory that will be loaded as
// the policy to apply to all inbound API requests. If unspecified, a policy
// that permits access to all API endpoints to both authenticated and
// unauthenticated users (the default as of v1.2.0) will be used.
AccessPolicyPath string `yaml:"AccessPolicyPath"`
}
1 change: 1 addition & 0 deletions pkg/config/types/generated_constants.go
Original file line number Diff line number Diff line change
@@ -156,3 +156,4 @@ const UpdateCheckFrequency = "Update.CheckFrequency"
const Auth = "Auth"
const AuthTokensPath = "Auth.TokensPath"
const AuthMethods = "Auth.Methods"
const AuthAccessPolicyPath = "Auth.AccessPolicyPath"
2 changes: 2 additions & 0 deletions pkg/config/types/generated_viper_defaults.go
Original file line number Diff line number Diff line change
@@ -178,6 +178,7 @@ func SetDefaults(cfg BacalhauConfig, opts ...SetOption) {
p.Viper.SetDefault(Auth, cfg.Auth)
p.Viper.SetDefault(AuthTokensPath, cfg.Auth.TokensPath)
p.Viper.SetDefault(AuthMethods, cfg.Auth.Methods)
p.Viper.SetDefault(AuthAccessPolicyPath, cfg.Auth.AccessPolicyPath)

}

@@ -343,4 +344,5 @@ func Set(cfg BacalhauConfig, opts ...SetOption) {
p.Viper.Set(Auth, cfg.Auth)
p.Viper.Set(AuthTokensPath, cfg.Auth.TokensPath)
p.Viper.Set(AuthMethods, cfg.Auth.Methods)
p.Viper.Set(AuthAccessPolicyPath, cfg.Auth.AccessPolicyPath)
}
8 changes: 7 additions & 1 deletion pkg/node/node.go
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import (
pkgconfig "github.com/bacalhau-project/bacalhau/pkg/config"
"github.com/bacalhau-project/bacalhau/pkg/config/types"
"github.com/bacalhau-project/bacalhau/pkg/ipfs"
"github.com/bacalhau-project/bacalhau/pkg/lib/policy"
libp2p_transport "github.com/bacalhau-project/bacalhau/pkg/libp2p/transport"
"github.com/bacalhau-project/bacalhau/pkg/model"
"github.com/bacalhau-project/bacalhau/pkg/models"
@@ -166,6 +167,11 @@ func NewNode(
"/api/v1/requester/logs",
}...)

authzPolicy, err := policy.FromPathOrDefault(config.AuthConfig.AccessPolicyPath, authz.AlwaysAllowPolicy)
if err != nil {
return nil, err
}

serverVersion := version.Get()
// public http api server
serverParams := publicapi.ServerParams{
@@ -174,7 +180,7 @@ func NewNode(
Port: config.APIPort,
HostID: config.NodeID,
Config: config.APIServerConfig,
Authorizer: authz.AlwaysAllow,
Authorizer: authz.NewPolicyAuthorizer(authzPolicy),
Headers: map[string]string{
apimodels.HTTPHeaderBacalhauGitVersion: serverVersion.GitVersion,
apimodels.HTTPHeaderBacalhauGitCommit: serverVersion.GitCommit,

0 comments on commit 45a9e5b

Please sign in to comment.