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