- Updated environment configuration to unify frontend for mail and drive under a single service. - Revised README to reflect changes in frontend setup and routing for the unified application. - Introduced new API documentation endpoints for better accessibility of API specifications. - Enhanced drive and mail services with improved handling of file uploads and metadata enrichment. - Implemented new API token management features, including creation, listing, and revocation of tokens. - Added tests for new functionalities in drive and mail services to ensure reliability and correctness.
284 lines
9.6 KiB
Go
284 lines
9.6 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/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/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 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 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 ""
|
|
}
|