feat: publish sanitized custom.97 source
This commit is contained in:
1022 files changed
+69903
-19207
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user