mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 00:16:38 +00:00
fix(backup): handle nested error response and add system owner fallback
SSE progress hook crashed React when backend returned nested error
object from writeError ({"error": {"code": ..., "message": ...}})
instead of flat string. Now parses both formats correctly.
PolicyEngine.IsOwner lacked the "system" fallback that isHTTPOwnerID
already had — when owner_ids is empty, "system" user was rejected from
all backup/restore endpoints. Added consistent fallback logic.
Also added slog.Warn to all silent owner-check rejections across
backup, restore, tenant backup/restore, and S3 handlers.
This commit is contained in:
1 parent
1a471cbde3
commit
1ca49734ff
7 files changed
+18
-1
No files matched your search
@@ -85,6 +85,7 @@ func (h *BackupHandler) handleBackup(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
|
||||
if !h.isOwnerUser(userID) {
|
||||
slog.Warn("security.backup_owner_denied", "user_id", userID)
|
||||
writeError(w, http.StatusForbidden, protocol.ErrUnauthorized,
|
||||
i18n.T(locale, i18n.MsgNoAccess, "system backup"))
|
||||
return
|
||||
|
||||
@@ -52,6 +52,7 @@ func (h *BackupS3Handler) handleGetConfig(w http.ResponseWriter, r *http.Request
|
||||
locale := extractLocale(r)
|
||||
|
||||
if !h.isOwnerUser(userID) {
|
||||
slog.Warn("security.s3_owner_denied", "user_id", userID, "path", r.URL.Path)
|
||||
writeError(w, http.StatusForbidden, protocol.ErrUnauthorized,
|
||||
i18n.T(locale, i18n.MsgNoAccess, "s3 config"))
|
||||
return
|
||||
@@ -83,6 +84,7 @@ func (h *BackupS3Handler) handleSaveConfig(w http.ResponseWriter, r *http.Reques
|
||||
locale := extractLocale(r)
|
||||
|
||||
if !h.isOwnerUser(userID) {
|
||||
slog.Warn("security.s3_owner_denied", "user_id", userID, "path", r.URL.Path)
|
||||
writeError(w, http.StatusForbidden, protocol.ErrUnauthorized,
|
||||
i18n.T(locale, i18n.MsgNoAccess, "s3 config"))
|
||||
return
|
||||
@@ -136,6 +138,7 @@ func (h *BackupS3Handler) handleList(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
|
||||
if !h.isOwnerUser(userID) {
|
||||
slog.Warn("security.s3_owner_denied", "user_id", userID, "path", r.URL.Path)
|
||||
writeError(w, http.StatusForbidden, protocol.ErrUnauthorized,
|
||||
i18n.T(locale, i18n.MsgNoAccess, "s3 list"))
|
||||
return
|
||||
@@ -167,6 +170,7 @@ func (h *BackupS3Handler) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
|
||||
if !h.isOwnerUser(userID) {
|
||||
slog.Warn("security.s3_owner_denied", "user_id", userID, "path", r.URL.Path)
|
||||
writeError(w, http.StatusForbidden, protocol.ErrUnauthorized,
|
||||
i18n.T(locale, i18n.MsgNoAccess, "s3 upload"))
|
||||
return
|
||||
@@ -234,6 +238,7 @@ func (h *BackupS3Handler) handleBackupAndUpload(w http.ResponseWriter, r *http.R
|
||||
locale := extractLocale(r)
|
||||
|
||||
if !h.isOwnerUser(userID) {
|
||||
slog.Warn("security.s3_owner_denied", "user_id", userID, "path", r.URL.Path)
|
||||
writeError(w, http.StatusForbidden, protocol.ErrUnauthorized,
|
||||
i18n.T(locale, i18n.MsgNoAccess, "s3 backup"))
|
||||
return
|
||||
|
||||
@@ -44,6 +44,7 @@ func (h *RestoreHandler) handleRestore(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
|
||||
if !h.isOwnerUser(userID) {
|
||||
slog.Warn("security.restore_owner_denied", "user_id", userID)
|
||||
writeError(w, http.StatusForbidden, protocol.ErrUnauthorized,
|
||||
i18n.T(locale, i18n.MsgNoAccess, "system restore"))
|
||||
return
|
||||
|
||||
@@ -73,6 +73,7 @@ func (h *TenantBackupHandler) handleBackup(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
if !h.authorised(r, userID, tenantID) {
|
||||
slog.Warn("security.tenant_backup_denied", "user_id", userID, "tenant_id", tenantID)
|
||||
writeError(w, http.StatusForbidden, protocol.ErrUnauthorized,
|
||||
i18n.T(locale, i18n.MsgNoAccess, "tenant backup"))
|
||||
return
|
||||
|
||||
@@ -20,6 +20,7 @@ func (h *TenantBackupHandler) handleRestore(w http.ResponseWriter, r *http.Reque
|
||||
locale := extractLocale(r)
|
||||
|
||||
if !h.isOwnerUser(userID) {
|
||||
slog.Warn("security.tenant_restore_owner_denied", "user_id", userID)
|
||||
writeError(w, http.StatusForbidden, protocol.ErrUnauthorized,
|
||||
i18n.T(locale, i18n.MsgNoAccess, "tenant restore"))
|
||||
return
|
||||
|
||||
@@ -74,9 +74,16 @@ func NewPolicyEngine(ownerIDs []string) *PolicyEngine {
|
||||
}
|
||||
|
||||
// IsOwner checks if a sender ID is an owner.
|
||||
// When no owner IDs are configured, "system" is treated as owner (fail-closed default).
|
||||
func (pe *PolicyEngine) IsOwner(senderID string) bool {
|
||||
if senderID == "" {
|
||||
return false
|
||||
}
|
||||
pe.mu.RLock()
|
||||
defer pe.mu.RUnlock()
|
||||
if len(pe.ownerIDs) == 0 {
|
||||
return senderID == "system"
|
||||
}
|
||||
return pe.ownerIDs[senderID]
|
||||
}
|
||||
|
||||
|
||||
@@ -166,7 +166,8 @@ export function useSseProgress(authHeaders: () => Record<string, string>): UseSs
|
||||
const res = await fetch(url, { ...init, signal: controller.signal });
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: res.statusText }));
|
||||
setError({ phase: "request", detail: err.error ?? res.statusText, rolled_back: false });
|
||||
const detail = typeof err.error === "string" ? err.error : err.error?.message ?? res.statusText;
|
||||
setError({ phase: "request", detail, rolled_back: false });
|
||||
setStatus("error");
|
||||
cleanup();
|
||||
return;
|
||||
|
||||
Reference in new issue
Block a user