You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@casbin.apache.org by hs...@apache.org on 2026/04/25 06:36:03 UTC
(casbin-caddy-authz) 01/02: feat: add CredentialValidator
This is an automated email from the ASF dual-hosted git repository.
hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-caddy-authz.git
commit c8ddc61de76aadedb44ae4fffe206fa7836c9f82
Author: Yang Luo <hs...@gmail.com>
AuthorDate: Sat Apr 25 14:28:31 2026 +0800
feat: add CredentialValidator
---
authz.go | 40 +++++++++++++++++++--------------
authz_test.go | 72 ++++++++++++++++++++++++++++++++++++++---------------------
2 files changed, 69 insertions(+), 43 deletions(-)
diff --git a/authz.go b/authz.go
index c9ee07a..58f8320 100644
--- a/authz.go
+++ b/authz.go
@@ -12,6 +12,11 @@ import (
"github.com/casbin/casbin/v2"
)
+// CredentialValidator validates a username and password from HTTP Basic Auth.
+// Assign a custom implementation before using the plugin.
+// If nil, any non-empty username is accepted without password verification.
+var CredentialValidator func(username, password string) bool
+
func init() {
caddy.RegisterModule(Authorizer{})
httpcaddyfile.RegisterHandlerDirective("authz", parseCaddyfile)
@@ -53,13 +58,25 @@ func (a *Authorizer) Validate() error {
// ServeHTTP implements caddyhttp.MiddlewareHandler.
func (a Authorizer) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
- allowed, err := a.CheckPermission(r)
+ username, password, ok := r.BasicAuth()
+ if !ok {
+ w.Header().Set("WWW-Authenticate", `Basic realm="restricted"`)
+ w.WriteHeader(http.StatusUnauthorized)
+ return nil
+ }
+
+ if CredentialValidator != nil && !CredentialValidator(username, password) {
+ w.Header().Set("WWW-Authenticate", `Basic realm="restricted"`)
+ w.WriteHeader(http.StatusUnauthorized)
+ return nil
+ }
+
+ allowed, err := a.CheckPermission(username, r)
if err != nil {
return err
}
-
if !allowed {
- w.WriteHeader(403)
+ w.WriteHeader(http.StatusForbidden)
return nil
}
@@ -88,20 +105,9 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
return m, err
}
-// GetUserName gets the user name from the request.
-// Currently, only HTTP basic authentication is supported
-func (a *Authorizer) GetUserName(r *http.Request) string {
- username := r.Header.Get("Authorization")
- return username
-}
-
-// CheckPermission checks the user/method/path combination from the request.
-// Returns true (permission granted) or false (permission forbidden)
-func (a *Authorizer) CheckPermission(r *http.Request) (bool, error) {
- user := a.GetUserName(r)
- method := r.Method
- path := r.URL.Path
- return a.Enforcer.Enforce(user, path, method)
+// CheckPermission checks if the given user is allowed to access the resource.
+func (a *Authorizer) CheckPermission(username string, r *http.Request) (bool, error) {
+ return a.Enforcer.Enforce(username, r.URL.Path, r.Method)
}
// Interface guards
diff --git a/authz_test.go b/authz_test.go
index c520377..b3c264c 100644
--- a/authz_test.go
+++ b/authz_test.go
@@ -8,20 +8,32 @@ import (
"github.com/caddyserver/caddy/v2/caddytest"
)
+// testPasswords simulates a user database for tests.
+var testPasswords = map[string]string{
+ "alice": "alice_pass",
+ "bob": "bob_pass",
+ "cathy": "cathy_pass",
+}
+
var tester *caddytest.Tester
-func testRequest(t *testing.T, user string, path string, method string, code int) {
+func testRequest(t *testing.T, user string, password string, path string, method string, code int) {
+ t.Helper()
req, err := http.NewRequest(method, fmt.Sprintf("http://localhost:9080%s", path), nil)
if err != nil {
t.Fatalf("unable to create request %s", err)
}
- req.Header.Set("Authorization", user)
+ req.SetBasicAuth(user, password)
tester.AssertResponse(req, code, "")
}
func initTester(t *testing.T) {
+ CredentialValidator = func(username, password string) bool {
+ return testPasswords[username] == password
+ }
+
tester = caddytest.NewTester(t)
- tester.InitServer(`
+ tester.InitServer(`
{
http_port 9080
https_port 9443
@@ -37,38 +49,46 @@ func initTester(t *testing.T) {
func TestBasic(t *testing.T) {
initTester(t)
- testRequest(t, "alice", "/dataset1/resource1", "GET", 200)
- testRequest(t, "alice", "/dataset1/resource1", "POST", 200)
- testRequest(t, "alice", "/dataset1/resource2", "GET", 200)
- testRequest(t, "alice", "/dataset1/resource2", "POST", 403)
+ testRequest(t, "alice", "alice_pass", "/dataset1/resource1", "GET", 200)
+ testRequest(t, "alice", "alice_pass", "/dataset1/resource1", "POST", 200)
+ testRequest(t, "alice", "alice_pass", "/dataset1/resource2", "GET", 200)
+ testRequest(t, "alice", "alice_pass", "/dataset1/resource2", "POST", 403)
+}
+
+func TestWrongPassword(t *testing.T) {
+ initTester(t)
+
+ testRequest(t, "alice", "wrong_pass", "/dataset1/resource1", "GET", 401)
+ testRequest(t, "bob", "wrong_pass", "/dataset2/resource1", "GET", 401)
+ testRequest(t, "cathy", "wrong_pass", "/dataset1/item", "DELETE", 401)
}
func TestPathWildcard(t *testing.T) {
initTester(t)
- testRequest(t, "bob", "/dataset2/resource1", "GET", 200)
- testRequest(t, "bob", "/dataset2/resource1", "POST", 200)
- testRequest(t, "bob", "/dataset2/resource1", "DELETE", 200)
- testRequest(t, "bob", "/dataset2/resource2", "GET", 200)
- testRequest(t, "bob", "/dataset2/resource2", "POST", 403)
- testRequest(t, "bob", "/dataset2/resource2", "DELETE", 403)
-
- testRequest(t, "bob", "/dataset2/folder1/item1", "GET", 403)
- testRequest(t, "bob", "/dataset2/folder1/item1", "POST", 200)
- testRequest(t, "bob", "/dataset2/folder1/item1", "DELETE", 403)
- testRequest(t, "bob", "/dataset2/folder1/item2", "GET", 403)
- testRequest(t, "bob", "/dataset2/folder1/item2", "POST", 200)
- testRequest(t, "bob", "/dataset2/folder1/item2", "DELETE", 403)
+ testRequest(t, "bob", "bob_pass", "/dataset2/resource1", "GET", 200)
+ testRequest(t, "bob", "bob_pass", "/dataset2/resource1", "POST", 200)
+ testRequest(t, "bob", "bob_pass", "/dataset2/resource1", "DELETE", 200)
+ testRequest(t, "bob", "bob_pass", "/dataset2/resource2", "GET", 200)
+ testRequest(t, "bob", "bob_pass", "/dataset2/resource2", "POST", 403)
+ testRequest(t, "bob", "bob_pass", "/dataset2/resource2", "DELETE", 403)
+
+ testRequest(t, "bob", "bob_pass", "/dataset2/folder1/item1", "GET", 403)
+ testRequest(t, "bob", "bob_pass", "/dataset2/folder1/item1", "POST", 200)
+ testRequest(t, "bob", "bob_pass", "/dataset2/folder1/item1", "DELETE", 403)
+ testRequest(t, "bob", "bob_pass", "/dataset2/folder1/item2", "GET", 403)
+ testRequest(t, "bob", "bob_pass", "/dataset2/folder1/item2", "POST", 200)
+ testRequest(t, "bob", "bob_pass", "/dataset2/folder1/item2", "DELETE", 403)
}
func TestRBAC(t *testing.T) {
initTester(t)
// cathy can access all /dataset1/* resources via all methods because it has the dataset1_admin role.
- testRequest(t, "cathy", "/dataset1/item", "GET", 200)
- testRequest(t, "cathy", "/dataset1/item", "POST", 200)
- testRequest(t, "cathy", "/dataset1/item", "DELETE", 200)
- testRequest(t, "cathy", "/dataset2/item", "GET", 403)
- testRequest(t, "cathy", "/dataset2/item", "POST", 403)
- testRequest(t, "cathy", "/dataset2/item", "DELETE", 403)
+ testRequest(t, "cathy", "cathy_pass", "/dataset1/item", "GET", 200)
+ testRequest(t, "cathy", "cathy_pass", "/dataset1/item", "POST", 200)
+ testRequest(t, "cathy", "cathy_pass", "/dataset1/item", "DELETE", 200)
+ testRequest(t, "cathy", "cathy_pass", "/dataset2/item", "GET", 403)
+ testRequest(t, "cathy", "cathy_pass", "/dataset2/item", "POST", 403)
+ testRequest(t, "cathy", "cathy_pass", "/dataset2/item", "DELETE", 403)
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@casbin.apache.org
For additional commands, e-mail: commits-help@casbin.apache.org