Changeset 0.24.2 (#158)
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
capspkg "git.gobha.me/xcaliber/chat-switchboard/capabilities"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/auth"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/health"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
@@ -56,6 +57,23 @@ func (h *ModelHandler) ListEnabledModels(c *gin.Context) {
|
||||
// Include admin default model so frontend can use it in resolution chain
|
||||
defaultModel, _ := h.stores.Policies.Get(c.Request.Context(), "default_model")
|
||||
|
||||
// Model allowlist filtering (v0.24.2) — non-admin users with group-level
|
||||
// model restrictions only see permitted models. Persona entries whose
|
||||
// underlying model is disallowed are also filtered.
|
||||
role, _ := c.Get("role")
|
||||
if role != "admin" {
|
||||
allowlist, err := auth.ResolveModelAllowlist(c.Request.Context(), h.stores, userID)
|
||||
if err == nil && allowlist != nil {
|
||||
filtered := make([]models.UserModel, 0, len(userModels))
|
||||
for _, m := range userModels {
|
||||
if allowlist[m.ModelID] {
|
||||
filtered = append(filtered, m)
|
||||
}
|
||||
}
|
||||
userModels = filtered
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"models": userModels, "default_model": defaultModel})
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/auth"
|
||||
capspkg "git.gobha.me/xcaliber/chat-switchboard/capabilities"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/crypto"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||
@@ -625,6 +626,44 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
|
||||
provReq.Tools = h.buildToolDefs(c.Request.Context(), userID, hasBrowserTools, req.DisabledTools, workspaceID)
|
||||
}
|
||||
|
||||
// ── Permission pre-flight ─────────────────────────────────────────────
|
||||
// Budget enforcement and model allowlist only apply when the request draws
|
||||
// from a team or global provider. BYOK (personal scope) is the user's money
|
||||
// — no ceiling applies.
|
||||
role, _ := c.Get("role")
|
||||
if role != "admin" && providerScope != "personal" {
|
||||
// Token budget
|
||||
if h.stores.Usage != nil {
|
||||
budget, err := auth.ResolveTokenBudget(c.Request.Context(), h.stores, userID)
|
||||
if err == nil && budget != nil {
|
||||
var opts store.UsageQueryOptions
|
||||
now := time.Now()
|
||||
if budget.Period == "daily" {
|
||||
dayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
|
||||
opts.Since = &dayStart
|
||||
} else {
|
||||
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
|
||||
opts.Since = &monthStart
|
||||
}
|
||||
totals, err := h.stores.Usage.GetPersonalTotals(c.Request.Context(), userID, opts)
|
||||
if err == nil && totals != nil {
|
||||
used := int64(totals.InputTokens + totals.OutputTokens)
|
||||
if used >= budget.Limit {
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{"error": "token budget exceeded"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Model allowlist
|
||||
allowlist, err := auth.ResolveModelAllowlist(c.Request.Context(), h.stores, userID)
|
||||
if err == nil && allowlist != nil && !allowlist[model] {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "model not permitted for your account: " + model})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Determine streaming
|
||||
stream := true // default
|
||||
if req.Stream != nil {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/auth"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/notifications"
|
||||
@@ -22,8 +23,15 @@ type createGroupRequest struct {
|
||||
}
|
||||
|
||||
type updateGroupRequest struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Permissions *[]string `json:"permissions,omitempty"`
|
||||
TokenBudgetDaily *int64 `json:"token_budget_daily,omitempty"`
|
||||
TokenBudgetMonthly *int64 `json:"token_budget_monthly,omitempty"`
|
||||
AllowedModels *[]string `json:"allowed_models,omitempty"`
|
||||
ClearBudgetDaily bool `json:"clear_budget_daily,omitempty"`
|
||||
ClearBudgetMonthly bool `json:"clear_budget_monthly,omitempty"`
|
||||
ClearAllowedModels bool `json:"clear_allowed_models,omitempty"`
|
||||
}
|
||||
|
||||
type addGroupMemberRequest struct {
|
||||
@@ -85,7 +93,7 @@ func (h *GroupHandler) CreateGroup(c *gin.Context) {
|
||||
Description: req.Description,
|
||||
Scope: req.Scope,
|
||||
TeamID: req.TeamID,
|
||||
CreatedBy: actorID,
|
||||
CreatedBy: &actorID,
|
||||
}
|
||||
|
||||
if err := h.stores.Groups.Create(c.Request.Context(), g); err != nil {
|
||||
@@ -132,12 +140,34 @@ func (h *GroupHandler) UpdateGroup(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == nil && req.Description == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
|
||||
return
|
||||
patch := models.GroupPatch{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Permissions: req.Permissions,
|
||||
TokenBudgetDaily: req.TokenBudgetDaily,
|
||||
TokenBudgetMonthly: req.TokenBudgetMonthly,
|
||||
AllowedModels: req.AllowedModels,
|
||||
ClearBudgetDaily: req.ClearBudgetDaily,
|
||||
ClearBudgetMonthly: req.ClearBudgetMonthly,
|
||||
ClearAllowedModels: req.ClearAllowedModels,
|
||||
}
|
||||
|
||||
err := h.stores.Groups.Update(c.Request.Context(), id, req.Name, req.Description)
|
||||
// Validate permissions if provided
|
||||
if patch.Permissions != nil {
|
||||
valid := auth.AllPermissions
|
||||
validSet := make(map[string]bool, len(valid))
|
||||
for _, p := range valid {
|
||||
validSet[p] = true
|
||||
}
|
||||
for _, p := range *patch.Permissions {
|
||||
if !validSet[p] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "unknown permission: " + p})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := h.stores.Groups.Update(c.Request.Context(), id, patch)
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "group not found"})
|
||||
return
|
||||
@@ -166,6 +196,11 @@ func (h *GroupHandler) DeleteGroup(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
// Store layer returns a plain error for system groups.
|
||||
if err.Error() == "system groups cannot be deleted" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "system groups cannot be deleted"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "delete failed"})
|
||||
return
|
||||
}
|
||||
@@ -382,3 +417,28 @@ func (h *GroupHandler) DeleteResourceGrant(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
AuditLog(c, "grant.delete", resourceType, resourceID, nil)
|
||||
}
|
||||
|
||||
// ListPermissions returns all valid permission strings.
|
||||
// GET /api/v1/admin/permissions
|
||||
func (h *GroupHandler) ListPermissions(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"permissions": auth.AllPermissions})
|
||||
}
|
||||
|
||||
// GetUserPermissions returns the effective permissions for a given user.
|
||||
// GET /api/v1/admin/users/:id/permissions
|
||||
func (h *GroupHandler) GetUserPermissions(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
|
||||
perms, err := auth.ResolvePermissions(c.Request.Context(), h.stores, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to resolve permissions"})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert map to sorted slice for stable output
|
||||
list := make([]string, 0, len(perms))
|
||||
for p := range perms {
|
||||
list = append(list, p)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"permissions": list, "user_id": userID})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user