Skip to content

Commit

Permalink
Add remoteuser authenticator
Browse files Browse the repository at this point in the history
  • Loading branch information
nbarrientos committed Jun 14, 2021
1 parent 800b8cb commit 318fb82
Show file tree
Hide file tree
Showing 14 changed files with 444 additions and 21 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ This is under active development, see the Issues list for current outstanding it
* Authentication
* [Okta identity cloud](https://okta.com/)
* Static configured users with support for basic agent+action ACLs as well as Open Policy Agent policies
* Login delegation via X-Remote-User
* Capable of running centrally separate from signers
* Supports setting the Choria Organization claim for multi tenancy (not for okta users)
* Authorization
Expand Down Expand Up @@ -312,6 +313,29 @@ Once you signed up for Okta and set up a application for Choria you'll get endpo

Here we configure `acls` based on Okta groups - all users can `rpcutil ping`, there are Puppet admins with appropriate rights and fleet wide admins capable of managing anything.

#### Login delegation via `X-Remote-User`

It's possible to delegate the responsibility of verifying the users' identity to a front-end configured using the mechanism of your choice (Kerberos, OIDC, etc) and setting the identity of the caller in the `X-Remote-User` HTTP header. In this case, configure the `remoteuser` authenticator and make sure that the entrypoint `/login` is well protected. All users will receive the same ACLs et al, configured via `default_user`.

```json
{
"authenticator": "remoteuser",
"remote_authenticator": {
"validity": "1h",
"signing_key": "/etc/choria/signer/signing_key.pem",
"default_user": {
"acls": [
"puppet.*"
],
"properties": {
"group": "users"
},
"organization": "acme"
}
}
}
```

## Authorization

Authorization is how you declare what an authenticated user can do, in this system the JWT tokens can contain either a simple agent/action list or a full features [Open Policy Agent](https://www.openpolicyagent.org/) based policy.
Expand Down
14 changes: 12 additions & 2 deletions api/gen/restapi/embedded_spec.go

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

2 changes: 1 addition & 1 deletion api/gen/restapi/operations/post_login.go

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

36 changes: 27 additions & 9 deletions api/gen/restapi/operations/post_login_parameters.go

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

2 changes: 1 addition & 1 deletion api/gen/restapi/operations/post_sign.go

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

7 changes: 6 additions & 1 deletion api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,14 @@ paths:
- in: "body"
name: "request"
description: "The Login request"
required: true
required: false
schema:
$ref: "#/definitions/LoginRequest"
- in: "header"
name: "X-Remote-User"
description: "The remote user"
required: false
type: string

responses:
"200":
Expand Down
4 changes: 2 additions & 2 deletions authenticators/authenticators.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ var mu = &sync.Mutex{}

// Authenticator providers user authentication
type Authenticator interface {
Login(*models.LoginRequest) *models.LoginResponse
Login(*models.LoginRequest, *string) *models.LoginResponse
}

// SetAuthenticator sets the authenticator to use
Expand All @@ -33,5 +33,5 @@ func LoginHandler(params operations.PostLoginParams) middleware.Responder {
return operations.NewPostLoginOK().WithPayload(&models.LoginResponse{Error: "No authenticator configured"})
}

return operations.NewPostLoginOK().WithPayload(authenticator.Login(params.Request))
return operations.NewPostLoginOK().WithPayload(authenticator.Login(params.Request, params.XRemoteUser))
}
8 changes: 7 additions & 1 deletion authenticators/okta/okta.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func New(c *AuthenticatorConfig, log *logrus.Entry, site string) (a *Authenticat
}

// Login logs someone in using Okta
func (a *Authenticator) Login(req *models.LoginRequest) (resp *models.LoginResponse) {
func (a *Authenticator) Login(req *models.LoginRequest, ru *string) (resp *models.LoginResponse) {
timer := authenticators.ProcessTime.WithLabelValues(a.site, "okta")
obs := prometheus.NewTimer(timer)
defer obs.ObserveDuration()
Expand All @@ -88,6 +88,12 @@ func (a *Authenticator) Login(req *models.LoginRequest) (resp *models.LoginRespo
func (a *Authenticator) processLogin(req *models.LoginRequest) (resp *models.LoginResponse) {
resp = &models.LoginResponse{}

if req == nil {
a.log.Warnf("Login failed due to missing message body")
resp.Error = "Login failed"
return
}

_, _, err := a.login(req.Username, req.Password)
if err != nil {
resp.Error = fmt.Sprintf("Login Failed: %s", err)
Expand Down
48 changes: 48 additions & 0 deletions authenticators/remoteuser/defaultuser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package remoteuser

import (
"io/ioutil"
"sync"
)

// DefaulUser is a choria user without user and/or password
type DefaultUser struct {
// Organization is a org name the user belongs to
Organization string `json:"organization"`

// ACLs are for the action list authorizer
ACLs []string `json:"acls"`

// OPAPolicy is a string holding a Open Policy Agent rego policy
OPAPolicy string `json:"opa_policy"`

// OPAPolicyFile is the path to a rego file to embed as the policy for this user
OPAPolicyFile string `json:"opa_policy_file"`

// Properties are free form additional information to add about a user, this can be
// referenced later in an authorizer like the Open Policy one
Properties map[string]string `json:"properties"`

sync.Mutex
}

// OpenPolicy retrieves the OPA Policy either from `OPAPolicy` or by reading the file in `OPAPolicyFile`
func (u DefaultUser) OpenPolicy() (policy string, err error) {
u.Lock()
defer u.Unlock()

if u.OPAPolicy != "" {
return u.OPAPolicy, nil
}

if u.OPAPolicyFile == "" {
return "", nil
}

out, err := ioutil.ReadFile(u.OPAPolicyFile)
if err != nil {
return "", err
}

return string(out), nil
}
Loading

0 comments on commit 318fb82

Please sign in to comment.