730099d2aa
domain_admin sah und konnte IMAP-Konten (inkl. Credentials) fremder Tenants auflisten, löschen, synchronisieren und umkonfigurieren, da Store.List() für Admins ungefiltert alle Konten lieferte und die Einzelhandler nur den Owner, nicht den Tenant prüften. - Store.List() filtert jetzt nach tenant_id, außer für superadmin - Store.Create() setzt tenant_id beim Anlegen - Alle Einzelhandler (delete/start-import/progress/sync/update) prüfen zusätzlich tenantAccessAllowed()
617 lines
18 KiB
Go
617 lines
18 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"archivmail/internal/auth"
|
|
imapstore "archivmail/internal/imap"
|
|
pop3store "archivmail/internal/pop3"
|
|
"archivmail/internal/userstore"
|
|
)
|
|
|
|
// ── IMAP handlers ─────────────────────────────────────────────────────────
|
|
|
|
// tenantAccessAllowed checks whether the given session may access an IMAP/POP3
|
|
// account belonging to accTenantID. Superadmins (sess.TenantID == nil) may
|
|
// access any tenant. Other admins may only access accounts within their own
|
|
// tenant (accTenantID must be set and match).
|
|
func tenantAccessAllowed(sess *auth.Session, accTenantID *int64) bool {
|
|
if sess.TenantID == nil {
|
|
return true
|
|
}
|
|
return accTenantID != nil && *accTenantID == *sess.TenantID
|
|
}
|
|
|
|
func (s *Server) handleListImap(w http.ResponseWriter, r *http.Request) {
|
|
if s.imapStore == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "IMAP not configured")
|
|
return
|
|
}
|
|
sess := sessionFromCtx(r.Context())
|
|
// SEC-03: Use HasRole to correctly check admin privileges (domain_admin, admin, superadmin).
|
|
isAdmin := auth.HasRole(sess.Role, userstore.RoleDomainAdmin)
|
|
accounts, err := s.imapStore.List(r.Context(), sess.Username, isAdmin, sess.TenantID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to list IMAP accounts")
|
|
return
|
|
}
|
|
if accounts == nil {
|
|
accounts = []imapstore.Account{}
|
|
}
|
|
writeJSON(w, http.StatusOK, accounts)
|
|
}
|
|
|
|
func (s *Server) handleCreateImap(w http.ResponseWriter, r *http.Request) {
|
|
if s.imapStore == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "IMAP not configured")
|
|
return
|
|
}
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
TLS string `json:"tls"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
ExcludedFolders []string `json:"excluded_folders"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
if req.Name == "" || req.Host == "" || req.Username == "" || req.Password == "" {
|
|
writeError(w, http.StatusBadRequest, "name, host, username and password are required")
|
|
return
|
|
}
|
|
if req.Port <= 0 {
|
|
req.Port = 993
|
|
}
|
|
if req.TLS == "" {
|
|
req.TLS = "ssl"
|
|
}
|
|
if req.ExcludedFolders == nil {
|
|
req.ExcludedFolders = []string{}
|
|
}
|
|
|
|
sess := sessionFromCtx(r.Context())
|
|
acc := imapstore.Account{
|
|
Owner: sess.Username,
|
|
Name: req.Name,
|
|
Host: req.Host,
|
|
Port: req.Port,
|
|
TLS: req.TLS,
|
|
Username: req.Username,
|
|
ExcludedFolders: req.ExcludedFolders,
|
|
TenantID: sess.TenantID,
|
|
}
|
|
|
|
created, err := s.imapStore.Create(r.Context(), acc, req.Password)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to create IMAP account")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, created)
|
|
}
|
|
|
|
func (s *Server) handleDeleteImap(w http.ResponseWriter, r *http.Request) {
|
|
if s.imapStore == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "IMAP not configured")
|
|
return
|
|
}
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
acc, err := s.imapStore.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "account not found")
|
|
return
|
|
}
|
|
|
|
sess := sessionFromCtx(r.Context())
|
|
if acc.Owner != sess.Username && !auth.HasRole(sess.Role, userstore.RoleDomainAdmin) {
|
|
writeError(w, http.StatusForbidden, "access denied")
|
|
return
|
|
}
|
|
if !tenantAccessAllowed(sess, acc.TenantID) {
|
|
writeError(w, http.StatusForbidden, "access denied")
|
|
return
|
|
}
|
|
|
|
if err := s.imapStore.Delete(r.Context(), id); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to delete account")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
|
}
|
|
|
|
func (s *Server) handleTestImap(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
TLS string `json:"tls"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
if req.Host == "" || req.Username == "" || req.Password == "" {
|
|
writeError(w, http.StatusBadRequest, "host, username and password are required")
|
|
return
|
|
}
|
|
if req.Port <= 0 {
|
|
req.Port = 993
|
|
}
|
|
if req.TLS == "" {
|
|
req.TLS = "ssl"
|
|
}
|
|
|
|
c, err := imapstore.Connect(req.Host, req.Port, req.TLS)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"ok": false,
|
|
"error": fmt.Sprintf("Verbindung fehlgeschlagen: %v", err),
|
|
})
|
|
return
|
|
}
|
|
defer c.Close()
|
|
|
|
if err := c.Login(req.Username, req.Password).Wait(); err != nil {
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"ok": false,
|
|
"error": fmt.Sprintf("Anmeldung fehlgeschlagen: %v", err),
|
|
})
|
|
return
|
|
}
|
|
|
|
folders, err := imapstore.ListFolders(c.Client)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"ok": false,
|
|
"error": fmt.Sprintf("Ordner konnten nicht gelesen werden: %v", err),
|
|
})
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"ok": true,
|
|
"folders": folders,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleStartImport(w http.ResponseWriter, r *http.Request) {
|
|
if s.imapStore == nil || s.imapImporter == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "IMAP not configured")
|
|
return
|
|
}
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
acc, err := s.imapStore.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "account not found")
|
|
return
|
|
}
|
|
|
|
sess := sessionFromCtx(r.Context())
|
|
if acc.Owner != sess.Username && !auth.HasRole(sess.Role, userstore.RoleDomainAdmin) {
|
|
writeError(w, http.StatusForbidden, "access denied")
|
|
return
|
|
}
|
|
if !tenantAccessAllowed(sess, acc.TenantID) {
|
|
writeError(w, http.StatusForbidden, "access denied")
|
|
return
|
|
}
|
|
|
|
if acc.Status == "running" {
|
|
writeError(w, http.StatusConflict, "import already running")
|
|
return
|
|
}
|
|
|
|
go s.imapImporter.Run(context.Background(), id)
|
|
|
|
// Return current account state (status will switch to "running" shortly)
|
|
acc.Status = "running"
|
|
writeJSON(w, http.StatusOK, acc)
|
|
}
|
|
|
|
func (s *Server) handleImapProgress(w http.ResponseWriter, r *http.Request) {
|
|
if s.imapStore == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "IMAP not configured")
|
|
return
|
|
}
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
acc, err := s.imapStore.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "account not found")
|
|
return
|
|
}
|
|
|
|
sess := sessionFromCtx(r.Context())
|
|
if acc.Owner != sess.Username && !auth.HasRole(sess.Role, userstore.RoleDomainAdmin) {
|
|
writeError(w, http.StatusForbidden, "access denied")
|
|
return
|
|
}
|
|
if !tenantAccessAllowed(sess, acc.TenantID) {
|
|
writeError(w, http.StatusForbidden, "access denied")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, acc)
|
|
}
|
|
|
|
// handleSyncNow triggers an immediate incremental sync for a single IMAP account.
|
|
func (s *Server) handleSyncNow(w http.ResponseWriter, r *http.Request) {
|
|
if s.imapStore == nil || s.imapScheduler == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "IMAP not configured")
|
|
return
|
|
}
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
acc, err := s.imapStore.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "account not found")
|
|
return
|
|
}
|
|
|
|
sess := sessionFromCtx(r.Context())
|
|
if acc.Owner != sess.Username && !auth.HasRole(sess.Role, userstore.RoleDomainAdmin) {
|
|
writeError(w, http.StatusForbidden, "access denied")
|
|
return
|
|
}
|
|
if !tenantAccessAllowed(sess, acc.TenantID) {
|
|
writeError(w, http.StatusForbidden, "access denied")
|
|
return
|
|
}
|
|
|
|
if err := s.imapScheduler.TriggerSync(r.Context(), id); err != nil {
|
|
s.logger.Error("trigger sync failed", "err", err)
|
|
writeError(w, http.StatusConflict, "sync already running or failed to start")
|
|
return
|
|
}
|
|
|
|
// Return the account with the updated sync_running flag reflected immediately.
|
|
acc.SyncRunning = true
|
|
writeJSON(w, http.StatusOK, acc)
|
|
}
|
|
|
|
// handleUpdateImapInterval updates the automatic sync interval or full credentials
|
|
// of an IMAP account. When name/host/username are present the credentials are updated;
|
|
// otherwise only sync_interval_min is changed.
|
|
func (s *Server) handleUpdateImapInterval(w http.ResponseWriter, r *http.Request) {
|
|
if s.imapStore == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "IMAP not configured")
|
|
return
|
|
}
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
acc, err := s.imapStore.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "account not found")
|
|
return
|
|
}
|
|
|
|
sess := sessionFromCtx(r.Context())
|
|
if acc.Owner != sess.Username && !auth.HasRole(sess.Role, userstore.RoleDomainAdmin) {
|
|
writeError(w, http.StatusForbidden, "access denied")
|
|
return
|
|
}
|
|
if !tenantAccessAllowed(sess, acc.TenantID) {
|
|
writeError(w, http.StatusForbidden, "access denied")
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
SyncIntervalMin int `json:"sync_interval_min"`
|
|
Name string `json:"name"`
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
TLS string `json:"tls"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
// Credential update when name/host/username are provided.
|
|
if req.Name != "" || req.Host != "" || req.Username != "" {
|
|
if req.Name == "" || req.Host == "" || req.Username == "" {
|
|
writeError(w, http.StatusBadRequest, "name, host and username are required for credential update")
|
|
return
|
|
}
|
|
port := req.Port
|
|
if port == 0 {
|
|
port = acc.Port
|
|
}
|
|
tls := req.TLS
|
|
if tls == "" {
|
|
tls = acc.TLS
|
|
}
|
|
updated := imapstore.Account{Name: req.Name, Host: req.Host, Port: port, TLS: tls, Username: req.Username}
|
|
if err := s.imapStore.UpdateCredentials(r.Context(), id, updated, req.Password); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to update credentials")
|
|
return
|
|
}
|
|
acc, err = s.imapStore.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to reload account")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, acc)
|
|
return
|
|
}
|
|
|
|
// Sync interval update.
|
|
// 0 = disabled; otherwise must be between 5 and 1440 minutes.
|
|
if req.SyncIntervalMin != 0 && (req.SyncIntervalMin < 5 || req.SyncIntervalMin > 1440) {
|
|
writeError(w, http.StatusBadRequest, "sync_interval_min must be 0 (disabled) or between 5 and 1440")
|
|
return
|
|
}
|
|
|
|
if err := s.imapStore.UpdateSyncInterval(r.Context(), id, req.SyncIntervalMin); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to update sync interval")
|
|
return
|
|
}
|
|
|
|
acc.SyncIntervalMin = req.SyncIntervalMin
|
|
writeJSON(w, http.StatusOK, acc)
|
|
}
|
|
|
|
// ── POP3 handlers ──────────────────────────────────────────────────────────
|
|
|
|
func (s *Server) handleListPop3(w http.ResponseWriter, r *http.Request) {
|
|
if s.pop3Store == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "POP3 not configured")
|
|
return
|
|
}
|
|
sess := sessionFromCtx(r.Context())
|
|
// SEC-03: Use HasRole to correctly check admin privileges (domain_admin, admin, superadmin).
|
|
isAdmin := auth.HasRole(sess.Role, userstore.RoleDomainAdmin)
|
|
accounts, err := s.pop3Store.List(r.Context(), sess.Username, isAdmin)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to list POP3 accounts")
|
|
return
|
|
}
|
|
if accounts == nil {
|
|
accounts = []pop3store.Account{}
|
|
}
|
|
writeJSON(w, http.StatusOK, accounts)
|
|
}
|
|
|
|
func (s *Server) handleCreatePop3(w http.ResponseWriter, r *http.Request) {
|
|
if s.pop3Store == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "POP3 not configured")
|
|
return
|
|
}
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
TLS string `json:"tls"`
|
|
TLSSkipVerify bool `json:"tls_skip_verify"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
if req.Name == "" || req.Host == "" || req.Username == "" || req.Password == "" {
|
|
writeError(w, http.StatusBadRequest, "name, host, username and password are required")
|
|
return
|
|
}
|
|
if req.Port <= 0 {
|
|
req.Port = 110
|
|
}
|
|
if req.TLS == "" {
|
|
req.TLS = "none"
|
|
}
|
|
|
|
sess := sessionFromCtx(r.Context())
|
|
acc := pop3store.Account{
|
|
Owner: sess.Username,
|
|
Name: req.Name,
|
|
Host: req.Host,
|
|
Port: req.Port,
|
|
TLS: req.TLS,
|
|
TLSSkipVerify: req.TLSSkipVerify,
|
|
Username: req.Username,
|
|
}
|
|
|
|
created, err := s.pop3Store.Create(r.Context(), acc, req.Password)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to create POP3 account")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, created)
|
|
}
|
|
|
|
func (s *Server) handleDeletePop3(w http.ResponseWriter, r *http.Request) {
|
|
if s.pop3Store == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "POP3 not configured")
|
|
return
|
|
}
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
acc, err := s.pop3Store.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "account not found")
|
|
return
|
|
}
|
|
|
|
sess := sessionFromCtx(r.Context())
|
|
if acc.Owner != sess.Username && !auth.HasRole(sess.Role, userstore.RoleDomainAdmin) {
|
|
writeError(w, http.StatusForbidden, "access denied")
|
|
return
|
|
}
|
|
|
|
if err := s.pop3Store.Delete(r.Context(), id); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to delete account")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
|
}
|
|
|
|
func (s *Server) handleTestPop3(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
TLS string `json:"tls"`
|
|
TLSSkipVerify bool `json:"tls_skip_verify"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
if req.Host == "" || req.Username == "" || req.Password == "" {
|
|
writeError(w, http.StatusBadRequest, "host, username and password are required")
|
|
return
|
|
}
|
|
if req.Port <= 0 {
|
|
req.Port = 110
|
|
}
|
|
if req.TLS == "" {
|
|
req.TLS = "none"
|
|
}
|
|
|
|
c, err := pop3store.Dial(req.Host, req.Port, req.TLS, req.TLSSkipVerify)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"ok": false,
|
|
"message": fmt.Sprintf("Verbindung fehlgeschlagen: %v", err),
|
|
})
|
|
return
|
|
}
|
|
defer c.Close()
|
|
|
|
if err := c.Login(req.Username, req.Password); err != nil {
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"ok": false,
|
|
"message": fmt.Sprintf("Anmeldung fehlgeschlagen: %v", err),
|
|
})
|
|
return
|
|
}
|
|
|
|
count, totalSize, err := c.Stat()
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"ok": false,
|
|
"message": fmt.Sprintf("STAT fehlgeschlagen: %v", err),
|
|
})
|
|
return
|
|
}
|
|
|
|
_ = c.Quit()
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"ok": true,
|
|
"message": fmt.Sprintf("Verbindung erfolgreich: %d E-Mails", count),
|
|
"message_count": count,
|
|
"total_size_bytes": totalSize,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleStartPop3Import(w http.ResponseWriter, r *http.Request) {
|
|
if s.pop3Store == nil || s.pop3Importer == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "POP3 not configured")
|
|
return
|
|
}
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
acc, err := s.pop3Store.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "account not found")
|
|
return
|
|
}
|
|
|
|
sess := sessionFromCtx(r.Context())
|
|
if acc.Owner != sess.Username && !auth.HasRole(sess.Role, userstore.RoleDomainAdmin) {
|
|
writeError(w, http.StatusForbidden, "access denied")
|
|
return
|
|
}
|
|
|
|
if acc.Status == "running" {
|
|
writeError(w, http.StatusConflict, "import already running")
|
|
return
|
|
}
|
|
|
|
go s.pop3Importer.Run(context.Background(), id)
|
|
|
|
// Return current account state (status will switch to "running" shortly)
|
|
acc.Status = "running"
|
|
writeJSON(w, http.StatusOK, acc)
|
|
}
|
|
|
|
func (s *Server) handlePop3Progress(w http.ResponseWriter, r *http.Request) {
|
|
if s.pop3Store == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "POP3 not configured")
|
|
return
|
|
}
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
acc, err := s.pop3Store.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "account not found")
|
|
return
|
|
}
|
|
|
|
sess := sessionFromCtx(r.Context())
|
|
if acc.Owner != sess.Username && !auth.HasRole(sess.Role, userstore.RoleDomainAdmin) {
|
|
writeError(w, http.StatusForbidden, "access denied")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, acc)
|
|
}
|