Skip to content

Commit

Permalink
Merge pull request #166 from iegomez/feat/jwt-files
Browse files Browse the repository at this point in the history
[JWT]: Add files-like ACLs
  • Loading branch information
iegomez authored Apr 27, 2021
2 parents 3eea168 + ee6e68d commit f284760
Show file tree
Hide file tree
Showing 29 changed files with 1,139 additions and 780 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ all:
go build pw-gen/pw.go

test:
cd plugin && make
go test ./backends ./cache ./hashing -v -count=1
rm plugin/*.so

test-backends:
cd plugin && make
go test ./backends -v -failfast -count=1
rm plugin/*.so

test-cache:
go test ./cache -v -failfast -count=1
Expand Down
32 changes: 27 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,8 +498,8 @@ Usage of ./pw:
For this backend `passwords` and `acls` file paths must be given:

```
auth_opt_password_path /path/to/password_file
auth_opt_acl_path /path/to/acl_file
auth_opt_files_password_path /path/to/password_file
auth_opt_files_acl_path /path/to/acl_file
```

The following are correctly formatted examples of password and acl files:
Expand Down Expand Up @@ -824,11 +824,11 @@ There are no requirements, as the tests create (and later delete) the DB and tab

### JWT

The `jwt` backend is for auth with a JWT remote API, a local DB or a JavaScript VM interpreter. Global otions for JWT are:
The `jwt` backend is for auth with a JWT remote API, a local DB, a JavaScript VM interpreter or an ACL file. Global otions for JWT are:

| Option | default | Mandatory | Meaning |
| ------------------------- | ----------------- | :---------: | ------------------------------------------------------- |
| jwt_mode | | Y | local, remote, js |
| jwt_mode | | Y | local, remote, js, files |
| jwt_parse_token | false | N | Parse token in remote/js modes |
| jwt_secret | | Y/N | JWT secret, required for local mode, optional otherwise |
| jwt_userfield | | N | When `Username`, expect `username` as part of claims |
Expand Down Expand Up @@ -1002,7 +1002,7 @@ Since local JWT follows the underlying DB backend's way of working, both of thes

#### JS mode

The last mode for this backend is JS mode, which allows to run a JavaScript interpreter VM to conduct checks. Options for this mode are:
When set to `js` JWT will act in JS mode, which allows to run a JavaScript interpreter VM to conduct checks. Options for this mode are:

| Option | default | Mandatory | Meaning |
| ------------------------------| --------------- | :---------: | ----------------------------------------------------- |
Expand Down Expand Up @@ -1049,6 +1049,28 @@ With `auth_opt_jwt_parse_token` the signature would be `function checkAcl(token,

Finally, this mode uses [otto](https://github.com/robertkrimen/otto) under the hood to run the scripts. Please check their documentation for supported features and known limitations.

#### Files mode

When set to `files` JWT will run in Files mode, which allows to check user ACLs from a given file.
These ACLs follow the exact same syntax and semantics as those from the [Files](#files) backend.

Options for this mode are:

| Option | default | Mandatory | Meaning |
| ------------------------------| --------------- | :---------: | --------------------- |
| jwt_files_acl_path | | Y | Path to ACL files |


Notice there's no `passwords` file option since usernames come from parsing the JWT token and no password check is required.
Thus, you should be careful about general ACL rules and prefer to explicitly set rules for each valid user.

If this shows to be a pain, I'm open to add a file that sets valid `users`,
i.e. like the `passwords` file for regular `Files` backend but without actual passwords.

If you run into the case where you want to grant some general access but only to valid registered users,
and find that duplicating rules for each of them in ACLs file is really a pain, please open an issue for discussion.


#### Password hashing

Since JWT needs not to check passwords, there's no need to configure a `hasher`.
Expand Down
59 changes: 31 additions & 28 deletions backends/backends.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,44 +257,47 @@ func (b *Backends) setCheckers(authOpts map[string]string) error {
}
}

if len(b.userCheckers) == 0 {
return errors.New("no backend registered user checks")
}

if len(b.aclCheckers) == 0 {
return errors.New("no backend registered ACL checks")
if len(b.userCheckers) == 0 && len(b.aclCheckers) == 0 {
return errors.New("no backends registered")
}

return nil
}

// setPrefixes sets options for prefixes handling.
func (b *Backends) setPrefixes(authOpts map[string]string, backends []string) {
if checkPrefix, ok := authOpts["check_prefix"]; ok && strings.Replace(checkPrefix, " ", "", -1) == "true" {
// Check that backends match prefixes.
if prefixesStr, ok := authOpts["prefixes"]; ok {
prefixes := strings.Split(strings.Replace(prefixesStr, " ", "", -1), ",")
if len(prefixes) == len(backends) {
// Set prefixes
// (I know some people find this type of comments useless, even harmful,
// but I find them helpful for quick code navigation on a project I don't work on daily, so screw them).
for i, backend := range backends {
b.prefixes[prefixes[i]] = backend
}
log.Infof("prefixes enabled for backends %s with prefixes %s.", authOpts["backends"], authOpts["prefixes"])
b.checkPrefix = true
} else {
log.Errorf("Error: got %d backends and %d prefixes, defaulting to prefixes disabled.", len(backends), len(prefixes))
b.checkPrefix = false
}
checkPrefix, ok := authOpts["check_prefix"]

} else {
log.Warn("Error: prefixes enabled but no options given, defaulting to prefixes disabled.")
b.checkPrefix = false
}
} else {
if !ok || strings.Replace(checkPrefix, " ", "", -1) != "true" {
b.checkPrefix = false

return
}

prefixesStr, ok := authOpts["prefixes"]

if !ok {
log.Warn("Error: prefixes enabled but no options given, defaulting to prefixes disabled.")
b.checkPrefix = false

return
}

prefixes := strings.Split(strings.Replace(prefixesStr, " ", "", -1), ",")

if len(prefixes) != len(backends) {
log.Errorf("Error: got %d backends and %d prefixes, defaulting to prefixes disabled.", len(backends), len(prefixes))
b.checkPrefix = false

return
}

for i, backend := range backends {
b.prefixes[prefixes[i]] = backend
}

log.Infof("prefixes enabled for backends %s with prefixes %s.", authOpts["backends"], authOpts["prefixes"])
b.checkPrefix = true
}

// checkPrefix checks if a username contains a valid prefix. If so, returns ok and the suitable backend name; else, !ok and empty string.
Expand Down
22 changes: 2 additions & 20 deletions backends/backends_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ func TestBackends(t *testing.T) {
pwPath, _ := filepath.Abs("../test-files/passwords")
aclPath, _ := filepath.Abs("../test-files/acls")

authOpts["password_path"] = pwPath
authOpts["acl_path"] = aclPath
authOpts["files_password_path"] = pwPath
authOpts["files_acl_path"] = aclPath

authOpts["redis_host"] = "localhost"
authOpts["redis_port"] = "6379"
Expand Down Expand Up @@ -65,24 +65,6 @@ func TestBackends(t *testing.T) {
So(err.Error(), ShouldEqual, "unknown backend unknown")
})

Convey("On initialization, lacking user/acl checkers should result in an error", t, func() {
authOpts["backends"] = "files, redis"
authOpts["files_register"] = "user"
authOpts["redis_register"] = "user"

_, err := Initialize(authOpts, log.DebugLevel)
So(err, ShouldNotBeNil)
So(err.Error(), ShouldEqual, "no backend registered ACL checks")

authOpts["backends"] = "files, redis"
authOpts["files_register"] = "acl"
authOpts["redis_register"] = "acl"

_, err = Initialize(authOpts, log.DebugLevel)
So(err, ShouldNotBeNil)
So(err.Error(), ShouldEqual, "no backend registered user checks")
})

Convey("On initialization, unknown checkers should result in an error", t, func() {
authOpts["backends"] = "files, redis"
authOpts["files_register"] = "user"
Expand Down
12 changes: 0 additions & 12 deletions backends/constants.go

This file was deleted.

12 changes: 12 additions & 0 deletions backends/constants/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package constants

// Mosquitto 1.5 introduces a new acc, MOSQ_ACL_SUBSCRIBE. Kept the names, so don't mind the linter.
// In almost any case, subscribe should be the same as read, except if you want to deny access to # by preventing it on subscribe.
const (
MOSQ_ACL_NONE = 0x00
MOSQ_ACL_READ = 0x01
MOSQ_ACL_WRITE = 0x02
MOSQ_ACL_READWRITE = 0x03
MOSQ_ACL_SUBSCRIBE = 0x04
MOSQ_ACL_DENY = 0x11
)
10 changes: 5 additions & 5 deletions backends/custom_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type CustomPlugin struct {
func NewCustomPlugin(authOpts map[string]string, logLevel log.Level) (*CustomPlugin, error) {
plug, err := plugin.Open(authOpts["plugin_path"])
if err != nil {
return nil, fmt.Errorf("Could not init custom plugin: %s", err)
return nil, fmt.Errorf("could not init custom plugin: %s", err)
}

customPlugin := &CustomPlugin{
Expand All @@ -31,22 +31,22 @@ func NewCustomPlugin(authOpts map[string]string, logLevel log.Level) (*CustomPlu
plInit, err := plug.Lookup("Init")

if err != nil {
return nil, fmt.Errorf("Couldn't find func Init in plugin: %s", err)
return nil, fmt.Errorf("couldn't find func Init in plugin: %s", err)
}

initFunc := plInit.(func(authOpts map[string]string, logLevel log.Level) error)

err = initFunc(authOpts, logLevel)
if err != nil {
return nil, fmt.Errorf("Couldn't init plugin: %s", err)
return nil, fmt.Errorf("couldn't init plugin: %s", err)
}

customPlugin.init = initFunc

plName, err := plug.Lookup("GetName")

if err != nil {
return nil, fmt.Errorf("Couldn't find func GetName in plugin: %s", err)
return nil, fmt.Errorf("couldn't find func GetName in plugin: %s", err)
}

nameFunc := plName.(func() string)
Expand Down Expand Up @@ -101,7 +101,7 @@ func NewCustomPlugin(authOpts map[string]string, logLevel log.Level) (*CustomPlu
plHalt, err := plug.Lookup("Halt")

if err != nil {
return nil, fmt.Errorf("Couldn't find func Halt in plugin: %s", err)
return nil, fmt.Errorf("couldn't find func Halt in plugin: %s", err)
}

haltFunc := plHalt.(func())
Expand Down
Loading

0 comments on commit f284760

Please sign in to comment.