Repository: honeydipper/honeydipper
Pull request #320 (2020-09-14 23:14:13)

chore(release): release
3297f17 fix(api): timing issue in tests
dde3d4c feat(configcheck): run tests against auth rules in configcheck
a85976e feat(api): use casbin authorization library
f7fd7d6 ci: add codeql analysis (#319)
8cc3ca8 fix(deps): update golang.org/x/oauth2 commit hash to 5d25da1 (#315)
7ce1fa4 fix(deps): update golang.org/x/crypto commit hash to 5c72a88 (#314)
b5133eb fix(deps): update google.golang.org/genproto commit hash to 0bd0a95 (#316)
4c947ff test: some gofumpt changes
f694ce1 feat(api): healthcheck and configurable prefix
baf5894 fix(drivers): auth-simple handling authorization header



Patch:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000..f5e7b3c
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,66 @@
+name: "CodeQL"
+
+on:
+  push:
+    branches: [dev, master]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [dev]
+  schedule:
+    - cron: '0 13 * * 1'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+
+    strategy:
+      fail-fast: false
+      matrix:
+        # Override automatic language detection by changing the below list
+        # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
+        language: ['go']
+        # Learn more...
+        # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v2
+      with:
+        # We must fetch at least the immediate parents so that if this is
+        # a pull request then we can checkout the head.
+        fetch-depth: 2
+
+    # If this run was triggered by a pull request event, then checkout
+    # the head of the pull request instead of the merge commit.
+    - run: git checkout HEAD^2
+      if: ${{ github.event_name == 'pull_request' }}
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v1
+      with:
+        languages: ${{ matrix.language }}
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file. 
+        # Prefix the list here with "+" to use these queries and those in the config file.
+        # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v1
+
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 https://git.io/JvXDl
+
+    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+    #    and modify them (or add more) to build your code if your project
+    #    uses a compiled language
+
+    #- run: |
+    #   make bootstrap
+    #   make release
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v1
diff --git a/cmd/honeydipper/configcheck.go b/cmd/honeydipper/configcheck.go
index 1763530..fb7b946 100644
--- a/cmd/honeydipper/configcheck.go
+++ b/cmd/honeydipper/configcheck.go
@@ -13,7 +13,10 @@ import (
 	"reflect"
 	"strings"
 
+	"github.com/ghodss/yaml"
+	"github.com/honeydipper/honeydipper/internal/api"
 	"github.com/honeydipper/honeydipper/internal/config"
+	"github.com/honeydipper/honeydipper/pkg/dipper"
 	"github.com/logrusorgru/aurora"
 	"github.com/mitchellh/mapstructure"
 )
@@ -108,9 +111,127 @@ func runConfigCheck(cfg *config.Config) int {
 		}
 	}
 
+	if checkAuthRules(cfg) > 0 {
+		ret = 1
+	}
+
 	return ret
 }
 
+func checkAuthRules(cfg *config.Config) int {
+	repo, ok := cfg.Loaded[cfg.InitRepo]
+	if !ok {
+		// no init repo during tests
+		return 0
+	}
+	b, e := repo.ReadFile("/tests/api_auth_tests.yaml")
+	if e != nil {
+		return 0
+	}
+
+	type AuthTests struct {
+		Models   []interface{}
+		Policies []interface{}
+		Tests    []struct {
+			Name    string
+			Subject string
+			Object  string
+			Action  string
+			Result  bool
+		}
+	}
+	var tests AuthTests
+	e = yaml.Unmarshal(b, &tests)
+	if e != nil {
+		fmt.Printf("\nFound errors loading authorization tests definition:\n")
+		fmt.Println("─────────────────────────────────────────────────────────────")
+		fmt.Printf("%+v\n", aurora.Red(e))
+		return 1
+	}
+
+	var apiCfg map[string]interface{}
+	func() {
+		defer func() {
+			if r := recover(); r != nil {
+				e = r.(error)
+				fmt.Printf("\nFound errors injecting tests into authorization rules:\n")
+				fmt.Println("─────────────────────────────────────────────────────────────")
+				fmt.Printf("%+v\n", aurora.Red(e))
+			}
+		}()
+		apiCfg = dipper.MustGetMapData(cfg.DataSet.Drivers, "daemon.services.api").(map[string]interface{})
+		casbinCfg := dipper.MustGetMapData(apiCfg, "auth.casbin").(map[string]interface{})
+		casbinCfg["models"] = append(casbinCfg["models"].([]interface{}), tests.Models...)
+		casbinCfg["policies"] = append(casbinCfg["policies"].([]interface{}), tests.Policies...)
+	}()
+	if e != nil {
+		return 1
+	}
+
+	l := api.NewStore(nil)
+	func() {
+		defer func() {
+			if r := recover(); r != nil {
+				e = r.(error)
+				fmt.Printf("\nFound errors loading authorization rules:\n")
+				fmt.Println("─────────────────────────────────────────────────────────────")
+				fmt.Printf("%+v\n", aurora.Red(e))
+			}
+		}()
+		l.GetAPIHandler("/api", apiCfg)
+	}()
+	if e != nil {
+		return 1
+	}
+
+	failedTests := []int{}
+	processed := 0
+	for i, test := range tests.Tests {
+		ok, err := l.Enforce(test.Subject, test.Object, test.Action)
+		if err != nil {
+			e = err
+			processed = i
+			break
+		}
+		if ok != test.Result {
+			failedTests = append(failedTests, i)
+		}
+	}
+
+	if e != nil || len(failedTests) > 0 {
+		fmt.Printf("\nFound errors running authorization tests:\n")
+		fmt.Println("─────────────────────────────────────────────────────────────")
+		for _, num := range failedTests {
+			test := tests.Tests[num]
+			fmt.Printf(
+				"%s: Sub: %s, Obj: %s, Act: %s, Expected: %t, Found: %t\n",
+				aurora.Yellow(test.Name),
+				test.Subject,
+				test.Object,
+				test.Action,
+				aurora.BrightYellow(test.Result),
+				aurora.Red(!test.Result),
+			)
+		}
+		if e != nil {
+			test := tests.Tests[processed]
+			fmt.Printf(
+				"%s: Sub: %s, Obj: %s, Act: %s, Expected: %t, Error: %+v\n",
+				aurora.Yellow(test.Name),
+				test.Subject,
+				test.Object,
+				test.Action,
+				test.Result,
+				aurora.Red(e),
+			)
+		}
+		fmt.Println()
+		return 1
+	}
+
+	return 0
+}
+
 func checkContext(cfg *config.Config, ctxWorkflowName string) (msg string) {
 	defer func() {
 		if r := recover(); r != nil {
diff --git a/drivers/cmd/auth-simple/main.go b/drivers/cmd/auth-simple/main.go
index 1dcf4ad..dad41cc 100644
--- a/drivers/cmd/auth-simple/main.go
+++ b/drivers/cmd/auth-simple/main.go
@@ -32,7 +32,7 @@ var (
 	ErrSkipped = errors.New("skipped")
 
 	// EmptySubject is used for an authenticated user without a defined subject.
-	_EmptySubject = map[string]interface{}{}
+	_EmptySubject = ""
 )
 
 func initFlags() {
@@ -66,7 +66,7 @@ func authWebRequest(m *dipper.Message) {
 	}
 
 	var err error
-	var subject map[string]interface{}
+	var subject string
 	for _, scheme := range schemes {
 		switch scheme.(string) {
 		case "basic":
@@ -88,14 +88,14 @@ func authWebRequest(m *dipper.Message) {
 	panic(err)
 }
 
-func tokenAuth(m *dipper.Message) (map[string]interface{}, error) {
+func tokenAuth(m *dipper.Message) (string, error) {
 	const prefix = "bearer "
 	authHash, ok := dipper.GetMapDataStr(m.Payload, "headers.Authorization.0")
 	if !ok {
 		authHash, ok = dipper.GetMapDataStr(m.Payload, "headers.authorization.0")
-		if !ok || len(authHash) < len(prefix) || !strings.EqualFold(authHash[:len(prefix)], prefix) {
-			return nil, ErrSkipped
-		}
+	}
+	if !ok || len(authHash) < len(prefix) || !strings.EqualFold(authHash[:len(prefix)], prefix) {
+		return _EmptySubject, ErrSkipped
 	}
 	token := []byte(authHash[len(prefix):])
 
@@ -112,22 +112,22 @@ func tokenAuth(m *dipper.Message) (map[string]interface{}, error) {
 				if !ok || subject == nil {
 					return _EmptySubject, nil
 				}
-				return subject.(map[string]interface{}), nil
+				return subject.(string), nil
 			}
 		}
 	}
 
-	return nil, ErrInvalidBearerToken
+	return _EmptySubject, ErrInvalidBearerToken
 }
 
-func basicAuth(m *dipper.Message) (map[string]interface{}, error) {
+func basicAuth(m *dipper.Message) (string, error) {
 	const prefix = "basic "
 	authHash, ok := dipper.GetMapDataStr(m.Payload, "headers.Authorization.0")
 	if !ok {
 		authHash, ok = dipper.GetMapDataStr(m.Payload, "headers.authorization.0")
-		if !ok || len(authHash) < len(prefix) || !strings.EqualFold(authHash[:len(prefix)], prefix) {
-			return nil, ErrSkipped
-		}
+	}
+	if !ok || len(authHash) < len(prefix) || !strings.EqualFold(authHash[:len(prefix)], prefix) {
+		return _EmptySubject, ErrSkipped
 	}
 	req := &http.Request{
 		Header: http.Header{
@@ -136,7 +136,7 @@ func basicAuth(m *dipper.Message) (map[string]interface{}, error) {
 	}
 	user, pass, ok := req.BasicAuth()
 	if !ok {
-		return nil, ErrInvalidBasicAuth
+		return _EmptySubject, ErrInvalidBasicAuth
 	}
 	passBytes := []byte(pass)
 
@@ -153,10 +153,10 @@ func basicAuth(m *dipper.Message) (map[string]interface{}, error) {
 				if !ok || subject == nil {
 					return _EmptySubject, nil
 				}
-				return subject.(map[string]interface{}), nil
+				return subject.(string), nil
 			}
 		}
 	}
 
-	return nil, ErrInvalidBasicCreds
+	return _EmptySubject, ErrInvalidBasicCreds
 }
diff --git a/go.mod b/go.mod
index 6a3713c..23e405c 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/honeydipper/honeydipper
 go 1.13
 
 require (
-	cloud.google.com/go v0.62.0
+	cloud.google.com/go v0.65.0
 	cloud.google.com/go/pubsub v1.3.1
 	cloud.google.com/go/spanner v1.6.0
 	cloud.google.com/go/storage v1.10.0
@@ -11,6 +11,7 @@ require (
 	github.com/Masterminds/goutils v1.1.0 // indirect
 	github.com/Masterminds/semver v1.5.0 // indirect
 	github.com/Masterminds/sprig v2.22.0+incompatible
+	github.com/casbin/casbin/v2 v2.11.3
 	github.com/ghodss/yaml v1.0.0
 	github.com/gin-gonic/gin v1.6.3
 	github.com/go-errors/errors v1.1.1
@@ -26,11 +27,12 @@ require (
 	github.com/mitchellh/mapstructure v1.3.2
 	github.com/mitchellh/reflectwalk v1.0.1 // indirect
 	github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
+	github.com/qiangmzsx/string-adapter/v2 v2.1.0
 	github.com/stretchr/testify v1.6.1
-	golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
-	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
-	google.golang.org/api v0.29.0
-	google.golang.org/genproto v0.0.0-20200731012542-8145dea6a485
+	golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
+	golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
+	google.golang.org/api v0.30.0
+	google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d
 	gopkg.in/h2non/gock.v1 v1.0.15
 	gopkg.in/src-d/go-git.v4 v4.13.1
 	gopkg.in/yaml.v2 v2.3.0 // indirect
diff --git a/go.sum b/go.sum
index 860bd9a..3feb936 100644
--- a/go.sum
+++ b/go.sum
@@ -18,6 +18,8 @@ cloud.google.com/go v0.57.0 h1:EpMNVUorLiZIELdMZbCYX/ByTFCdoYopYAGxaGVz9ms=
 cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
 cloud.google.com/go v0.62.0 h1:RmDygqvj27Zf3fCQjQRtLyC7KwFcHkeJitcO0OoGOcA=
 cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
 cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@@ -59,6 +61,8 @@ github.com/DataDog/datadog-go v3.7.2+incompatible h1:o4QtYjBU/rG58VPh8Ne6F65YiMY
 github.com/DataDog/datadog-go v3.7.2+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
 github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7 h1:qELHH0AWCvf98Yf+CNIJx9vOZOfHFDDzgDRYsnNk/vs=
 github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60=
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
 github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
 github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
 github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
@@ -76,6 +80,10 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
 github.com/benbjohnson/clock v1.0.3 h1:vkLuvpK4fmtSCuo60+yC63p7y0BmQ8gm5ZXGuBCJyXg=
 github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
+github.com/casbin/casbin/v2 v2.1.0 h1:FqE47qR7PNFrhh/mQFRqlXWdAM0lObvn/cl8ydyxi1c=
+github.com/casbin/casbin/v2 v2.1.0/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
+github.com/casbin/casbin/v2 v2.11.3 h1:nGpcPrD8KVhKjla29qGqe6ijKGD3Qkn+jP3S5hvgZ9U=
+github.com/casbin/casbin/v2 v2.11.3/go.mod h1:XXtYGrs/0zlOsJMeRteEdVi/FsB0ph7KgNfjoCoJUD8=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -299,6 +307,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/qiangmzsx/string-adapter/v2 v2.1.0 h1:q0y8TPa/sTwtriJPRe8gWL++PuZ+XbOUuvKU+hvtTYs=
+github.com/qiangmzsx/string-adapter/v2 v2.1.0/go.mod h1:PElPB7b7HnGKTsuADAffFpOQXHqjEGJz1+U1a6yR5wA=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
@@ -341,8 +351,8 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
-golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
+golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -407,12 +417,16 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -454,6 +468,8 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o=
 golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -508,10 +524,15 @@ golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roY
 golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7 h1:LHW24ah7B+uV/OePwNP0p/t889F3QSyLvY8Sg/bK0SY=
 golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -529,6 +550,8 @@ google.golang.org/api v0.25.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
 google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
 google.golang.org/api v0.29.0 h1:BaiDisFir8O4IJxvAabCGGkQ6yCJegNQqSVoYUNAnbk=
 google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -565,8 +588,10 @@ google.golang.org/genproto v0.0.0-20200526151428-9bb895338b15/go.mod h1:YsZOwe1m
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
 google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200731012542-8145dea6a485 h1:wTk5DQB3+1darAz4Ldomo0r5bUOCKX7gilxQ4sb2kno=
-google.golang.org/genproto v0.0.0-20200731012542-8145dea6a485/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d h1:92D1fum1bJLKSdr11OJ+54YeCMCGYIygTA7R/YZxH5M=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -579,6 +604,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
 google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE=
 google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
diff --git a/internal/api/def.go b/internal/api/def.go
index 36bd2dd..deb65b9 100644
--- a/internal/api/def.go
+++ b/internal/api/def.go
@@ -13,13 +13,14 @@ import (
 
 // Def is a structure defines how an API should be handled in api service.
 type Def struct {
-	path       string
-	name       string
-	method     string
-	reqType    int
-	service    string
-	ackTimeout time.Duration
-	timeout    time.Duration
+	Path       string
+	Object     string
+	Name       string
+	Method     string
+	ReqType    int
+	Service    string
+	AckTimeout time.Duration
+	Timeout    time.Duration
 }
 
 const (
@@ -37,12 +38,12 @@ const (
 // GetDefs return definition for all known API calls.
 func GetDefs() map[string]map[string]Def {
 	return map[string]map[string]Def{
-		"/events/:eventID/wait": {
-			http.MethodGet: {name: "eventWait", reqType: TypeMatch, service: "engine", timeout: InfiniteDuration},
+		"events/:eventID/wait": {
+			http.MethodGet: {Object: "event", Name: "eventWait", ReqType: TypeMatch, Service: "engine", Timeout: InfiniteDuration},
 		},
-		"/events": {
-			http.MethodGet:  {name: "eventList", reqType: TypeAll, service: "engine"},
-			http.MethodPost: {name: "eventAdd", reqType: TypeFirst, service: "receiver"},
+		"events": {
+			http.MethodGet:  {Object: "event", Name: "eventList", ReqType: TypeAll, Service: "engine"},
+			http.MethodPost: {Object: "event", Name: "eventAdd", ReqType: TypeFirst, Service: "receiver"},
 		},
 	}
 }
@@ -52,9 +53,9 @@ func GetDefsByName() map[string]Def {
 	ret := map[string]Def{}
 	for path, defs := range GetDefs() {
 		for method, def := range defs {
-			def.path = path
-			def.method = method
-			ret[def.name] = def
+			def.Path = path
+			def.Method = method
+			ret[def.Name] = def
 		}
 	}
 	return ret
diff --git a/internal/api/mock_api/request_context.go b/internal/api/mock_api/request_context.go
index a406490..4cb6e50 100644
--- a/internal/api/mock_api/request_context.go
+++ b/internal/api/mock_api/request_context.go
@@ -12,8 +12,9 @@
 package mock_api
 
 import (
-	gomock "github.com/golang/mock/gomock"
 	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
 )
 
 // MockRequestContext is a mock of RequestContext interface.
diff --git a/internal/api/request.go b/internal/api/request.go
index e66effe..332c873 100644
--- a/internal/api/request.go
+++ b/internal/api/request.go
@@ -47,6 +47,10 @@ type Request struct {
 // Dispatch sends the call to the intended services.
 func (a *Request) Dispatch() {
 	if a.ready == nil {
+		a.ready = make(chan byte)
+		a.results = map[string]interface{}{}
+		a.store.SaveRequest(a)
+
 		dipper.Must(a.store.caller.Call("api-broadcast", "send", map[string]interface{}{
 			"broadcastSubject": "call",
 			"labels": map[string]interface{}{
@@ -57,9 +61,6 @@ func (a *Request) Dispatch() {
 			},
 			"data": a.params,
 		}))
-		a.ready = make(chan byte)
-		a.results = map[string]interface{}{}
-		a.store.SaveRequest(a)
 
 		go func() {
 			defer dipper.SafeExitOnError("error waiting on acks for API call [%s]", a.uuid)
diff --git a/internal/api/response.go b/internal/api/response.go
index f06a4ea..72d9cc3 100644
--- a/internal/api/response.go
+++ b/internal/api/response.go
@@ -105,7 +105,7 @@ func (rf *ResponseFactory) NewResponse(caller dipper.RPCCaller, eventbus io.Writ
 		dipper.Logger.Warningf("Unknown API method: %s", method)
 		return nil
 	}
-	switch def.reqType {
+	switch def.ReqType {
 	case TypeAll:
 		go func() {
 			defer dipper.SafeExitOnError("failed to send ack for api [%s]", method)
diff --git a/internal/api/store.go b/internal/api/store.go
index f559335..c49f10c 100644
--- a/internal/api/store.go
+++ b/internal/api/store.go
@@ -14,8 +14,11 @@ import (
 	"sync"
 	"time"
 
+	"github.com/casbin/casbin/v2"
+	"github.com/casbin/casbin/v2/model"
 	"github.com/gin-gonic/gin"
 	"github.com/honeydipper/honeydipper/pkg/dipper"
+	scas "github.com/qiangmzsx/string-adapter/v2"
 )
 
 const (
@@ -44,6 +47,7 @@ type Store struct {
 	config          interface{}
 	apiDef          map[string]map[string]Def
 	newUUID         dipper.UUIDSource
+	enforcer        *casbin.Enforcer
 
 	writeTimeout time.Duration
 }
@@ -127,8 +131,8 @@ func NewStore(c dipper.RPCCaller) *Store {
 	return store
 }
 
-// PrepareHTTPServer prepares the provided http server.
-func (l *Store) PrepareHTTPServer(s *http.Server, cfg interface{}) {
+// GetAPIHandler prepares and returns the gin Engine for API.
+func (l *Store) GetAPIHandler(prefix string, cfg interface{}) http.Handler {
 	gin.DefaultWriter = dipper.LoggingWriter
 	l.config = cfg
 	l.engine = gin.New()
@@ -141,8 +145,33 @@ func (l *Store) PrepareHTTPServer(s *http.Server, cfg interface{}) {
 		l.writeTimeout = dipper.Must(time.ParseDuration(writeTimeoutStr)).(time.Duration)
 	}
 
-	l.setupRoutes()
-	s.Handler = l.engine
+	l.setupRoutes(prefix)
+	l.setupAuthorization()
+
+	return l.engine
+}
+
+// setupAuthorization sets up authorization enforcer.
+func (l *Store) setupAuthorization() {
+	modelList := dipper.MustGetMapData(l.config, "auth.casbin.models").([]interface{})
+	modelText := make([]string, len(modelList))
+	for i, line := range modelList {
+		modelText[i] = line.(string)
+	}
+	policyList := dipper.MustGetMapData(l.config, "auth.casbin.policies").([]interface{})
+	policyText := make([]string, len(policyList))
+	for i, line := range dipper.MustGetMapData(l.config, "auth.casbin.policies").([]interface{}) {
+		policyText[i] = line.(string)
+	}
+	models := model.NewModel()
+	dipper.Must(models.LoadModelFromText(strings.Join(modelText, "\n")))
+	policies := scas.NewAdapter(strings.Join(policyText, "\n"))
+	l.enforcer = dipper.Must(casbin.NewEnforcer(models, policies)).(*casbin.Enforcer)
+}
+
+// Enforce checks if the action is allowed based on rules.
+func (l *Store) Enforce(args ...interface{}) (bool, error) {
+	return l.enforcer.Enforce(args...)
 }
 
 // AuthMiddleware is a middleware handles auth.
@@ -183,35 +212,13 @@ func (l *Store) Authorize(c RequestContext, def Def) bool {
 		return false
 	}
 
-	rulesObj, ok := dipper.GetMapData(l.config, "acls."+def.name)
-	if !ok {
-		return false
-	}
-
-	rules, ok := rulesObj.([]interface{})
-	if !ok {
-		return false
-	}
-
-	action := ACLDeny
-	for _, ruleObj := range rules {
-		rule := ruleObj.(map[string]interface{})
-		switch v := rule["subjects"].(type) {
-		case string:
-			if v != "all" {
-				panic(fmt.Errorf("%w: unknown subjects: %s", APIError, v))
-			}
-			action = rule["type"].(string)
-		case []interface{}:
-			if dipper.CompareAll(subject, v) {
-				return rule["type"].(string) == ACLAllow
-			}
-		default:
-			panic(fmt.Errorf("%w: unknown subjects: %+v", APIError, v))
-		}
+	dipper.Logger.Warningf("'%s' , '%s', '%s'", subject, def.Object, def.Method)
+	if res, err := l.enforcer.Enforce(subject.(string), def.Object, def.Method); res && err == nil {
+		return true
+	} else if err != nil {
+		dipper.Logger.Warningf("[api] denied access with enforcer error: %+v", err)
 	}
-
-	return action == ACLAllow
+	return false
 }
 
 // HandleHTTPRequest handles http requests.
@@ -255,16 +262,20 @@ func (l *Store) CreateHTTPHandlerFunc(def Def) gin.HandlerFunc {
 }
 
 // setupRoutes sets up the routes.
-func (l *Store) setupRoutes() {
+func (l *Store) setupRoutes(prefix string) {
+	var group *gin.RouterGroup = &l.engine.RouterGroup
+	if prefix != "" {
+		group = group.Group(prefix)
+	}
 	for path, defs := range l.apiDef {
 		for method, def := range defs {
-			def.method = method
-			def.path = path
+			def.Method = method
+			def.Path = path
 			switch method {
 			case http.MethodGet:
-				l.engine.GET(path, l.CreateHTTPHandlerFunc(def))
+				group.GET(path, l.CreateHTTPHandlerFunc(def))
 			case http.MethodPost:
-				l.engine.POST(path, l.CreateHTTPHandlerFunc(def))
+				group.POST(path, l.CreateHTTPHandlerFunc(def))
 			}
 		}
 	}
@@ -289,24 +300,24 @@ func (l *Store) SaveRequest(r *Request) {
 // GetRequest creates a new Request with the given definition and parameters or return an existing one based on uuid.
 func (l *Store) GetRequest(def Def, c RequestContext) *Request {
 	path := c.GetPath()
-	if def.method == http.MethodGet {
+	if def.Method == http.MethodGet {
 		if req, ok := l.requestsByInput.Load(path); ok && req != nil {
 			return req.(*Request)
 		}
 	}
 
 	// prepare the parameters
-	payload := c.GetPayload(def.method)
+	payload := c.GetPayload(def.Method)
 
 	return &Request{
 		store:       l,
 		uuid:        l.newUUID(),
 		urlPath:     path,
-		method:      def.method,
-		fn:          def.name,
+		method:      def.Method,
+		fn:          def.Name,
 		params:      payload,
-		reqType:     def.reqType,
-		service:     def.service,
+		reqType:     def.ReqType,
+		service:     def.Service,
 		ackTimeout:  l.getAckTimeout(def),
 		timeout:     l.getTimeout(def),
 		contentType: c.ContentType(),
@@ -315,8 +326,8 @@ func (l *Store) GetRequest(def Def, c RequestContext) *Request {
 
 // get the ackTimeout with default value.
 func (l *Store) getAckTimeout(d Def) time.Duration {
-	if d.ackTimeout != 0 {
-		return d.ackTimeout
+	if d.AckTimeout != 0 {
+		return d.AckTimeout
 	}
 
 	timeoutStr, ok := dipper.GetMapDataStr(l.config, "ack_timeout")
@@ -329,8 +340,8 @@ func (l *Store) getAckTimeout(d Def) time.Duration {
 
 // get the timeout with default value.
 func (l *Store) getTimeout(d Def) time.Duration {
-	if d.timeout != 0 {
-		return d.timeout
+	if d.Timeout != 0 {
+		return d.Timeout
 	}
 
 	timeoutStr, ok := dipper.GetMapDataStr(l.config, "timeout")
diff --git a/internal/config/repo.go b/internal/config/repo.go
index fe00aa7..6efd9b2 100644
--- a/internal/config/repo.go
+++ b/internal/config/repo.go
@@ -55,6 +55,11 @@ func (c *Repo) isFileLoaded(filename string) bool {
 	return c.files[filename]
 }
 
+// ReadFile reads a file from the repo.
+func (c *Repo) ReadFile(filename string) ([]byte, error) {
+	return ioutil.ReadFile(path.Join(c.root, filename[1:]))
+}
+
 func (c *Repo) loadFile(filename string) {
 	defer c.recovering(filename, "")
 
diff --git a/internal/service/api.go b/internal/service/api.go
index 9a0b9a7..bd07023 100644
--- a/internal/service/api.go
+++ b/internal/service/api.go
@@ -65,10 +65,27 @@ func startAPIListener() {
 	if !ok {
 		addr = ":9000"
 	}
+	prefix, ok := dipper.GetMapDataStr(APICfg, "api_prefix")
+	if !ok || prefix == "" {
+		prefix = "/api/"
+	}
+	healthcheckPrefix, ok := dipper.GetMapDataStr(APICfg, "healthcheck_prefix")
+	if !ok || healthcheckPrefix == "" {
+		healthcheckPrefix = "/healthz"
+	}
+	mux := http.NewServeMux()
+	mux.Handle(prefix, APIRequestStore.GetAPIHandler(prefix, APICfg))
+	mux.HandleFunc(healthcheckPrefix, func(w http.ResponseWriter, r *http.Request) {
+		if API.CheckHealth() {
+			w.WriteHeader(http.StatusOK)
+		} else {
+			w.WriteHeader(http.StatusInternalServerError)
+		}
+	})
 	APIServer = &http.Server{
-		Addr: addr,
+		Addr:    addr,
+		Handler: mux,
 	}
-	APIRequestStore.PrepareHTTPServer(APIServer, APICfg)
 	go func() {
 		dipper.Logger.Infof("[api] start listening for webhook requests")
 		dipper.Logger.Warningf("[api] listener stopped: %+v", APIServer.ListenAndServe())
diff --git a/internal/service/service.go b/internal/service/service.go
index 24c9c48..a431fd6 100644
--- a/internal/service/service.go
+++ b/internal/service/service.go
@@ -76,6 +76,7 @@ type Service struct {
 	EmitMetrics        func()
 	APIs               map[string]func(*api.Response)
 	ResponseFactory    *api.ResponseFactory
+	healthy            bool
 }
 
 // Services holds a catalog of running services in this daemon process.
@@ -104,6 +105,11 @@ func NewService(cfg *config.Config, name string) *Service {
 	return svc
 }
 
+// CheckHealth is a http handler for healthcheck.
+func (s *Service) CheckHealth() bool {
+	return s.healthy
+}
+
 // GetName returns the name of the service.
 func (s *Service) GetName() string {
 	return s.name
@@ -268,6 +274,7 @@ func (s *Service) start() {
 		go s.serviceLoop()
 		time.Sleep(time.Second)
 		s.loadAdditionalFeatures(featureList)
+		s.healthy = true
 		go s.metricsLoop()
 	}()
 }
@@ -280,6 +287,7 @@ func (s *Service) Reload() {
 	func() {
 		defer func() {
 			if r := recover(); r != nil {
+				s.healthy = false
 				dipper.Logger.Errorf("[%s] reverting config due to fatal failure %v", s.name, r)
 				s.config.RollBack()
 			}
@@ -292,6 +300,7 @@ func (s *Service) Reload() {
 	}()
 
 	s.loadAdditionalFeatures(featureList)
+	s.healthy = true
 	s.removeUnusedFeatures(featureList)
 }
 
@@ -484,6 +493,8 @@ func (s *Service) serviceLoop() {
 		}
 	}
 
+	s.healthy = false
+
 	for fname, runtime := range s.driverRuntimes {
 		func() {
 			defer dipper.SafeExitOnError("[%s] driver runtime for feature %s already closed", s.name, fname)
diff --git a/pkg/dipper/mock_dipper/rpc.go b/pkg/dipper/mock_dipper/rpc.go
index a2308bb..9dc1264 100644
--- a/pkg/dipper/mock_dipper/rpc.go
+++ b/pkg/dipper/mock_dipper/rpc.go
@@ -12,9 +12,10 @@
 package mock_dipper
 
 import (
-	gomock "github.com/golang/mock/gomock"
 	io "io"
 	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
 )
 
 // MockRPCCallerStub is a mock of RPCCallerStub interface.


Test patch:
diff --git a/internal/api/main_test.go b/internal/api/main_test.go
index 11e6c70..2c277c4 100644
--- a/internal/api/main_test.go
+++ b/internal/api/main_test.go
@@ -27,14 +27,14 @@ func TestMain(m *testing.M) {
 }
 
 type TestStep struct {
-	feature         string
-	method          string
-	expectedMessage interface{}
-	returnMessage   interface{}
-	err             error
+	Feature         string
+	Method          string
+	ExpectedMessage interface{}
+	ReturnMessage   interface{}
+	Err             error
 }
 
 type ReturnMessage struct {
-	delay time.Duration
-	msg   *dipper.Message
+	Delay time.Duration
+	Msg   *dipper.Message
 }
diff --git a/internal/api/request_test.go b/internal/api/request_test.go
index 0166820..0a244cc 100644
--- a/internal/api/request_test.go
+++ b/internal/api/request_test.go
@@ -9,48 +9,69 @@
 package api
 
 import (
+	"fmt"
+	"io/ioutil"
 	"testing"
 	"time"
 
+	"github.com/ghodss/yaml"
 	"github.com/golang/mock/gomock"
 	"github.com/honeydipper/honeydipper/internal/api/mock_api"
 	"github.com/honeydipper/honeydipper/pkg/dipper"
 	"github.com/honeydipper/honeydipper/pkg/dipper/mock_dipper"
+	"github.com/imdario/mergo"
+	"github.com/mitchellh/mapstructure"
 	"github.com/stretchr/testify/assert"
 )
 
 type RequestTestCase struct {
-	subject         map[string]interface{}
-	contentType     string
-	path            string
-	payload         map[string]interface{}
-	steps           []TestStep
-	returns         []ReturnMessage
-	expectedCode    int
-	expectedContent map[string]interface{}
-	config          interface{}
-	def             Def
-	uuids           []string
-	shouldAuthorize bool
+	Subject         string
+	ContentType     string `json:"content-type" mapstructure:"content-type"`
+	Path            string
+	Payload         map[string]interface{}
+	Steps           []TestStep
+	Returns         []ReturnMessage
+	ExpectedCode    int
+	ExpectedContent map[string]interface{}
+	Config          interface{}
+	Def             Def
+	UUIDs           []string `json:"uuids" mapstructure:"uuids"`
+	ShouldAuthorize bool
 }
 
-func requestTest(t *testing.T, c *RequestTestCase) *Store {
+func requestTest(t *testing.T, caseName string) (*Store, *RequestTestCase) {
+	var buffer, delta map[string]interface{}
+	dipper.Must(yaml.Unmarshal(dipper.Must(ioutil.ReadFile("test_fixtures/common.yaml")).([]byte), &buffer))
+	dipper.Must(yaml.Unmarshal(dipper.Must(ioutil.ReadFile(fmt.Sprintf("test_fixtures/%s.yaml", caseName))).([]byte), &delta))
+	dipper.Must(mergo.Merge(&buffer, delta, mergo.WithOverride))
+
+	c := &RequestTestCase{}
+	dipper.Must(mapstructure.Decode(buffer, c))
+
+	// convert all times from test definition to milliseconds
+	c.Def.AckTimeout = c.Def.AckTimeout * time.Millisecond
+	c.Def.Timeout = c.Def.Timeout * time.Millisecond
+	for i := range c.Returns {
+		c.Returns[i].Delay = c.Returns[i].Delay * time.Millisecond
+	}
+
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
 	mockReqCtx := mock_api.NewMockRequestContext(ctrl)
-	mockReqCtx.EXPECT().Get(gomock.Eq("subject")).Times(1).Return(c.subject, c.subject != nil)
-	if c.shouldAuthorize {
-		mockReqCtx.EXPECT().GetPath().Times(1).Return(c.path)
-		mockReqCtx.EXPECT().GetPayload(gomock.Eq(c.def.method)).Times(1).Return(c.payload)
-		mockReqCtx.EXPECT().ContentType().Times(1).Return(c.contentType)
+	mockReqCtx.EXPECT().Get(gomock.Eq("subject")).Times(1).Return(c.Subject, c.Subject != "")
+	if c.ShouldAuthorize {
+		mockReqCtx.EXPECT().GetPath().Times(1).Return(c.Path)
+		mockReqCtx.EXPECT().GetPayload(gomock.Eq(c.Def.Method)).Times(1).Return(c.Payload)
+		mockReqCtx.EXPECT().ContentType().Times(1).Return(c.ContentType)
 	}
 
 	mockRPCCaller := mock_dipper.NewMockRPCCaller(ctrl)
 	l := NewStore(mockRPCCaller)
-	l.config = c.config
+	l.config = c.Config
+	l.setupAuthorization()
 
-	uuids := c.uuids
+	uuids := c.UUIDs
 	nextUUID := func() string {
 		uuid := uuids[0]
 		uuids = uuids[1:]
@@ -58,559 +79,82 @@ func requestTest(t *testing.T, c *RequestTestCase) *Store {
 	}
 	l.newUUID = nextUUID
 
-	if wt, ok := dipper.GetMapData(c.config, "writeTimeout"); ok {
-		l.writeTimeout = wt.(time.Duration)
+	if wt, ok := dipper.GetMapData(c.Config, "writeTimeout"); ok {
+		l.writeTimeout = time.Millisecond * time.Duration(wt.(float64))
 	} else {
 		l.writeTimeout = time.Millisecond * 100
 	}
 
-	for _, st := range c.steps {
-		mockRPCCaller.EXPECT().Call(gomock.Eq(st.feature), gomock.Eq(st.method), gomock.Eq(st.expectedMessage)).Times(1).Return(st.returnMessage, st.err)
+	startSignal := make(chan struct{})
+	started := false
+	for _, st := range c.Steps {
+		mockRPCCaller.EXPECT().Call(gomock.Eq(st.Feature), gomock.Eq(st.Method), gomock.Eq(st.ExpectedMessage)).Times(1).DoAndReturn(func(_, _ string, _ map[string]interface{}) (interface{}, error) {
+			if !started {
+				started = true
+				close(startSignal)
+			}
+			return st.ReturnMessage, st.Err
+		})
 	}
 
-	if c.expectedCode >= 400 {
-		mockReqCtx.EXPECT().AbortWithStatusJSON(gomock.Eq(c.expectedCode), gomock.Eq(c.expectedContent)).Times(1)
+	if c.ExpectedCode >= 400 {
+		mockReqCtx.EXPECT().AbortWithStatusJSON(gomock.Eq(c.ExpectedCode), gomock.Eq(c.ExpectedContent)).Times(1)
 	} else {
-		mockReqCtx.EXPECT().IndentedJSON(gomock.Eq(c.expectedCode), gomock.Eq(c.expectedContent)).Times(1)
+		mockReqCtx.EXPECT().IndentedJSON(gomock.Eq(c.ExpectedCode), gomock.Eq(c.ExpectedContent)).Times(1)
 	}
 
 	go func() {
-		for _, st := range c.returns {
-			time.Sleep(st.delay)
-			switch st.msg.Labels["type"] {
+		<-startSignal
+		for _, st := range c.Returns {
+			dipper.Logger.Warning("delaying %d ms", st.Delay)
+			time.Sleep(st.Delay)
+			switch st.Msg.Labels["type"] {
 			case "ack":
-				l.HandleAPIACK(st.msg)
+				l.HandleAPIACK(st.Msg)
 			case "result":
-				l.HandleAPIReturn(st.msg)
+				l.HandleAPIReturn(st.Msg)
 			}
 		}
 	}()
 
-	l.HandleHTTPRequest(mockReqCtx, c.def)
-	return l
+	l.HandleHTTPRequest(mockReqCtx, c.Def)
+	return l, c
 }
 
 func TestTypeAllAPI(t *testing.T) {
-	uuids := []string{"34ik-ijo3i4jt84932-aiau3kegkjrl"}
-	c := &RequestTestCase{
-		// cfg and api definition
-		config: map[string]interface{}{
-			"acls": map[string]interface{}{
-				"test_type_all": []interface{}{
-					map[string]interface{}{
-						"subjects": []interface{}{
-							map[string]interface{}{"role": "any"},
-						},
-						"type": "allow",
-					},
-				},
-			},
-		},
-		def: Def{
-			path:    "/test_type_all",
-			name:    "test_type_all",
-			method:  "GET",
-			reqType: TypeAll,
-			service: "foo",
-		},
-		shouldAuthorize: true,
-
-		// input
-		uuids:       uuids,
-		subject:     map[string]interface{}{"role": "any"},
-		contentType: "application/json",
-		path:        "/test_type_all",
-		payload:     map[string]interface{}{},
-
-		steps: []TestStep{
-			{
-				feature: "api-broadcast",
-				method:  "send",
-				expectedMessage: map[string]interface{}{
-					"broadcastSubject": "call",
-					"labels": map[string]interface{}{
-						"fn":           "test_type_all",
-						"uuid":         uuids[0],
-						"service":      "foo",
-						"content-type": "application/json",
-					},
-					"data": map[string]interface{}{},
-				},
-				returnMessage: nil,
-			},
-		},
-
-		// returned message from service
-		returns: []ReturnMessage{
-			{
-				delay: time.Millisecond,
-				msg: &dipper.Message{
-					Labels: map[string]string{
-						"type": "ack",
-						"uuid": uuids[0],
-						"from": "bar",
-					},
-				},
-			},
-			{
-				delay: time.Millisecond,
-				msg: &dipper.Message{
-					Labels: map[string]string{
-						"type": "result",
-						"uuid": uuids[0],
-						"from": "bar",
-					},
-					Payload: map[string]interface{}{
-						"result": "all",
-					},
-				},
-			},
-		},
-
-		// expected result
-		expectedCode:    200,
-		expectedContent: map[string]interface{}{"bar": map[string]interface{}{"result": "all"}},
-	}
-
-	requestTest(t, c)
+	requestTest(t, "TypeAllAPI")
 }
 
 func TestTypeFirstAPI(t *testing.T) {
-	uuids := []string{"34ik-ijo3i4jt84932-aiau3kegkjrl"}
-	c := &RequestTestCase{
-		// cfg and api definition
-		config: map[string]interface{}{
-			"acls": map[string]interface{}{
-				"test_type_first": []interface{}{
-					map[string]interface{}{
-						"subjects": []interface{}{
-							map[string]interface{}{"role": "any"},
-						},
-						"type": "allow",
-					},
-				},
-			},
-		},
-		def: Def{
-			path:    "/test_type_first",
-			name:    "test_type_first",
-			method:  "GET",
-			reqType: TypeFirst,
-			service: "foo",
-		},
-		shouldAuthorize: true,
-
-		// input
-		uuids:       uuids,
-		subject:     map[string]interface{}{"role": "any"},
-		contentType: "application/json",
-		path:        "/test_type_first",
-		payload:     map[string]interface{}{},
-
-		steps: []TestStep{
-			{
-				feature: "api-broadcast",
-				method:  "send",
-				expectedMessage: map[string]interface{}{
-					"broadcastSubject": "call",
-					"labels": map[string]interface{}{
-						"fn":           "test_type_first",
-						"uuid":         uuids[0],
-						"service":      "foo",
-						"content-type": "application/json",
-					},
-					"data": map[string]interface{}{},
-				},
-				returnMessage: nil,
-			},
-		},
-
-		// returned message from service
-		returns: []ReturnMessage{
-			{
-				delay: 2 * time.Millisecond,
-				msg: &dipper.Message{
-					Labels: map[string]string{
-						"type": "result",
-						"uuid": uuids[0],
-						"from": "bar",
-					},
-					Payload: map[string]interface{}{
-						"result": "all",
-					},
-				},
-			},
-		},
-
-		// expected result
-		expectedCode:    200,
-		expectedContent: map[string]interface{}{"bar": map[string]interface{}{"result": "all"}},
-	}
-
-	requestTest(t, c)
+	requestTest(t, "TypeFirstAPI")
 }
 
 func TestTypeMatchAPI(t *testing.T) {
-	uuids := []string{"34ik-ijo3i4jt84932-aiau3kegkjrl"}
-	c := &RequestTestCase{
-		// cfg and api definition
-		config: map[string]interface{}{
-			"acls": map[string]interface{}{
-				"test_type_match": []interface{}{
-					map[string]interface{}{
-						"subjects": []interface{}{
-							map[string]interface{}{"role": "any"},
-						},
-						"type": "allow",
-					},
-				},
-			},
-		},
-		def: Def{
-			path:    "/test_type_match",
-			name:    "test_type_match",
-			method:  "GET",
-			reqType: TypeMatch,
-			service: "foo",
-		},
-		shouldAuthorize: true,
-
-		// input
-		uuids:       uuids,
-		subject:     map[string]interface{}{"role": "any"},
-		contentType: "application/json",
-		path:        "/test_type_match",
-		payload:     map[string]interface{}{},
-
-		steps: []TestStep{
-			{
-				feature: "api-broadcast",
-				method:  "send",
-				expectedMessage: map[string]interface{}{
-					"broadcastSubject": "call",
-					"labels": map[string]interface{}{
-						"fn":           "test_type_match",
-						"uuid":         uuids[0],
-						"service":      "foo",
-						"content-type": "application/json",
-					},
-					"data": map[string]interface{}{},
-				},
-				returnMessage: nil,
-			},
-		},
-
-		// returned message from service
-		returns: []ReturnMessage{
-			{
-				delay: time.Millisecond,
-				msg: &dipper.Message{
-					Labels: map[string]string{
-						"type": "ack",
-						"uuid": uuids[0],
-						"from": "bar",
-					},
-				},
-			},
-			{
-				delay: time.Millisecond,
-				msg: &dipper.Message{
-					Labels: map[string]string{
-						"type": "result",
-						"uuid": uuids[0],
-						"from": "bar",
-					},
-					Payload: map[string]interface{}{
-						"result": "all",
-					},
-				},
-			},
-		},
-
-		// expected result
-		expectedCode:    200,
-		expectedContent: map[string]interface{}{"bar": map[string]interface{}{"result": "all"}},
-	}
-
-	requestTest(t, c)
+	requestTest(t, "TypeMatchAPI")
 }
 
 func TestTypeMatchAPINoMatch(t *testing.T) {
-	uuids := []string{"34ik-ijo3i4jt84932-aiau3kegkjrl"}
-	c := &RequestTestCase{
-		// cfg and api definition
-		config: map[string]interface{}{
-			"acls": map[string]interface{}{
-				"test_type_match": []interface{}{
-					map[string]interface{}{
-						"subjects": []interface{}{
-							map[string]interface{}{"role": "any"},
-						},
-						"type": "allow",
-					},
-				},
-			},
-		},
-		def: Def{
-			path:    "/test_type_match",
-			name:    "test_type_match",
-			method:  "GET",
-			reqType: TypeMatch,
-			service: "foo",
-		},
-		shouldAuthorize: true,
-
-		// input
-		uuids:       uuids,
-		subject:     map[string]interface{}{"role": "any"},
-		contentType: "application/json",
-		path:        "/test_type_match",
-		payload:     map[string]interface{}{},
-
-		steps: []TestStep{
-			{
-				feature: "api-broadcast",
-				method:  "send",
-				expectedMessage: map[string]interface{}{
-					"broadcastSubject": "call",
-					"labels": map[string]interface{}{
-						"fn":           "test_type_match",
-						"uuid":         uuids[0],
-						"service":      "foo",
-						"content-type": "application/json",
-					},
-					"data": map[string]interface{}{},
-				},
-				returnMessage: nil,
-			},
-		},
-
-		// returned message from service
-		returns: []ReturnMessage{},
-
-		// expected result
-		expectedCode:    404,
-		expectedContent: map[string]interface{}{"error": "object not found"},
-	}
-
-	requestTest(t, c)
+	requestTest(t, "TypeMatchAPINoMatch")
 }
 
 func TestTypeAllAPITimeout(t *testing.T) {
-	uuids := []string{"34ik-ijo3i4jt84932-aiau3kegkjrl"}
-	c := &RequestTestCase{
-		// cfg and api definition
-		config: map[string]interface{}{
-			"acls": map[string]interface{}{
-				"test_type_all": []interface{}{
-					map[string]interface{}{
-						"subjects": []interface{}{
-							map[string]interface{}{"role": "any"},
-						},
-						"type": "allow",
-					},
-				},
-			},
-		},
-		def: Def{
-			path:    "/test_type_all",
-			name:    "test_type_all",
-			method:  "GET",
-			reqType: TypeAll,
-			service: "foo",
-			timeout: time.Millisecond,
-		},
-		shouldAuthorize: true,
-
-		// input
-		uuids:       uuids,
-		subject:     map[string]interface{}{"role": "any"},
-		contentType: "application/json",
-		path:        "/test_type_all",
-		payload:     map[string]interface{}{},
-
-		steps: []TestStep{
-			{
-				feature: "api-broadcast",
-				method:  "send",
-				expectedMessage: map[string]interface{}{
-					"broadcastSubject": "call",
-					"labels": map[string]interface{}{
-						"fn":           "test_type_all",
-						"uuid":         uuids[0],
-						"service":      "foo",
-						"content-type": "application/json",
-					},
-					"data": map[string]interface{}{},
-				},
-				returnMessage: nil,
-			},
-		},
-
-		// returned message from service
-		returns: []ReturnMessage{
-			{
-				delay: time.Millisecond,
-				msg: &dipper.Message{
-					Labels: map[string]string{
-						"type": "ack",
-						"uuid": uuids[0],
-						"from": "bar",
-					},
-				},
-			},
-		},
-
-		// expected result
-		expectedCode:    500,
-		expectedContent: map[string]interface{}{"error": "timeout"},
-	}
-
-	requestTest(t, c)
+	requestTest(t, "TypeAllAPITimeout")
 }
 
 func TestTypeMatchAPILongRequest(t *testing.T) {
-	uuids := []string{"34ik-ijo3i4jt84932-aiau3kegkjrl"}
-	c := &RequestTestCase{
-		// cfg and api definition
-		config: map[string]interface{}{
-			"acls": map[string]interface{}{
-				"test_type_match": []interface{}{
-					map[string]interface{}{
-						"subjects": []interface{}{
-							map[string]interface{}{"role": "any"},
-						},
-						"type": "allow",
-					},
-				},
-			},
-			"writeTimeout": time.Millisecond * 6,
-		},
-		def: Def{
-			path:    "/test_type_match",
-			name:    "test_type_match",
-			method:  "GET",
-			reqType: TypeMatch,
-			service: "foo",
-			timeout: InfiniteDuration,
-		},
-		shouldAuthorize: true,
-
-		// input
-		uuids:       uuids,
-		subject:     map[string]interface{}{"role": "any"},
-		contentType: "application/json",
-		path:        "/test_type_match",
-		payload:     map[string]interface{}{},
-
-		steps: []TestStep{
-			{
-				feature: "api-broadcast",
-				method:  "send",
-				expectedMessage: map[string]interface{}{
-					"broadcastSubject": "call",
-					"labels": map[string]interface{}{
-						"fn":           "test_type_match",
-						"uuid":         uuids[0],
-						"service":      "foo",
-						"content-type": "application/json",
-					},
-					"data": map[string]interface{}{},
-				},
-				returnMessage: nil,
-			},
-		},
-
-		// returned message from service
-		returns: []ReturnMessage{
-			{
-				delay: time.Millisecond,
-				msg: &dipper.Message{
-					Labels: map[string]string{
-						"type": "ack",
-						"uuid": uuids[0],
-						"from": "bar",
-					},
-				},
-			},
-			{
-				delay: 20 * time.Millisecond,
-				msg: &dipper.Message{
-					Labels: map[string]string{
-						"type": "result",
-						"uuid": uuids[0],
-						"from": "bar",
-					},
-					Payload: map[string]interface{}{
-						"result": "all",
-					},
-				},
-			},
-		},
-
-		// expected result
-		expectedCode:    202,
-		expectedContent: map[string]interface{}{"results": map[string]interface{}{}, "uuid": uuids[0]},
-	}
-
-	l := requestTest(t, c)
+	l, c := requestTest(t, "TypeMatchAPILongRequest")
 
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
 	mockReqCtx := mock_api.NewMockRequestContext(ctrl)
-	mockReqCtx.EXPECT().GetPath().Times(1).Return(c.def.path)
-	req := l.GetRequest(c.def, mockReqCtx)
-	assert.Equal(t, uuids[0], req.uuid)
+	mockReqCtx.EXPECT().GetPath().Times(1).Return(c.Def.Path)
+	req := l.GetRequest(c.Def, mockReqCtx)
+	assert.Equal(t, c.UUIDs[0], req.uuid)
 
 	assert.NotPanics(t, func() { l.ClearRequest(req) })
 }
 
 func TestUnauthorizedAPI(t *testing.T) {
-	uuids := []string{"34ik-ijo3i4jt84932-aiau3kegkjrl"}
-	c := &RequestTestCase{
-		// cfg and api definition
-		config: map[string]interface{}{
-			"acls": map[string]interface{}{
-				"test_type_match": []interface{}{
-					map[string]interface{}{
-						"subjects": "all",
-						"type":     "deny",
-					},
-					map[string]interface{}{
-						"subjects": []interface{}{
-							map[string]interface{}{"role": "privileged"},
-						},
-						"type": "allow",
-					},
-				},
-			},
-		},
-		def: Def{
-			path:    "/test_type_match",
-			name:    "test_type_match",
-			method:  "GET",
-			reqType: TypeFirst,
-			service: "foo",
-		},
-		shouldAuthorize: false,
-
-		// input
-		uuids:       uuids,
-		subject:     map[string]interface{}{"role": "someone"},
-		contentType: "application/json",
-		path:        "/test_type_match",
-		payload:     map[string]interface{}{},
-
-		steps: []TestStep{},
-
-		// returned message from service
-		returns: []ReturnMessage{},
-
-		// expected result
-		expectedCode:    403,
-		expectedContent: map[string]interface{}{"errors": "not allowed"},
-	}
-
-	requestTest(t, c)
+	requestTest(t, "UnauthorizedAPI")
 }
diff --git a/internal/api/response_test.go b/internal/api/response_test.go
index 3aad46a..2106287 100644
--- a/internal/api/response_test.go
+++ b/internal/api/response_test.go
@@ -73,8 +73,8 @@ func responseTest(t *testing.T, c *ResponseTestCase) {
 		go c.mockAPI(resp, c)
 
 		for i, st := range c.returnMessages {
-			if m := waitForMsg(st.delay); m != nil {
-				assert.Equal(t, st.msg, m)
+			if m := waitForMsg(st.Delay); m != nil {
+				assert.Equal(t, st.Msg, m)
 			} else {
 				assert.Fail(t, fmt.Sprintf("timeout at step %d", i))
 			}
@@ -85,7 +85,7 @@ func responseTest(t *testing.T, c *ResponseTestCase) {
 func TestTypeAllAPIResponse(t *testing.T) {
 	c := &ResponseTestCase{
 		defsByName: map[string]Def{
-			"test": {name: "test", reqType: TypeAll},
+			"test": {Name: "test", ReqType: TypeAll},
 		},
 		msg: &dipper.Message{
 			Labels: map[string]string{
@@ -106,8 +106,8 @@ func TestTypeAllAPIResponse(t *testing.T) {
 		},
 		returnMessages: []ReturnMessage{
 			{
-				delay: 30 * time.Millisecond,
-				msg: &dipper.Message{
+				Delay: 30 * time.Millisecond,
+				Msg: &dipper.Message{
 					Channel: "eventbus",
 					Subject: "api",
 					Labels: map[string]string{
@@ -118,8 +118,8 @@ func TestTypeAllAPIResponse(t *testing.T) {
 				},
 			},
 			{
-				delay: 30 * time.Millisecond,
-				msg: &dipper.Message{
+				Delay: 30 * time.Millisecond,
+				Msg: &dipper.Message{
 					Channel: "eventbus",
 					Subject: "api",
 					Labels: map[string]string{
@@ -141,7 +141,7 @@ func TestTypeAllAPIResponse(t *testing.T) {
 func TestTypeFirstAPIResponse(t *testing.T) {
 	c := &ResponseTestCase{
 		defsByName: map[string]Def{
-			"test": {name: "test", reqType: TypeFirst},
+			"test": {Name: "test", ReqType: TypeFirst},
 		},
 		shouldLock: true,
 		msg: &dipper.Message{
@@ -163,8 +163,8 @@ func TestTypeFirstAPIResponse(t *testing.T) {
 		},
 		returnMessages: []ReturnMessage{
 			{
-				delay: 30 * time.Millisecond,
-				msg: &dipper.Message{
+				Delay: 30 * time.Millisecond,
+				Msg: &dipper.Message{
 					Channel: "eventbus",
 					Subject: "api",
 					Labels: map[string]string{
@@ -186,7 +186,7 @@ func TestTypeFirstAPIResponse(t *testing.T) {
 func TestTypeMatchAPIResponse(t *testing.T) {
 	c := &ResponseTestCase{
 		defsByName: map[string]Def{
-			"test": {name: "test", reqType: TypeMatch},
+			"test": {Name: "test", ReqType: TypeMatch},
 		},
 		msg: &dipper.Message{
 			Labels: map[string]string{
@@ -209,8 +209,8 @@ func TestTypeMatchAPIResponse(t *testing.T) {
 		},
 		returnMessages: []ReturnMessage{
 			{
-				delay: 30 * time.Millisecond,
-				msg: &dipper.Message{
+				Delay: 30 * time.Millisecond,
+				Msg: &dipper.Message{
 					Channel: "eventbus",
 					Subject: "api",
 					Labels: map[string]string{
@@ -221,8 +221,8 @@ func TestTypeMatchAPIResponse(t *testing.T) {
 				},
 			},
 			{
-				delay: 30 * time.Millisecond,
-				msg: &dipper.Message{
+				Delay: 30 * time.Millisecond,
+				Msg: &dipper.Message{
 					Channel: "eventbus",
 					Subject: "api",
 					Labels: map[string]string{
@@ -244,7 +244,7 @@ func TestTypeMatchAPIResponse(t *testing.T) {
 func TestTypeMatchAPIResponseReturnError(t *testing.T) {
 	c := &ResponseTestCase{
 		defsByName: map[string]Def{
-			"test": {name: "test", reqType: TypeMatch},
+			"test": {Name: "test", ReqType: TypeMatch},
 		},
 		msg: &dipper.Message{
 			Labels: map[string]string{
@@ -265,8 +265,8 @@ func TestTypeMatchAPIResponseReturnError(t *testing.T) {
 		},
 		returnMessages: []ReturnMessage{
 			{
-				delay: 30 * time.Millisecond,
-				msg: &dipper.Message{
+				Delay: 30 * time.Millisecond,
+				Msg: &dipper.Message{
 					Channel: "eventbus",
 					Subject: "api",
 					Labels: map[string]string{
@@ -277,8 +277,8 @@ func TestTypeMatchAPIResponseReturnError(t *testing.T) {
 				},
 			},
 			{
-				delay: 30 * time.Millisecond,
-				msg: &dipper.Message{
+				Delay: 30 * time.Millisecond,
+				Msg: &dipper.Message{
 					Channel: "eventbus",
 					Subject: "api",
 					Labels: map[string]string{
@@ -298,7 +298,7 @@ func TestTypeMatchAPIResponseReturnError(t *testing.T) {
 func TestAPIResponseUnknownAPI(t *testing.T) {
 	c := &ResponseTestCase{
 		defsByName: map[string]Def{
-			"test": {name: "test", reqType: TypeMatch},
+			"test": {Name: "test", ReqType: TypeMatch},
 		},
 		msg: &dipper.Message{
 			Labels: map[string]string{
@@ -319,7 +319,7 @@ func TestAPIResponseUnknownAPI(t *testing.T) {
 func TestAPIResponseLockFailure(t *testing.T) {
 	c := &ResponseTestCase{
 		defsByName: map[string]Def{
-			"test": {name: "test", reqType: TypeFirst},
+			"test": {Name: "test", ReqType: TypeFirst},
 		},
 		shouldLock:   true,
 		lockingError: fmt.Errorf("busy"),
diff --git a/internal/api/test_fixtures/TypeAllAPI.yaml b/internal/api/test_fixtures/TypeAllAPI.yaml
new file mode 100644
index 0000000..98e4e70
--- /dev/null
+++ b/internal/api/test_fixtures/TypeAllAPI.yaml
@@ -0,0 +1,49 @@
+---
+# mock api definition to be tested
+def:
+  path: /test_type_all
+  name: test_type_all
+  method: GET
+  # TypeAll
+  reqType: 1
+  service: foo
+  object: event
+
+# mock incoming call
+path: /test_type_all
+
+# exepected messages sent to services
+steps:
+  - feature: api-broadcast
+    method: send
+    expectedMessage:
+      broadcastSubject: call
+      labels:
+        fn: test_type_all
+        uuid: 34ik-ijo3i4jt84932-aiau3kegkjrl
+        service: foo
+        content-type: application/json
+      data: {}
+
+# mock return messages received from services
+returns:
+  - delay: 1
+    msg:
+      labels:
+        type: ack
+        uuid: 34ik-ijo3i4jt84932-aiau3kegkjrl
+        from: bar
+  - delay: 1
+    msg:
+      labels:
+        type: result
+        uuid: 34ik-ijo3i4jt84932-aiau3kegkjrl
+        from: bar
+      payload:
+        result: all
+
+# expected end result
+expectedCode: 200
+expectedContent:
+  bar:
+    result: all
diff --git a/internal/api/test_fixtures/TypeAllAPITimeout.yaml b/internal/api/test_fixtures/TypeAllAPITimeout.yaml
new file mode 100644
index 0000000..d063320
--- /dev/null
+++ b/internal/api/test_fixtures/TypeAllAPITimeout.yaml
@@ -0,0 +1,42 @@
+---
+# mock api definition to be tested
+def:
+  path: /test_type_all
+  name: test_type_all
+  method: GET
+  # TypeAll
+  reqType: 1
+  service: foo
+  object: event
+  timeout: 1
+
+# mock incoming call
+path: /test_type_all
+
+# exepected messages sent to services
+steps:
+  - feature: api-broadcast
+    method: send
+    expectedMessage:
+      broadcastSubject: call
+      labels:
+        fn: test_type_all
+        uuid: 34ik-ijo3i4jt84932-aiau3kegkjrl
+        service: foo
+        content-type: application/json
+      data: {}
+
+# mock return messages received from services
+returns:
+  - delay: 1
+    msg:
+      labels:
+        type: ack
+        uuid: 34ik-ijo3i4jt84932-aiau3kegkjrl
+        from: bar
+  # only ack, result timed out
+
+# expected end result
+expectedCode: 500
+expectedContent:
+  error: timeout
diff --git a/internal/api/test_fixtures/TypeFirstAPI.yaml b/internal/api/test_fixtures/TypeFirstAPI.yaml
new file mode 100644
index 0000000..ad2a733
--- /dev/null
+++ b/internal/api/test_fixtures/TypeFirstAPI.yaml
@@ -0,0 +1,44 @@
+---
+# mock api definition to be tested
+def:
+  path: /test_type_first
+  name: test_type_first
+  method: GET
+  # TypeFirst
+  reqType: 0
+  service: foo
+  object: event
+
+# mock incoming call
+path: /test_type_first
+
+# expected messages sent to services
+steps:
+  - feature: api-broadcast
+    method: send
+    expectedMessage:
+      broadcastSubject: call
+      labels:
+        fn: test_type_first
+        uuid: 34ik-ijo3i4jt84932-aiau3kegkjrl
+        service: foo
+        content-type: application/json
+      data: {}
+
+# mock return messages received from services
+returns:
+  # no ack
+  - delay: 2
+    msg:
+      labels:
+        type: result
+        uuid: 34ik-ijo3i4jt84932-aiau3kegkjrl
+        from: bar
+      payload:
+        result: all
+
+# expected end result
+expectedCode: 200
+expectedContent:
+  bar:
+    result: all
diff --git a/internal/api/test_fixtures/TypeMatchAPI.yaml b/internal/api/test_fixtures/TypeMatchAPI.yaml
new file mode 100644
index 0000000..f9f4ce6
--- /dev/null
+++ b/internal/api/test_fixtures/TypeMatchAPI.yaml
@@ -0,0 +1,50 @@
+---
+# mock api definition to be tested
+def:
+  path: /test_type_match
+  name: test_type_match
+  method: GET
+  # TypeMatch
+  reqType: 2
+  service: foo
+  object: event
+
+
+# mock incoming call
+path: /test_type_match
+
+# exepected messages sent to services
+steps:
+  - feature: api-broadcast
+    method: send
+    expectedMessage:
+      broadcastSubject: call
+      labels:
+        fn: test_type_match
+        uuid: 34ik-ijo3i4jt84932-aiau3kegkjrl
+        service: foo
+        content-type: application/json
+      data: {}
+
+# mock return messages received from services
+returns:
+  - delay: 1
+    msg:
+      labels:
+        type: ack
+        uuid: 34ik-ijo3i4jt84932-aiau3kegkjrl
+        from: bar
+  - delay: 1
+    msg:
+      labels:
+        type: result
+        uuid: 34ik-ijo3i4jt84932-aiau3kegkjrl
+        from: bar
+      payload:
+        result: matched
+
+# expected end result
+expectedCode: 200
+expectedContent:
+  bar:
+    result: matched
diff --git a/internal/api/test_fixtures/TypeMatchAPILongRequest.yaml b/internal/api/test_fixtures/TypeMatchAPILongRequest.yaml
new file mode 100644
index 0000000..ecfff4f
--- /dev/null
+++ b/internal/api/test_fixtures/TypeMatchAPILongRequest.yaml
@@ -0,0 +1,54 @@
+---
+# mock api definition to be tested
+def:
+  path: /test_type_match
+  name: test_type_match
+  method: GET
+  # TypeMatch
+  reqType: 2
+  service: foo
+  object: event
+  timeout: 0
+
+# test config
+config:
+  writeTimeout: 6
+
+# mock incoming call
+path: /test_type_match
+
+# exepected messages sent to services
+steps:
+  - feature: api-broadcast
+    method: send
+    expectedMessage:
+      broadcastSubject: call
+      labels:
+        fn: test_type_match
+        uuid: 34ik-ijo3i4jt84932-aiau3kegkjrl
+        service: foo
+        content-type: application/json
+      data: {}
+
+# mock return messages received from services
+returns:
+  - delay: 1
+    msg:
+      labels:
+        type: ack
+        uuid: 34ik-ijo3i4jt84932-aiau3kegkjrl
+        from: bar
+  - delay: 11000
+    msg:
+      labels:
+        type: result
+        uuid: 34ik-ijo3i4jt84932-aiau3kegkjrl
+        from: bar
+      payload:
+        result: matched
+
+# expected end result
+expectedCode: 202
+expectedContent:
+  results: {}
+  uuid: 34ik-ijo3i4jt84932-aiau3kegkjrl
diff --git a/internal/api/test_fixtures/TypeMatchAPINoMatch.yaml b/internal/api/test_fixtures/TypeMatchAPINoMatch.yaml
new file mode 100644
index 0000000..f7bfe59
--- /dev/null
+++ b/internal/api/test_fixtures/TypeMatchAPINoMatch.yaml
@@ -0,0 +1,34 @@
+---
+# mock api definition to be tested
+def:
+  path: /test_type_match
+  name: test_type_match
+  method: GET
+  # TypeMatch
+  reqType: 2
+  service: foo
+  object: event
+
+# mock incoming call
+path: /test_type_match
+
+# exepected messages sent to services
+steps:
+  - feature: api-broadcast
+    method: send
+    expectedMessage:
+      broadcastSubject: call
+      labels:
+        fn: test_type_match
+        uuid: 34ik-ijo3i4jt84932-aiau3kegkjrl
+        service: foo
+        content-type: application/json
+      data: {}
+
+# mock return messages received from services
+returns: []
+
+# expected end result
+expectedCode: 404
+expectedContent:
+  error: object not found
diff --git a/internal/api/test_fixtures/UnauthorizedAPI.yaml b/internal/api/test_fixtures/UnauthorizedAPI.yaml
new file mode 100644
index 0000000..5356cff
--- /dev/null
+++ b/internal/api/test_fixtures/UnauthorizedAPI.yaml
@@ -0,0 +1,22 @@
+---
+# mock api definition to be tested
+def:
+  path: /test_type_match
+  name: test_type_match
+  method: GET
+  # TypeMatch
+  reqType: 2
+  service: foo
+  object: event
+
+# mock incoming call
+subject: someone
+path: /test_type_match
+
+# should pass the authorization or not
+shouldAuthorize: false
+
+# expected end result
+expectedCode: 403
+expectedContent:
+  errors: not allowed
diff --git a/internal/api/test_fixtures/common.yaml b/internal/api/test_fixtures/common.yaml
new file mode 100644
index 0000000..055e481
--- /dev/null
+++ b/internal/api/test_fixtures/common.yaml
@@ -0,0 +1,35 @@
+---
+# test config
+config:
+  auth:
+    casbin:
+      models:
+        - |-
+          [request_definition]
+          r = sub, obj, act
+
+          [policy_definition]
+          p = sub, obj, act
+
+          [policy_effect]
+          e = some(where (p.eft == allow))
+
+          [matchers]
+          m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
+
+      policies:
+        - |-
+          p, test, event, GET
+
+# mock uuids
+uuids:
+  - 34ik-ijo3i4jt84932-aiau3kegkjrl
+
+# mock incoming call
+subject: test
+content-type: application/json
+payload: {}
+
+# should pass the authorization or not
+shouldAuthorize: true
+