mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-15 05:29:28 +00:00
feat(auth): let a user join several groups and hold a personal grant
A user entry can now list several groups in "groups", next to the existing "group" field. A user can also hold a personal grant with its own "providers" and optional "models" lists. Users with one group and no personal grant behave exactly as before. A request is allowed only when one grant allows the provider, the model and, for passthrough, the path. Grants never merge, so one grant cannot supply the provider while another supplies the model or the path. The personal grant uses the path list of the first group. A personal "models" list without "providers" is a configuration error. Each member group applies its own limits. Caching stays on unless a member group turns it off, and the shortest group TTL wins. The /v1/models cache uses a stable, length-prefixed key, so reloads do not grow it. Tests cover the grant union, cross-grant denials, passthrough path and model coupling, reloads and the cache key. The yaegi-check harness asserts the same cases under the interpreter. Claude-Session: https://claude.ai/code/session_019uJHQKcNLMACkXssb3L7Q3
This commit is contained in:
@@ -1080,6 +1080,42 @@ func TestAdminUsage_GroupAccessLists(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdminUsage_MultiGroupUser_ListsAllGroupsInJSON proves the admin
|
||||
// usage JSON view (adminUsageEntryView.Groups) lists every member group
|
||||
// for a multi-group user, not just the first — GroupName (singular)
|
||||
// keeps the first group for back-compat.
|
||||
func TestAdminUsage_MultiGroupUser_ListsAllGroupsInJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := CreateConfig()
|
||||
cfg.Admin = &AdminConfig{Enabled: true}
|
||||
cfg.Providers = map[string]*ProviderConfig{
|
||||
"alpha": {Type: "openai", BaseURL: "http://alpha.invalid", APIKey: "sk-alpha"},
|
||||
}
|
||||
cfg.Groups = map[string]*GroupConfig{"eng": {}, "ops": {}}
|
||||
cfg.Users = &UsersConfig{Inline: []*UserConfig{
|
||||
{Name: "carol", Group: "eng", Groups: []string{"ops"}, APIKey: "sk-carol"},
|
||||
{Name: "admin1", Group: "eng", APIKey: "sk-admin1", Admin: true},
|
||||
}}
|
||||
h, _ := newAdminGatewayHandle(t, cfg)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, adminRequest(http.MethodGet, adminUsagePath, "sk-admin1"))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var got adminUsageResponse
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
carol := findUsageEntry(t, got.Users, "carol")
|
||||
if carol.GroupName != "eng" {
|
||||
t.Errorf("carol.GroupName = %q, want %q (first group, back-compat)", carol.GroupName, "eng")
|
||||
}
|
||||
if !slices.Equal(carol.Groups, []string{"eng", "ops"}) {
|
||||
t.Errorf("carol.Groups = %v, want [eng ops]", carol.Groups)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLimiterCurrentUsage_StoreDown drives limiter.currentUsage's
|
||||
// fail-closed path directly: a configured store whose every operation
|
||||
// errors, with failOpen false, must report storeDown and zero values
|
||||
|
||||
Reference in New Issue
Block a user