fix: whomai with bearer
This commit is contained in:
@@ -238,7 +238,7 @@ func runServer() {
|
||||
|
||||
meta.Register(api, mods)
|
||||
meta.RegisterHealth(api, sessions)
|
||||
meta.RegisterWhoami(api, sessions, roles, mods)
|
||||
meta.RegisterWhoami(api, sessions, tokenAuth, roles, mods)
|
||||
meta.RegisterUpdate(api, configPath)
|
||||
|
||||
auth.RegisterLogin(api, sessions, auditStore, cfg.SecureCookie())
|
||||
|
||||
+26
-7
@@ -15,6 +15,7 @@ import (
|
||||
// itself.
|
||||
type WhoamiInput struct {
|
||||
SessionID string `cookie:"nadir_session_id"`
|
||||
Auth string `header:"Authorization"`
|
||||
}
|
||||
|
||||
// WhoamiBody reports who the caller is and, per module, which permissions they
|
||||
@@ -30,7 +31,7 @@ type WhoamiOutput struct{ Body WhoamiBody }
|
||||
// RegisterWhoami adds the current-user endpoint. It resolves the caller's
|
||||
// concrete grants by asking the RBAC store about each module's permissions,
|
||||
// so "*" wildcards in roles are expanded for free.
|
||||
func RegisterWhoami(api huma.API, sessions *auth.SessionStore, roles *rbac.RBAC, mods []module.Module) {
|
||||
func RegisterWhoami(api huma.API, sessions *auth.SessionStore, tokens *auth.TokenAuth, roles *rbac.RBAC, mods []module.Module) {
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "whoami",
|
||||
Method: "GET",
|
||||
@@ -40,18 +41,35 @@ func RegisterWhoami(api huma.API, sessions *auth.SessionStore, roles *rbac.RBAC,
|
||||
"permissions the caller holds (wildcards resolved). Pair with " +
|
||||
"/api/_modules to render the full permission matrix.",
|
||||
Tags: []string{"Meta"},
|
||||
Errors: []int{401},
|
||||
Errors: []int{401, 429},
|
||||
}, func(ctx context.Context, in *WhoamiInput) (*WhoamiOutput, error) {
|
||||
sess, ok := sessions.GetByToken(in.SessionID)
|
||||
if !ok {
|
||||
return nil, huma.Error401Unauthorized("unauthorized")
|
||||
var username string
|
||||
|
||||
if raw, isBearer := auth.BearerToken(in.Auth); isBearer {
|
||||
if tokens == nil {
|
||||
return nil, huma.Error401Unauthorized("unauthorized")
|
||||
}
|
||||
name, ok, throttled := tokens.Verify(auth.ClientIP(ctx), raw)
|
||||
if throttled {
|
||||
return nil, huma.Error429TooManyRequests("too many failed token attempts; wait a minute")
|
||||
}
|
||||
if !ok {
|
||||
return nil, huma.Error401Unauthorized("unauthorized")
|
||||
}
|
||||
username = name
|
||||
} else {
|
||||
sess, ok := sessions.GetByToken(in.SessionID)
|
||||
if !ok {
|
||||
return nil, huma.Error401Unauthorized("unauthorized")
|
||||
}
|
||||
username = sess.Username
|
||||
}
|
||||
|
||||
held := make(map[string][]string)
|
||||
for _, m := range mods {
|
||||
var perms []string
|
||||
for _, p := range m.Permissions() {
|
||||
if roles.Can(sess.Username, m.ID(), p) {
|
||||
if roles.Can(username, m.ID(), p) {
|
||||
perms = append(perms, string(p))
|
||||
}
|
||||
}
|
||||
@@ -61,7 +79,8 @@ func RegisterWhoami(api huma.API, sessions *auth.SessionStore, roles *rbac.RBAC,
|
||||
}
|
||||
|
||||
out := &WhoamiOutput{}
|
||||
out.Body = WhoamiBody{Username: sess.Username, Permissions: held}
|
||||
out.Body = WhoamiBody{Username: username, Permissions: held}
|
||||
return out, nil
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"nadir/internal/auth"
|
||||
"nadir/internal/module"
|
||||
"nadir/internal/rbac"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/danielgtaylor/huma/v2/adapters/humago"
|
||||
"github.com/danielgtaylor/huma/v2/humatest"
|
||||
)
|
||||
|
||||
type dummyModule struct {
|
||||
id string
|
||||
perms []rbac.Permission
|
||||
}
|
||||
|
||||
func (m *dummyModule) ID() string { return m.id }
|
||||
func (m *dummyModule) Name() string { return m.id }
|
||||
func (m *dummyModule) Permissions() []rbac.Permission { return m.perms }
|
||||
func (m *dummyModule) Register(api huma.API) {}
|
||||
|
||||
func TestWhoami(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
sessions, err := auth.NewSessionStore(filepath.Join(tempDir, "sessions.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tokenStore, err := auth.NewTokenStore(filepath.Join(tempDir, "tokens.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tokenStore.Close()
|
||||
tokenAuth := auth.NewTokenAuth(tokenStore)
|
||||
|
||||
roles := rbac.New()
|
||||
roles.DefineRole(rbac.Role{
|
||||
Name: "admin-role",
|
||||
ModuleGrants: map[string][]rbac.Permission{
|
||||
"system": {rbac.Read},
|
||||
},
|
||||
})
|
||||
roles.AssignRole("admin", "admin-role")
|
||||
|
||||
mods := []module.Module{
|
||||
&dummyModule{
|
||||
id: "system",
|
||||
perms: []rbac.Permission{rbac.Read, rbac.Write},
|
||||
},
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
api := humatest.Wrap(t, humago.New(mux, huma.DefaultConfig("Test", "1.0.0")))
|
||||
RegisterWhoami(api, sessions, tokenAuth, roles, mods)
|
||||
|
||||
// 1. Unauthorized request (no token, no session)
|
||||
resp := api.Get("/api/whoami")
|
||||
if resp.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d", resp.Code)
|
||||
}
|
||||
|
||||
// 2. Cookie session request
|
||||
sessToken, err := sessions.Create("admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp = api.Get("/api/whoami", "Cookie: nadir_session_id="+sessToken)
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d", resp.Code)
|
||||
}
|
||||
var body WhoamiBody
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body.Username != "admin" {
|
||||
t.Errorf("expected username admin, got %q", body.Username)
|
||||
}
|
||||
if !slices.Contains(body.Permissions["system"], "read") {
|
||||
t.Errorf("expected system read permission, got %v", body.Permissions["system"])
|
||||
}
|
||||
|
||||
// 3. Token request
|
||||
bearerToken, err := tokenStore.Create("api-user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
roles.DefineRole(rbac.Role{
|
||||
Name: "api-role",
|
||||
ModuleGrants: map[string][]rbac.Permission{
|
||||
"system": {rbac.Write},
|
||||
},
|
||||
})
|
||||
roles.AssignRole("api-user", "api-role")
|
||||
|
||||
resp = api.Get("/api/whoami", "Authorization: Bearer "+bearerToken)
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d", resp.Code)
|
||||
}
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body.Username != "api-user" {
|
||||
t.Errorf("expected username api-user, got %q", body.Username)
|
||||
}
|
||||
if !slices.Contains(body.Permissions["system"], "write") {
|
||||
t.Errorf("expected system write permission, got %v", body.Permissions["system"])
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ func TestOpenAPISchemaNoCollisions(t *testing.T) {
|
||||
}
|
||||
meta.Register(api, mods)
|
||||
meta.RegisterHealth(api, sessions)
|
||||
meta.RegisterWhoami(api, sessions, roles, mods)
|
||||
meta.RegisterWhoami(api, sessions, nil, roles, mods)
|
||||
auth.RegisterLogin(api, sessions, auditStore, true)
|
||||
auth.RegisterLogout(api, sessions, true)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user