2026-02-20 16:36:38 +00:00
|
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
|
|
|
|
|
|
package api_test
|
|
|
|
|
|
|
|
|
|
import (
|
2026-02-20 16:38:13 +00:00
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
2026-02-20 16:43:55 +00:00
|
|
|
"strings"
|
2026-02-20 16:36:38 +00:00
|
|
|
"testing"
|
|
|
|
|
|
2026-02-20 16:38:13 +00:00
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
2026-02-20 16:36:38 +00:00
|
|
|
api "forge.lthn.ai/core/go-api"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ── AuthentikUser ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestAuthentikUser_Good(t *testing.T) {
|
|
|
|
|
u := api.AuthentikUser{
|
|
|
|
|
Username: "alice",
|
|
|
|
|
Email: "alice@example.com",
|
|
|
|
|
Name: "Alice Smith",
|
|
|
|
|
UID: "abc-123",
|
|
|
|
|
Groups: []string{"editors", "admins"},
|
|
|
|
|
Entitlements: []string{"premium"},
|
|
|
|
|
JWT: "tok.en.here",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if u.Username != "alice" {
|
|
|
|
|
t.Fatalf("expected Username=%q, got %q", "alice", u.Username)
|
|
|
|
|
}
|
|
|
|
|
if u.Email != "alice@example.com" {
|
|
|
|
|
t.Fatalf("expected Email=%q, got %q", "alice@example.com", u.Email)
|
|
|
|
|
}
|
|
|
|
|
if u.Name != "Alice Smith" {
|
|
|
|
|
t.Fatalf("expected Name=%q, got %q", "Alice Smith", u.Name)
|
|
|
|
|
}
|
|
|
|
|
if u.UID != "abc-123" {
|
|
|
|
|
t.Fatalf("expected UID=%q, got %q", "abc-123", u.UID)
|
|
|
|
|
}
|
|
|
|
|
if len(u.Groups) != 2 || u.Groups[0] != "editors" {
|
|
|
|
|
t.Fatalf("expected Groups=[editors admins], got %v", u.Groups)
|
|
|
|
|
}
|
|
|
|
|
if len(u.Entitlements) != 1 || u.Entitlements[0] != "premium" {
|
|
|
|
|
t.Fatalf("expected Entitlements=[premium], got %v", u.Entitlements)
|
|
|
|
|
}
|
|
|
|
|
if u.JWT != "tok.en.here" {
|
|
|
|
|
t.Fatalf("expected JWT=%q, got %q", "tok.en.here", u.JWT)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAuthentikUserHasGroup_Good(t *testing.T) {
|
|
|
|
|
u := api.AuthentikUser{
|
|
|
|
|
Groups: []string{"editors", "admins"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !u.HasGroup("admins") {
|
|
|
|
|
t.Fatal("expected HasGroup(admins) = true")
|
|
|
|
|
}
|
|
|
|
|
if !u.HasGroup("editors") {
|
|
|
|
|
t.Fatal("expected HasGroup(editors) = true")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAuthentikUserHasGroup_Bad_Empty(t *testing.T) {
|
|
|
|
|
u := api.AuthentikUser{}
|
|
|
|
|
|
|
|
|
|
if u.HasGroup("admins") {
|
|
|
|
|
t.Fatal("expected HasGroup(admins) = false for empty user")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAuthentikConfig_Good(t *testing.T) {
|
|
|
|
|
cfg := api.AuthentikConfig{
|
|
|
|
|
Issuer: "https://auth.example.com",
|
|
|
|
|
ClientID: "my-client",
|
|
|
|
|
TrustedProxy: true,
|
|
|
|
|
PublicPaths: []string{"/public", "/docs"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if cfg.Issuer != "https://auth.example.com" {
|
|
|
|
|
t.Fatalf("expected Issuer=%q, got %q", "https://auth.example.com", cfg.Issuer)
|
|
|
|
|
}
|
|
|
|
|
if cfg.ClientID != "my-client" {
|
|
|
|
|
t.Fatalf("expected ClientID=%q, got %q", "my-client", cfg.ClientID)
|
|
|
|
|
}
|
|
|
|
|
if !cfg.TrustedProxy {
|
|
|
|
|
t.Fatal("expected TrustedProxy=true")
|
|
|
|
|
}
|
|
|
|
|
if len(cfg.PublicPaths) != 2 {
|
|
|
|
|
t.Fatalf("expected 2 public paths, got %d", len(cfg.PublicPaths))
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-20 16:38:13 +00:00
|
|
|
|
|
|
|
|
// ── Forward auth middleware ────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestForwardAuthHeaders_Good(t *testing.T) {
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
|
|
|
|
|
cfg := api.AuthentikConfig{TrustedProxy: true}
|
|
|
|
|
e, _ := api.New(api.WithAuthentik(cfg))
|
|
|
|
|
|
|
|
|
|
var gotUser *api.AuthentikUser
|
|
|
|
|
e.Register(&authTestGroup{onRequest: func(c *gin.Context) {
|
|
|
|
|
gotUser = api.GetUser(c)
|
|
|
|
|
c.JSON(http.StatusOK, api.OK("ok"))
|
|
|
|
|
}})
|
|
|
|
|
|
|
|
|
|
h := e.Handler()
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/v1/check", nil)
|
|
|
|
|
req.Header.Set("X-authentik-username", "bob")
|
|
|
|
|
req.Header.Set("X-authentik-email", "bob@example.com")
|
|
|
|
|
req.Header.Set("X-authentik-name", "Bob Jones")
|
|
|
|
|
req.Header.Set("X-authentik-uid", "uid-456")
|
|
|
|
|
req.Header.Set("X-authentik-jwt", "jwt.tok.en")
|
|
|
|
|
req.Header.Set("X-authentik-groups", "staff|admins|ops")
|
|
|
|
|
req.Header.Set("X-authentik-entitlements", "read|write")
|
|
|
|
|
h.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
|
|
|
}
|
|
|
|
|
if gotUser == nil {
|
|
|
|
|
t.Fatal("expected GetUser to return a user, got nil")
|
|
|
|
|
}
|
|
|
|
|
if gotUser.Username != "bob" {
|
|
|
|
|
t.Fatalf("expected Username=%q, got %q", "bob", gotUser.Username)
|
|
|
|
|
}
|
|
|
|
|
if gotUser.Email != "bob@example.com" {
|
|
|
|
|
t.Fatalf("expected Email=%q, got %q", "bob@example.com", gotUser.Email)
|
|
|
|
|
}
|
|
|
|
|
if gotUser.Name != "Bob Jones" {
|
|
|
|
|
t.Fatalf("expected Name=%q, got %q", "Bob Jones", gotUser.Name)
|
|
|
|
|
}
|
|
|
|
|
if gotUser.UID != "uid-456" {
|
|
|
|
|
t.Fatalf("expected UID=%q, got %q", "uid-456", gotUser.UID)
|
|
|
|
|
}
|
|
|
|
|
if gotUser.JWT != "jwt.tok.en" {
|
|
|
|
|
t.Fatalf("expected JWT=%q, got %q", "jwt.tok.en", gotUser.JWT)
|
|
|
|
|
}
|
|
|
|
|
if len(gotUser.Groups) != 3 {
|
|
|
|
|
t.Fatalf("expected 3 groups, got %d: %v", len(gotUser.Groups), gotUser.Groups)
|
|
|
|
|
}
|
|
|
|
|
if gotUser.Groups[0] != "staff" || gotUser.Groups[1] != "admins" || gotUser.Groups[2] != "ops" {
|
|
|
|
|
t.Fatalf("expected groups [staff admins ops], got %v", gotUser.Groups)
|
|
|
|
|
}
|
|
|
|
|
if len(gotUser.Entitlements) != 2 {
|
|
|
|
|
t.Fatalf("expected 2 entitlements, got %d: %v", len(gotUser.Entitlements), gotUser.Entitlements)
|
|
|
|
|
}
|
|
|
|
|
if gotUser.Entitlements[0] != "read" || gotUser.Entitlements[1] != "write" {
|
|
|
|
|
t.Fatalf("expected entitlements [read write], got %v", gotUser.Entitlements)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestForwardAuthHeaders_Good_NoHeaders(t *testing.T) {
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
|
|
|
|
|
cfg := api.AuthentikConfig{TrustedProxy: true}
|
|
|
|
|
e, _ := api.New(api.WithAuthentik(cfg))
|
|
|
|
|
|
|
|
|
|
var gotUser *api.AuthentikUser
|
|
|
|
|
e.Register(&authTestGroup{onRequest: func(c *gin.Context) {
|
|
|
|
|
gotUser = api.GetUser(c)
|
|
|
|
|
c.JSON(http.StatusOK, api.OK("ok"))
|
|
|
|
|
}})
|
|
|
|
|
|
|
|
|
|
h := e.Handler()
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/v1/check", nil)
|
|
|
|
|
h.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
|
|
|
}
|
|
|
|
|
if gotUser != nil {
|
|
|
|
|
t.Fatalf("expected GetUser to return nil without headers, got %+v", gotUser)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestForwardAuthHeaders_Bad_NotTrusted(t *testing.T) {
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
|
|
|
|
|
cfg := api.AuthentikConfig{TrustedProxy: false}
|
|
|
|
|
e, _ := api.New(api.WithAuthentik(cfg))
|
|
|
|
|
|
|
|
|
|
var gotUser *api.AuthentikUser
|
|
|
|
|
e.Register(&authTestGroup{onRequest: func(c *gin.Context) {
|
|
|
|
|
gotUser = api.GetUser(c)
|
|
|
|
|
c.JSON(http.StatusOK, api.OK("ok"))
|
|
|
|
|
}})
|
|
|
|
|
|
|
|
|
|
h := e.Handler()
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/v1/check", nil)
|
|
|
|
|
req.Header.Set("X-authentik-username", "mallory")
|
|
|
|
|
req.Header.Set("X-authentik-email", "mallory@evil.com")
|
|
|
|
|
h.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
|
|
|
}
|
|
|
|
|
if gotUser != nil {
|
|
|
|
|
t.Fatalf("expected GetUser to return nil when TrustedProxy=false, got %+v", gotUser)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHealthBypassesAuthentik_Good(t *testing.T) {
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
|
|
|
|
|
cfg := api.AuthentikConfig{TrustedProxy: true}
|
|
|
|
|
e, _ := api.New(api.WithAuthentik(cfg))
|
|
|
|
|
|
|
|
|
|
h := e.Handler()
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/health", nil)
|
|
|
|
|
h.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("expected 200 for /health, got %d", w.Code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGetUser_Good_NilContext(t *testing.T) {
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
|
|
|
|
|
// Engine without WithAuthentik — GetUser should return nil.
|
|
|
|
|
e, _ := api.New()
|
|
|
|
|
|
|
|
|
|
var gotUser *api.AuthentikUser
|
|
|
|
|
e.Register(&authTestGroup{onRequest: func(c *gin.Context) {
|
|
|
|
|
gotUser = api.GetUser(c)
|
|
|
|
|
c.JSON(http.StatusOK, api.OK("ok"))
|
|
|
|
|
}})
|
|
|
|
|
|
|
|
|
|
h := e.Handler()
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/v1/check", nil)
|
|
|
|
|
h.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
|
|
|
}
|
|
|
|
|
if gotUser != nil {
|
|
|
|
|
t.Fatalf("expected GetUser to return nil without middleware, got %+v", gotUser)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 16:42:23 +00:00
|
|
|
// ── JWT validation ────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestJWTValidation_Bad_InvalidToken(t *testing.T) {
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
|
|
|
|
|
// Use a fake issuer that won't resolve — JWT validation should fail open.
|
|
|
|
|
cfg := api.AuthentikConfig{
|
|
|
|
|
Issuer: "https://fake-issuer.invalid",
|
|
|
|
|
ClientID: "test-client",
|
|
|
|
|
}
|
|
|
|
|
e, _ := api.New(api.WithAuthentik(cfg))
|
|
|
|
|
|
|
|
|
|
var gotUser *api.AuthentikUser
|
|
|
|
|
e.Register(&authTestGroup{onRequest: func(c *gin.Context) {
|
|
|
|
|
gotUser = api.GetUser(c)
|
|
|
|
|
c.JSON(http.StatusOK, api.OK("ok"))
|
|
|
|
|
}})
|
|
|
|
|
|
|
|
|
|
h := e.Handler()
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/v1/check", nil)
|
|
|
|
|
req.Header.Set("Authorization", "Bearer invalid-jwt-token")
|
|
|
|
|
h.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("expected 200 (permissive), got %d", w.Code)
|
|
|
|
|
}
|
|
|
|
|
if gotUser != nil {
|
|
|
|
|
t.Fatalf("expected GetUser to return nil for invalid JWT, got %+v", gotUser)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBearerAndAuthentikCoexist_Good(t *testing.T) {
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
|
|
|
|
|
// Engine with BOTH bearer auth AND authentik middleware.
|
|
|
|
|
cfg := api.AuthentikConfig{TrustedProxy: true}
|
|
|
|
|
e, _ := api.New(
|
|
|
|
|
api.WithBearerAuth("secret-token"),
|
|
|
|
|
api.WithAuthentik(cfg),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var gotUser *api.AuthentikUser
|
|
|
|
|
e.Register(&authTestGroup{onRequest: func(c *gin.Context) {
|
|
|
|
|
gotUser = api.GetUser(c)
|
|
|
|
|
c.JSON(http.StatusOK, api.OK("ok"))
|
|
|
|
|
}})
|
|
|
|
|
|
|
|
|
|
h := e.Handler()
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/v1/check", nil)
|
|
|
|
|
req.Header.Set("Authorization", "Bearer secret-token")
|
|
|
|
|
req.Header.Set("X-authentik-username", "carol")
|
|
|
|
|
req.Header.Set("X-authentik-email", "carol@example.com")
|
|
|
|
|
req.Header.Set("X-authentik-name", "Carol White")
|
|
|
|
|
req.Header.Set("X-authentik-uid", "uid-789")
|
|
|
|
|
req.Header.Set("X-authentik-groups", "developers|admins")
|
|
|
|
|
h.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
|
|
|
}
|
|
|
|
|
if gotUser == nil {
|
|
|
|
|
t.Fatal("expected GetUser to return a user, got nil")
|
|
|
|
|
}
|
|
|
|
|
if gotUser.Username != "carol" {
|
|
|
|
|
t.Fatalf("expected Username=%q, got %q", "carol", gotUser.Username)
|
|
|
|
|
}
|
|
|
|
|
if gotUser.Email != "carol@example.com" {
|
|
|
|
|
t.Fatalf("expected Email=%q, got %q", "carol@example.com", gotUser.Email)
|
|
|
|
|
}
|
|
|
|
|
if len(gotUser.Groups) != 2 || gotUser.Groups[0] != "developers" || gotUser.Groups[1] != "admins" {
|
|
|
|
|
t.Fatalf("expected groups [developers admins], got %v", gotUser.Groups)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 16:43:55 +00:00
|
|
|
// ── RequireAuth / RequireGroup ────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestRequireAuth_Good(t *testing.T) {
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
|
|
|
|
|
cfg := api.AuthentikConfig{TrustedProxy: true}
|
|
|
|
|
e, _ := api.New(api.WithAuthentik(cfg))
|
|
|
|
|
e.Register(&protectedGroup{})
|
|
|
|
|
|
|
|
|
|
h := e.Handler()
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/v1/protected/data", nil)
|
|
|
|
|
req.Header.Set("X-authentik-username", "alice")
|
|
|
|
|
req.Header.Set("X-authentik-email", "alice@example.com")
|
|
|
|
|
h.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRequireAuth_Bad_NoUser(t *testing.T) {
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
|
|
|
|
|
cfg := api.AuthentikConfig{TrustedProxy: true}
|
|
|
|
|
e, _ := api.New(api.WithAuthentik(cfg))
|
|
|
|
|
e.Register(&protectedGroup{})
|
|
|
|
|
|
|
|
|
|
h := e.Handler()
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/v1/protected/data", nil)
|
|
|
|
|
h.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
|
|
|
t.Fatalf("expected 401, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
body := w.Body.String()
|
|
|
|
|
if !strings.Contains(body, `"unauthorised"`) {
|
|
|
|
|
t.Fatalf("expected error code 'unauthorised' in body, got %s", body)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRequireAuth_Bad_NoAuthentikMiddleware(t *testing.T) {
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
|
|
|
|
|
// Engine without WithAuthentik — RequireAuth should still reject.
|
|
|
|
|
e, _ := api.New()
|
|
|
|
|
e.Register(&protectedGroup{})
|
|
|
|
|
|
|
|
|
|
h := e.Handler()
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/v1/protected/data", nil)
|
|
|
|
|
h.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
|
|
|
t.Fatalf("expected 401, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRequireGroup_Good(t *testing.T) {
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
|
|
|
|
|
cfg := api.AuthentikConfig{TrustedProxy: true}
|
|
|
|
|
e, _ := api.New(api.WithAuthentik(cfg))
|
|
|
|
|
e.Register(&groupRequireGroup{})
|
|
|
|
|
|
|
|
|
|
h := e.Handler()
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/v1/admin/panel", nil)
|
|
|
|
|
req.Header.Set("X-authentik-username", "admin-user")
|
|
|
|
|
req.Header.Set("X-authentik-groups", "admins|staff")
|
|
|
|
|
h.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRequireGroup_Bad_WrongGroup(t *testing.T) {
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
|
|
|
|
|
cfg := api.AuthentikConfig{TrustedProxy: true}
|
|
|
|
|
e, _ := api.New(api.WithAuthentik(cfg))
|
|
|
|
|
e.Register(&groupRequireGroup{})
|
|
|
|
|
|
|
|
|
|
h := e.Handler()
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/v1/admin/panel", nil)
|
|
|
|
|
req.Header.Set("X-authentik-username", "dev-user")
|
|
|
|
|
req.Header.Set("X-authentik-groups", "developers")
|
|
|
|
|
h.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
|
|
|
t.Fatalf("expected 403, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
body := w.Body.String()
|
|
|
|
|
if !strings.Contains(body, `"forbidden"`) {
|
|
|
|
|
t.Fatalf("expected error code 'forbidden' in body, got %s", body)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 16:38:13 +00:00
|
|
|
// ── Test helpers ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
// authTestGroup provides a /v1/check endpoint that calls a custom handler.
|
|
|
|
|
type authTestGroup struct {
|
|
|
|
|
onRequest func(c *gin.Context)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *authTestGroup) Name() string { return "auth-test" }
|
|
|
|
|
func (a *authTestGroup) BasePath() string { return "/v1" }
|
|
|
|
|
func (a *authTestGroup) RegisterRoutes(rg *gin.RouterGroup) {
|
|
|
|
|
rg.GET("/check", a.onRequest)
|
|
|
|
|
}
|
2026-02-20 16:43:55 +00:00
|
|
|
|
|
|
|
|
// protectedGroup provides a /v1/protected/data endpoint guarded by RequireAuth.
|
|
|
|
|
type protectedGroup struct{}
|
|
|
|
|
|
|
|
|
|
func (g *protectedGroup) Name() string { return "protected" }
|
|
|
|
|
func (g *protectedGroup) BasePath() string { return "/v1/protected" }
|
|
|
|
|
func (g *protectedGroup) RegisterRoutes(rg *gin.RouterGroup) {
|
|
|
|
|
rg.GET("/data", api.RequireAuth(), func(c *gin.Context) {
|
|
|
|
|
user := api.GetUser(c)
|
|
|
|
|
c.JSON(200, api.OK(user.Username))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// groupRequireGroup provides a /v1/admin/panel endpoint guarded by RequireGroup.
|
|
|
|
|
type groupRequireGroup struct{}
|
|
|
|
|
|
|
|
|
|
func (g *groupRequireGroup) Name() string { return "adminonly" }
|
|
|
|
|
func (g *groupRequireGroup) BasePath() string { return "/v1/admin" }
|
|
|
|
|
func (g *groupRequireGroup) RegisterRoutes(rg *gin.RouterGroup) {
|
|
|
|
|
rg.GET("/panel", api.RequireGroup("admins"), func(c *gin.Context) {
|
|
|
|
|
c.JSON(200, api.OK("admin panel"))
|
|
|
|
|
})
|
|
|
|
|
}
|