ultisuite-backend/internal/apitokens/policy.go
R3D347HR4Y 1d063237b9
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
feat(transcription): integrate Faster Whisper for Jitsi transcriptions
- Added support for Faster Whisper transcription via Jigasi and Skynet.
- Updated .env.example to include new environment variables for transcription settings.
- Enhanced Jitsi Docker Compose configuration to include Skynet and Jigasi services.
- Introduced new API endpoints for managing organizational folders in the drive service.
- Updated Nextcloud initialization script to enable external file mounting.
- Improved error handling and response structures in the drive API.
- Added new properties for organization settings related to transcription and agenda management.
2026-06-12 19:10:18 +02:00

341 lines
12 KiB
Go

package apitokens
import (
"net/http"
"strings"
)
type ScopeHint int
const (
ScopeNone ScopeHint = iota
ScopeMailAccountQuery
ScopeMailAccountPath
ScopeDrivePathFromURL
)
type Requirement struct {
Resource string
Alternatives []string
Write bool
ScopeHint ScopeHint
}
func RequirementForRequest(method, fullPath, typesQuery string) (Requirement, bool) {
method = strings.ToUpper(strings.TrimSpace(method))
path := strings.TrimSuffix(strings.TrimSpace(fullPath), "/")
if path == "" {
path = "/"
}
write := method != http.MethodGet && method != http.MethodHead
switch {
case strings.HasPrefix(path, "/api/v1/ai/chat/completions"),
strings.HasPrefix(path, "/api/v1/ai/v1/chat/completions"):
return Requirement{Resource: "automation.chat", Write: true}, true
case strings.HasPrefix(path, "/api/v1/ai/sessions"),
strings.HasPrefix(path, "/api/v1/ai/chats/sync"):
return Requirement{Resource: "automation.chat", Write: true}, true
case strings.HasPrefix(path, "/api/v1/ai/chats/"):
if write || method == http.MethodDelete {
return Requirement{Resource: "automation.chat", Write: true}, true
}
return Requirement{Resource: "automation.chat", Write: false}, true
case strings.HasPrefix(path, "/api/v1/ai/quota"),
strings.HasPrefix(path, "/api/v1/ai/models"):
return Requirement{Resource: "automation.chat", Write: false}, true
case strings.HasPrefix(path, "/api/v1/mail/api-tokens"):
return Requirement{Resource: "automation.api_tokens", Write: write || method == http.MethodDelete}, true
case strings.HasPrefix(path, "/api/v1/mail/webhooks"):
return Requirement{Resource: "automation.webhooks", Write: write}, true
case strings.HasPrefix(path, "/api/v1/mail/rules"):
return Requirement{Resource: "automation.rules", Write: write}, true
case strings.HasPrefix(path, "/api/v1/contacts/discovery/llm-settings"),
strings.HasPrefix(path, "/api/v1/contacts/discovery/llm-models/"):
return Requirement{Resource: "automation.llm", Write: write}, true
case strings.HasPrefix(path, "/api/v1/contacts/discovery/search-settings"):
return Requirement{Resource: "automation.search", Write: write}, true
case strings.HasPrefix(path, "/api/v1/contacts/discovery/"):
return Requirement{Resource: "contacts.write", Write: write}, true
case strings.HasPrefix(path, "/api/v1/contacts/search"):
return Requirement{Resource: "contacts.search", Write: false}, true
case strings.HasPrefix(path, "/api/v1/contacts/"):
switch method {
case http.MethodPost, http.MethodPut, http.MethodPatch:
if strings.Contains(path, "/merge-duplicates") || strings.Contains(path, "/improve") {
return Requirement{Resource: "contacts.write", Write: true}, true
}
if strings.Contains(path, "/books/") {
return Requirement{Resource: "contacts.write", Write: true}, true
}
return Requirement{Resource: "contacts.write", Write: true}, true
case http.MethodDelete:
return Requirement{Resource: "contacts.delete", Write: true}, true
default:
return Requirement{Resource: "contacts.read", Write: false}, true
}
case strings.HasPrefix(path, "/api/v1/drive/"):
return driveRequirement(method, path)
case strings.HasPrefix(path, "/api/v1/calendar/"):
return calendarRequirement(method, path)
case strings.HasPrefix(path, "/api/v1/richtext/"):
return richtextRequirement(method, path)
case strings.HasPrefix(path, "/api/v1/search"):
return searchRequirement(typesQuery)
case strings.HasPrefix(path, "/api/v1/mail/"):
return mailRequirement(method, path)
}
return Requirement{}, false
}
func mailRequirement(method, path string) (Requirement, bool) {
write := method != http.MethodGet && method != http.MethodHead
switch {
case strings.HasPrefix(path, "/api/v1/mail/settings"):
return Requirement{Resource: "mail.settings", Write: write}, true
case strings.HasPrefix(path, "/api/v1/mail/search"):
return Requirement{Resource: "mail.search", Write: false, ScopeHint: ScopeMailAccountQuery}, true
case strings.HasPrefix(path, "/api/v1/mail/send"),
strings.HasPrefix(path, "/api/v1/mail/outbox/"):
return Requirement{Resource: "mail.send", Write: true}, true
case strings.HasPrefix(path, "/api/v1/mail/signatures"):
return Requirement{Resource: "mail.settings", Write: write}, true
case strings.HasPrefix(path, "/api/v1/mail/identities/"):
return Requirement{Resource: "mail.identities", Write: write}, true
case strings.Contains(path, "/accounts/") && strings.Contains(path, "/identities"):
return Requirement{Resource: "mail.identities", Write: write, ScopeHint: ScopeMailAccountPath}, true
case strings.HasPrefix(path, "/api/v1/mail/accounts"):
if write {
return Requirement{Resource: "mail.settings", Write: true, ScopeHint: ScopeMailAccountPath}, true
}
return Requirement{Resource: "mail.mailboxes", Write: false, ScopeHint: ScopeMailAccountPath}, true
case strings.HasPrefix(path, "/api/v1/mail/unified-folders"),
strings.HasPrefix(path, "/api/v1/mail/folders"):
return Requirement{Resource: "mail.mailboxes", Write: write, ScopeHint: ScopeMailAccountQuery}, true
case strings.HasPrefix(path, "/api/v1/mail/labels"):
return Requirement{Resource: "mail.labels", Write: write}, true
case strings.HasPrefix(path, "/api/v1/mail/attachments/"),
strings.Contains(path, "/attachments"):
if write {
return Requirement{Resource: "mail.attachments", Write: true}, true
}
return Requirement{Resource: "mail.attachments", Write: false}, true
case strings.HasPrefix(path, "/api/v1/mail/messages"):
if strings.HasSuffix(path, "/labels") || strings.HasSuffix(path, "/flags") {
return Requirement{Resource: "mail.labels", Write: true}, true
}
if write {
return Requirement{Resource: "mail.labels", Write: true}, true
}
return Requirement{Resource: "mail.messages", Write: false, ScopeHint: ScopeMailAccountQuery}, true
case strings.HasPrefix(path, "/api/v1/mail/threads"):
return Requirement{Resource: "mail.messages", Write: false}, true
case strings.HasPrefix(path, "/api/v1/mail/drafts"):
if write {
return Requirement{Resource: "mail.send", Write: true}, true
}
return Requirement{Resource: "mail.messages", Write: false}, true
default:
return Requirement{}, false
}
}
func richtextRequirement(method, path string) (Requirement, bool) {
write := method != http.MethodGet && method != http.MethodHead
switch {
case strings.HasSuffix(path, "/save"),
strings.HasSuffix(path, "/assets"),
strings.HasSuffix(path, "/user-paragraph-styles"):
return Requirement{Resource: "drive.files", Write: true}, true
default:
return Requirement{Resource: "drive.files", Write: write}, true
}
}
func driveRequirement(method, path string) (Requirement, bool) {
write := method != http.MethodGet && method != http.MethodHead
switch {
case strings.Contains(path, "/preview/"):
return Requirement{Resource: "drive.thumbnails", Write: false, ScopeHint: ScopeDrivePathFromURL}, true
case strings.Contains(path, "/download/"):
return Requirement{Resource: "drive.download", Write: false, ScopeHint: ScopeDrivePathFromURL}, true
case strings.Contains(path, "/shares"):
return Requirement{Resource: "drive.share", Write: write}, true
case strings.Contains(path, "/move"):
return Requirement{Resource: "drive.move", Write: true}, true
case strings.Contains(path, "/copy"):
return Requirement{Resource: "drive.copy", Write: true}, true
case strings.Contains(path, "/rename"):
return Requirement{Resource: "drive.rename", Write: true}, true
case strings.Contains(path, "/files/") || strings.Contains(path, "/folders/"):
if write {
return Requirement{Resource: "drive.upload", Write: true, ScopeHint: ScopeDrivePathFromURL}, true
}
return Requirement{
Resource: "drive.folders",
Alternatives: []string{"drive.files"},
Write: false,
ScopeHint: ScopeDrivePathFromURL,
}, true
case strings.Contains(path, "/search"),
strings.Contains(path, "/recent"),
strings.Contains(path, "/starred"),
strings.Contains(path, "/shared"),
strings.Contains(path, "/filter-corpus"),
strings.Contains(path, "/quota"),
strings.Contains(path, "/trash"):
if write {
return Requirement{Resource: "drive.upload", Write: true}, true
}
return Requirement{Resource: "drive.files", Write: false}, true
default:
return Requirement{}, false
}
}
func calendarRequirement(method, path string) (Requirement, bool) {
write := method != http.MethodGet && method != http.MethodHead
switch {
case strings.HasSuffix(path, "/freebusy"):
return Requirement{Resource: "agenda.freebusy", Write: false}, true
case strings.Contains(path, "/events/response/"):
return Requirement{Resource: "agenda.response", Write: true}, true
case strings.Contains(path, "/events/meet-link/"):
return Requirement{Resource: "agenda.events.write", Write: true}, true
case strings.Contains(path, "/events/"):
if write {
return Requirement{Resource: "agenda.events.write", Write: true}, true
}
return Requirement{Resource: "agenda.events", Write: false}, true
case method == http.MethodDelete:
return Requirement{Resource: "agenda.events.delete", Write: true}, true
case write:
return Requirement{Resource: "agenda.calendars", Write: true}, true
default:
return Requirement{Resource: "agenda.calendars", Write: false}, true
}
}
func searchRequirement(typesQuery string) (Requirement, bool) {
types := parseSearchTypes(typesQuery)
if len(types) == 0 {
return Requirement{Resource: "mail.search", Write: false, ScopeHint: ScopeMailAccountQuery}, true
}
req := Requirement{Write: false, ScopeHint: ScopeMailAccountQuery}
for _, t := range types {
switch t {
case "mail":
req.Resource = "mail.search"
case "contacts":
req.Resource = "contacts.search"
case "drive":
req.Resource = "drive.files"
default:
continue
}
return req, true
}
return Requirement{Resource: "mail.search", Write: false, ScopeHint: ScopeMailAccountQuery}, true
}
func parseSearchTypes(raw string) []string {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(strings.ToLower(part))
if part != "" {
out = append(out, part)
}
}
return out
}
func AllowsRequirement(auth *AuthContext, req Requirement) bool {
if auth == nil {
return true
}
if req.Resource == "automation.api_tokens" && !req.Write {
return HasPermission(auth, req.Resource, true)
}
if HasPermission(auth, req.Resource, req.Write) {
return true
}
for _, alt := range req.Alternatives {
if HasPermission(auth, alt, req.Write) {
return true
}
}
return false
}
func SearchRequirements(typesQuery string) []Requirement {
types := parseSearchTypes(typesQuery)
if len(types) == 0 {
return []Requirement{{Resource: "mail.search", Write: false, ScopeHint: ScopeMailAccountQuery}}
}
reqs := make([]Requirement, 0, len(types))
for _, t := range types {
switch t {
case "mail":
reqs = append(reqs, Requirement{Resource: "mail.search", Write: false, ScopeHint: ScopeMailAccountQuery})
case "contacts":
reqs = append(reqs, Requirement{Resource: "contacts.search", Write: false})
case "drive":
reqs = append(reqs, Requirement{Resource: "drive.files", Write: false})
}
}
if len(reqs) == 0 {
return []Requirement{{Resource: "mail.search", Write: false, ScopeHint: ScopeMailAccountQuery}}
}
return reqs
}
func ExtractMailAccountID(path, queryAccountID string) string {
if id := strings.TrimSpace(queryAccountID); id != "" {
return id
}
parts := strings.Split(strings.Trim(path, "/"), "/")
for i := 0; i < len(parts)-1; i++ {
if parts[i] == "accounts" && i+1 < len(parts) {
return parts[i+1]
}
}
return ""
}
func ExtractDrivePathFromURL(fullPath string) string {
markers := []string{
"/api/v1/drive/files/",
"/api/v1/drive/download/",
"/api/v1/drive/preview/",
"/api/v1/drive/folders/",
}
for _, marker := range markers {
if idx := strings.Index(fullPath, marker); idx >= 0 {
rest := fullPath[idx+len(marker):]
if rest == "" {
return "/"
}
return NormalizeDriveScopePath("/" + rest)
}
}
return ""
}