feat: publish sanitized custom.97 source

This commit is contained in:
cneicy committed 2026-10-27 20:43:39 +08:00
1 parent 8e6ed0317b
commit 86c0276156
1022 files changed
+69903 -19207

No files matched your search

+273
View File
@@ -0,0 +1,273 @@
package service
import (
"errors"
"slices"
"strings"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/service/authz"
)
const (
AccessTokenGroupPersonal = "personal"
AccessTokenGroupAdmin = "admin"
AccessTokenGroupSystem = "system"
AccessTokenMaxPerUser = 20
AccessTokenDefaultExpiryDays = 30
accessTokenActionReveal = "reveal"
accessTokenScopeSeparator = ":"
)
var (
ErrAccessTokenScopeInvalid = errors.New("access token scope is invalid")
ErrAccessTokenScopeForbidden = errors.New("access token scope cannot be granted")
)
// AccessTokenResource mirrors authz.ResourceDefinition so both catalogs share
// one JSON shape and one frontend matrix component.
type AccessTokenResource struct {
Resource string `json:"resource"`
LabelKey string `json:"label_key"`
Actions []authz.ActionDefinition `json:"actions"`
group string
minRole int
}
type AccessTokenCatalogGroup struct {
Group string `json:"group"`
Resources []AccessTokenResource `json:"resources"`
}
func accessTokenView(description string) authz.ActionDefinition {
return authz.ActionDefinition{Action: authz.ActionRead, LabelKey: "View", DescriptionKey: description}
}
func accessTokenEdit(description string) authz.ActionDefinition {
return authz.ActionDefinition{Action: authz.ActionWrite, LabelKey: "Edit", DescriptionKey: description}
}
// accessTokenStaticResources covers dashboard routes that are not guarded by a
// Casbin permission. Casbin resources come from authz.Catalog() at runtime and
// must never be duplicated here.
var accessTokenStaticResources = []AccessTokenResource{
{Resource: "profile", LabelKey: "Profile", group: AccessTokenGroupPersonal, minRole: common.RoleCommonUser, Actions: []authz.ActionDefinition{
accessTokenView("View your profile, groups, and available models."),
accessTokenEdit("Change your display name and personal settings."),
}},
{Resource: "api_key", LabelKey: "API keys", group: AccessTokenGroupPersonal, minRole: common.RoleCommonUser, Actions: []authz.ActionDefinition{
accessTokenView("View API keys and their settings without the full key."),
accessTokenEdit("Create, edit, and delete API keys."),
{Action: accessTokenActionReveal, LabelKey: "Reveal full keys", DescriptionKey: "View complete API keys."},
}},
{Resource: "usage", LabelKey: "Usage", group: AccessTokenGroupPersonal, minRole: common.RoleCommonUser, Actions: []authz.ActionDefinition{
accessTokenView("View your usage logs, statistics, tasks, and audit logs."),
}},
{Resource: "wallet", LabelKey: "Wallet", group: AccessTokenGroupPersonal, minRole: common.RoleCommonUser, Actions: []authz.ActionDefinition{
accessTokenView("View balance, top-up records, subscriptions, and referral information."),
accessTokenEdit("Top up, pay for subscriptions, check in, and transfer referral rewards."),
}},
{Resource: "account_security", LabelKey: "Account security", group: AccessTokenGroupPersonal, minRole: common.RoleCommonUser, Actions: []authz.ActionDefinition{
accessTokenView("View Passkey, two-factor, and sign-in binding status."),
accessTokenEdit("Includes changes that require security verification, such as password, two-factor settings, and account deletion."),
}},
{Resource: "user", LabelKey: "Users", group: AccessTokenGroupAdmin, minRole: common.RoleAdminUser, Actions: []authz.ActionDefinition{
accessTokenView("View users, their sign-in bindings, and permission settings."),
accessTokenEdit("Create, edit, disable, and delete users, and reset their security settings."),
}},
{Resource: "billing", LabelKey: "Billing management", group: AccessTokenGroupAdmin, minRole: common.RoleAdminUser, Actions: []authz.ActionDefinition{
accessTokenView("View all top-up records, subscription plans, and user subscriptions."),
accessTokenEdit("Complete top-up orders and manage subscription plans and user subscriptions."),
}},
{Resource: "model", LabelKey: "Models", group: AccessTokenGroupAdmin, minRole: common.RoleAdminUser, Actions: []authz.ActionDefinition{
accessTokenView("View models and vendors, and preview upstream sync."),
accessTokenEdit("Edit models and vendors, and sync them from upstream."),
}},
{Resource: "deployment", LabelKey: "Deployments", group: AccessTokenGroupAdmin, minRole: common.RoleAdminUser, Actions: []authz.ActionDefinition{
accessTokenView("View deployments and price estimates."),
accessTokenEdit("Create, edit, and delete deployments, and test connections."),
}},
{Resource: "log", LabelKey: "Logs", group: AccessTokenGroupAdmin, minRole: common.RoleAdminUser, Actions: []authz.ActionDefinition{
accessTokenView("View all users' usage logs, statistics, and tasks."),
}},
{Resource: "redemption", LabelKey: "Redemption codes", group: AccessTokenGroupAdmin, minRole: common.RoleAdminUser, Actions: []authz.ActionDefinition{
accessTokenView("View redemption codes."),
accessTokenEdit("Create, edit, and delete redemption codes."),
}},
{Resource: "group", LabelKey: "Groups", group: AccessTokenGroupAdmin, minRole: common.RoleAdminUser, Actions: []authz.ActionDefinition{
accessTokenView("View groups and prefill groups."),
accessTokenEdit("Create, edit, and delete prefill groups."),
}},
{Resource: "option", LabelKey: "System settings", group: AccessTokenGroupSystem, minRole: common.RoleRootUser, Actions: []authz.ActionDefinition{
accessTokenView("View system settings, custom OAuth providers, and price sync sources."),
accessTokenEdit("Change system settings and custom OAuth providers, and sync prices."),
}},
{Resource: "plugin", LabelKey: "Task plugins", group: AccessTokenGroupSystem, minRole: common.RoleRootUser, Actions: []authz.ActionDefinition{
accessTokenView("View installed task plugins and their settings."),
accessTokenEdit("Install, configure, test, and remove task plugins."),
}},
{Resource: "ops", LabelKey: "Operations", group: AccessTokenGroupSystem, minRole: common.RoleRootUser, Actions: []authz.ActionDefinition{
accessTokenView("View performance, system tasks, and system information."),
accessTokenEdit("Run and manage maintenance tasks and performance settings."),
}},
}
// AccessTokenScopeOf is the token scope key for a Casbin permission.
func AccessTokenScopeOf(permission authz.Permission) string {
return permission.Resource + accessTokenScopeSeparator + permission.Action
}
// AccessTokenCatalog lists the scopes this user may grant right now, grouped
// for display. Resources without a grantable action and empty groups are
// omitted.
func AccessTokenCatalog(userID, role int) []AccessTokenCatalogGroup {
resources := map[string][]AccessTokenResource{}
for _, resource := range accessTokenStaticResources {
if role < resource.minRole {
continue
}
resource.Actions = slices.Clone(resource.Actions)
resources[resource.group] = append(resources[resource.group], resource)
}
if role >= common.RoleAdminUser {
for _, definition := range authz.Catalog() {
resource := AccessTokenResource{Resource: definition.Resource, LabelKey: definition.LabelKey}
for _, action := range definition.Actions {
if authz.Can(userID, role, authz.Permission{Resource: definition.Resource, Action: action.Action}) {
resource.Actions = append(resource.Actions, action)
}
}
if len(resource.Actions) > 0 {
resources[AccessTokenGroupAdmin] = append(resources[AccessTokenGroupAdmin], resource)
}
}
}
groups := make([]AccessTokenCatalogGroup, 0, 3)
for _, group := range []string{AccessTokenGroupPersonal, AccessTokenGroupAdmin, AccessTokenGroupSystem} {
if len(resources[group]) > 0 {
groups = append(groups, AccessTokenCatalogGroup{Group: group, Resources: resources[group]})
}
}
return groups
}
// AccessTokenScopeDictionary lists every resource a token scope can name,
// whatever the viewer may grant, so stored grants keep their labels after the
// granting user loses a permission or when another user reads them.
func AccessTokenScopeDictionary() []AccessTokenResource {
definitions := authz.Catalog()
resources := make([]AccessTokenResource, 0, len(accessTokenStaticResources)+len(definitions))
for _, resource := range accessTokenStaticResources {
resource.Actions = slices.Clone(resource.Actions)
resources = append(resources, resource)
}
for _, definition := range definitions {
resources = append(resources, AccessTokenResource{Resource: definition.Resource, LabelKey: definition.LabelKey, Actions: definition.Actions})
}
return resources
}
// NormalizeAccessTokenScopes validates a requested grant against the catalog
// the user sees and returns it trimmed, de-duplicated and sorted.
func NormalizeAccessTokenScopes(userID, role int, scopes []string) ([]string, error) {
normalized, ok := NormalizeAccessTokenScopeList(scopes)
if !ok {
return nil, ErrAccessTokenScopeInvalid
}
grantable := make(map[string]bool)
for _, group := range AccessTokenCatalog(userID, role) {
for _, resource := range group.Resources {
for _, action := range resource.Actions {
grantable[resource.Resource+accessTokenScopeSeparator+action.Action] = true
}
}
}
for _, scope := range normalized {
if grantable[scope] {
continue
}
if !knownAccessTokenScope(scope) {
return nil, ErrAccessTokenScopeInvalid
}
return nil, ErrAccessTokenScopeForbidden
}
return normalized, nil
}
func knownAccessTokenScope(scope string) bool {
resourceName, action, ok := strings.Cut(scope, accessTokenScopeSeparator)
if !ok {
return false
}
for _, resource := range accessTokenStaticResources {
if resource.Resource == resourceName && slices.ContainsFunc(resource.Actions, func(definition authz.ActionDefinition) bool { return definition.Action == action }) {
return true
}
}
return slices.Contains(authz.AllPermissions(), authz.Permission{Resource: resourceName, Action: action})
}
func AccessTokenScopeGranted(scopes []string, scope string) bool {
return scope != "" && slices.Contains(scopes, scope)
}
// accessTokenVerificationScopes maps every verification scope to the token
// scope a PAT needs to obtain its proof. An empty value means PATs can never
// obtain it; scopes missing from this table are denied too.
var accessTokenVerificationScopes = map[string]string{
VerificationScopeChannelKeyRead: AccessTokenScopeOf(authz.ChannelSecretView),
VerificationScopeAdminUserCreate: "user:write",
VerificationScopeAdminUserUpdate: "user:write",
VerificationScopeAdminUserDelete: "user:write",
VerificationScopeAdminUserManage: "user:write",
VerificationScopeAdminUserPasskeyReset: "user:write",
VerificationScopeAdminUserTwoFADisable: "user:write",
VerificationScopeAdminUserBindingClear: "user:write",
VerificationScopePasskeyRegister: "account_security:write",
VerificationScopePasskeyDelete: "account_security:write",
VerificationScopeTwoFASetup: "account_security:write",
VerificationScopeTwoFADisable: "account_security:write",
VerificationScopeTwoFABackupCodes: "account_security:write",
VerificationScopePasswordSet: "account_security:write",
VerificationScopePasswordChange: "account_security:write",
VerificationScopeAccountBind: "account_security:write",
VerificationScopeAccountUnbind: "account_security:write",
VerificationScopeAccountDelete: "account_security:write",
VerificationScopeAccessTokenGenerate: "",
VerificationScopeAccessTokenUpdate: "",
VerificationScopeAccessTokenRevoke: "",
VerificationScopeLogin: "",
}
// AccessTokenVerificationScope returns the token scope a PAT needs for a
// verification scope and whether the scope is listed at all.
func AccessTokenVerificationScope(verificationScope string) (scope string, listed bool) {
scope, listed = accessTokenVerificationScopes[verificationScope]
return scope, listed
}
// requireAccessTokenVerificationScope rejects a step-up request from a PAT
// whose grant does not cover the operation. Browser sessions pass through.
func requireAccessTokenVerificationScope(identity AuthIdentity, verificationScope string) error {
tokenID, ok := model.ParseAccessTokenSessionID(identity.SessionID)
if !ok {
return nil
}
required, _ := AccessTokenVerificationScope(verificationScope)
if required == "" {
return ErrVerificationForbidden
}
token, err := model.GetUserAccessToken(identity.UserID, tokenID)
if errors.Is(err, model.ErrAccessTokenNotFound) {
return ErrAuthTokenInvalid
}
if err != nil {
return err
}
if !AccessTokenScopeGranted(token.GetScopes(), required) {
return ErrVerificationForbidden
}
return nil
}