Skip to content

Commit

Permalink
Merge pull request #116 from iegomez/feat/jwt-add-js-interpreter
Browse files Browse the repository at this point in the history
JWT checkers and JS mode
  • Loading branch information
iegomez authored Feb 11, 2021
2 parents f0eeb85 + 7a84459 commit f8108eb
Show file tree
Hide file tree
Showing 20 changed files with 1,581 additions and 875 deletions.
74 changes: 0 additions & 74 deletions Gopkg.toml

This file was deleted.

208 changes: 154 additions & 54 deletions README.md

Large diffs are not rendered by default.

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

import (
"strconv"

"github.com/iegomez/mosquitto-go-auth/backends/js"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)

type Javascript struct {
stackDepthLimit int
msMaxDuration int64

userScript string
superuserScript string
aclScript string

runner *js.Runner
}

func NewJavascript(authOpts map[string]string, logLevel log.Level) (*Javascript, error) {

log.SetLevel(logLevel)

javascript := &Javascript{
stackDepthLimit: js.DefaultStackDepthLimit,
msMaxDuration: js.DefaultMsMaxDuration,
}

jsOk := true
missingOptions := ""

if stackLimit, ok := authOpts["js_stack_depth_limit"]; ok {
limit, err := strconv.ParseInt(stackLimit, 10, 64)
if err != nil {
log.Errorf("invalid stack depth limit %s, defaulting to %d", stackLimit, js.DefaultStackDepthLimit)
} else {
javascript.stackDepthLimit = int(limit)
}
}

if maxDuration, ok := authOpts["js_ms_max_duration"]; ok {
duration, err := strconv.ParseInt(maxDuration, 10, 64)
if err != nil {
log.Errorf("invalid stack depth limit %s, defaulting to %d", maxDuration, js.DefaultMsMaxDuration)
} else {
javascript.msMaxDuration = duration
}
}

if userScriptPath, ok := authOpts["js_user_script_path"]; ok {
script, err := js.LoadScript(userScriptPath)
if err != nil {
return javascript, err
}

javascript.userScript = script
} else {
jsOk = false
missingOptions += " js_user_script_path"
}

if superuserScriptPath, ok := authOpts["js_superuser_script_path"]; ok {
script, err := js.LoadScript(superuserScriptPath)
if err != nil {
return javascript, err
}

javascript.superuserScript = script
} else {
jsOk = false
missingOptions += " js_superuser_script_path"
}

if aclScriptPath, ok := authOpts["js_acl_script_path"]; ok {
script, err := js.LoadScript(aclScriptPath)
if err != nil {
return javascript, err
}

javascript.aclScript = script
} else {
jsOk = false
missingOptions += " js_acl_script_path"
}

//Exit if any mandatory option is missing.
if !jsOk {
return nil, errors.Errorf("Javascript backend error: missing options: %s", missingOptions)
}

javascript.runner = js.NewRunner(javascript.stackDepthLimit, javascript.msMaxDuration)

return javascript, nil
}

func (o *Javascript) GetUser(username, password, clientid string) bool {
params := map[string]interface{}{
"username": username,
"password": password,
"clientid": clientid,
}

granted, err := o.runner.RunScript(o.userScript, params)
if err != nil {
log.Errorf("js error: %s", err)
}

return granted
}

func (o *Javascript) GetSuperuser(username string) bool {
params := map[string]interface{}{
"username": username,
}

granted, err := o.runner.RunScript(o.superuserScript, params)
if err != nil {
log.Errorf("js error: %s", err)
}

return granted
}

func (o *Javascript) CheckAcl(username, topic, clientid string, acc int32) bool {
params := map[string]interface{}{
"username": username,
"topic": topic,
"clientid": clientid,
"acc": acc,
}

granted, err := o.runner.RunScript(o.aclScript, params)
if err != nil {
log.Errorf("js error: %s", err)
}

return granted
}

//GetName returns the backend's name
func (o *Javascript) GetName() string {
return "Javascript"
}

func (o *Javascript) Halt() {
// NO-OP
}
78 changes: 78 additions & 0 deletions backends/javascript_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package backends

import (
"testing"

log "github.com/sirupsen/logrus"
. "github.com/smartystreets/goconvey/convey"
)

func TestJavascript(t *testing.T) {
authOpts := make(map[string]string)

authOpts["js_user_script_path"] = "../test-files/js/user_script.js"
authOpts["js_superuser_script_path"] = "../test-files/js/superuser_script.js"
authOpts["js_acl_script_path"] = "../test-files/js/acl_script.js"

Convey("When constructing a Javascript backend", t, func() {
Convey("It returns error if there's a missing option", func() {
badOpts := make(map[string]string)

badOpts["js_user_script"] = authOpts["js_user_script"]
badOpts["js_superuser_script"] = authOpts["js_superuser_script"]

_, err := NewJavascript(badOpts, log.DebugLevel)
So(err, ShouldNotBeNil)
})

Convey("It returns error if a script can't be opened", func() {
badOpts := make(map[string]string)

badOpts["js_user_script"] = authOpts["js_user_script"]
badOpts["js_superuser_script"] = authOpts["js_superuser_script"]
badOpts["js_acl_script_path"] = "../test-files/js/nothing_here.js"

_, err := NewJavascript(badOpts, log.DebugLevel)
So(err, ShouldNotBeNil)
})

javascript, err := NewJavascript(authOpts, log.DebugLevel)
So(err, ShouldBeNil)

Convey("User checks should work", func() {
userResponse := javascript.GetUser("correct", "good", "some-id")
So(userResponse, ShouldBeTrue)

userResponse = javascript.GetUser("correct", "bad", "some-id")
So(userResponse, ShouldBeFalse)

userResponse = javascript.GetUser("wrong", "good", "some-id")
So(userResponse, ShouldBeFalse)
})

Convey("Superuser checks should work", func() {
superuserResponse := javascript.GetSuperuser("admin")
So(superuserResponse, ShouldBeTrue)

superuserResponse = javascript.GetSuperuser("non-admin")
So(superuserResponse, ShouldBeFalse)
})

Convey("ACL checks should work", func() {
aclResponse := javascript.CheckAcl("correct", "test/topic", "id", 1)
So(aclResponse, ShouldBeTrue)

aclResponse = javascript.CheckAcl("incorrect", "test/topic", "id", 1)
So(aclResponse, ShouldBeFalse)

aclResponse = javascript.CheckAcl("correct", "bad/topic", "id", 1)
So(aclResponse, ShouldBeFalse)

aclResponse = javascript.CheckAcl("correct", "test/topic", "wrong-id", 1)
So(aclResponse, ShouldBeFalse)

aclResponse = javascript.CheckAcl("correct", "test/topic", "id", 2)
So(aclResponse, ShouldBeFalse)
})
})
}
80 changes: 80 additions & 0 deletions backends/js/runner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package js

import (
"errors"
"io/ioutil"
"time"

"github.com/robertkrimen/otto"
)

// Default conf values for runner.
const (
DefaultStackDepthLimit = 32
DefaultMsMaxDuration = 200
)

type Runner struct {
StackDepthLimit int
MsMaxDuration int64
}

var Halt = errors.New("exceeded max execution time")

func NewRunner(stackDepthLimit int, msMaxDuration int64) *Runner {
return &Runner{
StackDepthLimit: stackDepthLimit,
MsMaxDuration: msMaxDuration,
}
}

func LoadScript(path string) (string, error) {
script, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}

return string(script), nil
}

func (o *Runner) RunScript(script string, params map[string]interface{}) (granted bool, err error) {
// The VM is not thread-safe, so we need to create a new VM on every run.
// TODO: This could be enhanced by having a pool of VMs.
vm := otto.New()
vm.SetStackDepthLimit(o.StackDepthLimit)
vm.Interrupt = make(chan func(), 1)

defer func() {
if caught := recover(); caught != nil {
if caught == Halt {
granted = false
err = Halt
return
}
panic(caught)
}
}()

go func() {
time.Sleep(time.Duration(o.MsMaxDuration) * time.Millisecond)
vm.Interrupt <- func() {
panic(Halt)
}
}()

for k, v := range params {
vm.Set(k, v)
}

val, err := vm.Run(script)
if err != nil {
return false, err
}

granted, err = val.ToBoolean()
if err != nil {
return false, err
}

return
}
Loading

0 comments on commit f8108eb

Please sign in to comment.