mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-06-13 23:36:45 +03:00
fix(admin): S3 Tables CSRF token + non-empty 409 status (#9221)
* fix(admin): attach CSRF token to S3 Tables write requests Several POST/PUT/DELETE calls in s3tables.js were sent without an X-CSRF-Token header while the corresponding handlers in weed/admin/dash/s3tables_management.go enforce CSRF via requireSessionCSRFToken, so authenticated users hit "invalid CSRF token" on actions like creating a table bucket (#9220), updating policies, and managing tags. Add an s3tWriteHeaders helper that pulls the token from the existing csrf-token meta tag and use it on every write to /api/s3tables/buckets, /bucket-policy, /tables, /table-policy, and /tags. The Iceberg-page write paths already attached the token and are unchanged. Fixes #9220 * fix(admin): map BucketNotEmpty/NamespaceNotEmpty to 409 for S3 Tables DELETE on a non-empty table bucket or namespace returned HTTP 500 because s3TablesErrorStatus didn't list ErrCodeBucketNotEmpty or ErrCodeNamespaceNotEmpty in its conflict case, even though the backend handler emits them with 409 Conflict (matching AWS S3 Tables). Add both codes to the existing conflict mapping. * refactor(admin): route Iceberg S3 Tables writes through s3tWriteHeaders Iceberg namespace/table create and Iceberg table delete were still hand-rolling CSRF headers. Replace those blocks with the existing s3tWriteHeaders() helper so every S3 Tables write uses the same code path. Drop the now-unused csrfTokenInput.value population in initIcebergNamespaces and initIcebergTables (the templ hidden inputs have no server-rendered value, and nothing reads the input now that the JS reads the token from the meta tag via getCSRFToken()).
This commit is contained in:
@@ -1061,7 +1061,7 @@ func s3TablesErrorStatus(err error) int {
|
||||
return http.StatusNotFound
|
||||
case s3tables.ErrCodeAccessDenied:
|
||||
return http.StatusForbidden
|
||||
case s3tables.ErrCodeBucketAlreadyExists, s3tables.ErrCodeNamespaceAlreadyExists, s3tables.ErrCodeTableAlreadyExists, s3tables.ErrCodeConflict:
|
||||
case s3tables.ErrCodeBucketAlreadyExists, s3tables.ErrCodeNamespaceAlreadyExists, s3tables.ErrCodeTableAlreadyExists, s3tables.ErrCodeBucketNotEmpty, s3tables.ErrCodeNamespaceNotEmpty, s3tables.ErrCodeConflict:
|
||||
return http.StatusConflict
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,15 @@ function getCSRFToken() {
|
||||
return tokenMeta.getAttribute('content') || '';
|
||||
}
|
||||
|
||||
function s3tWriteHeaders(extra) {
|
||||
const headers = Object.assign({}, extra || {});
|
||||
const csrfToken = getCSRFToken();
|
||||
if (csrfToken) {
|
||||
headers['X-CSRF-Token'] = csrfToken;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize S3 Tables Buckets Page
|
||||
*/
|
||||
@@ -104,7 +113,7 @@ function initS3TablesBuckets() {
|
||||
try {
|
||||
const response = await fetch(s3tBasePath('/api/s3tables/buckets'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: s3tWriteHeaders({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await response.json();
|
||||
@@ -133,7 +142,7 @@ function initS3TablesBuckets() {
|
||||
try {
|
||||
const response = await fetch(s3tBasePath('/api/s3tables/bucket-policy'), {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: s3tWriteHeaders({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({ bucket_arn: bucketArn, policy: policy })
|
||||
});
|
||||
const data = await response.json();
|
||||
@@ -239,7 +248,7 @@ function initS3TablesTables() {
|
||||
try {
|
||||
const response = await fetch(s3tBasePath('/api/s3tables/tables'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: s3tWriteHeaders({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await response.json();
|
||||
@@ -267,7 +276,7 @@ function initS3TablesTables() {
|
||||
try {
|
||||
const response = await fetch(s3tBasePath('/api/s3tables/table-policy'), {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: s3tWriteHeaders({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({ bucket_arn: dataBucketArn, namespace: dataNamespace, name: document.getElementById('s3tablesTablePolicyName').value, policy: policy })
|
||||
});
|
||||
const data = await response.json();
|
||||
@@ -306,10 +315,6 @@ function initIcebergNamespaces() {
|
||||
if (!container) return;
|
||||
const bucketArn = container.dataset.bucketArn || '';
|
||||
const catalogName = container.dataset.catalogName || '';
|
||||
const csrfTokenInput = document.getElementById('icebergNamespaceCsrfToken');
|
||||
if (csrfTokenInput) {
|
||||
csrfTokenInput.value = getCSRFToken();
|
||||
}
|
||||
|
||||
const namespaceInput = document.getElementById('icebergNamespaceName');
|
||||
if (namespaceInput) {
|
||||
@@ -330,14 +335,9 @@ function initIcebergNamespaces() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const csrfToken = csrfTokenInput ? csrfTokenInput.value : getCSRFToken();
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (csrfToken) {
|
||||
headers['X-CSRF-Token'] = csrfToken;
|
||||
}
|
||||
const response = await fetch(s3tBasePath('/api/s3tables/namespaces'), {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
headers: s3tWriteHeaders({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({ bucket_arn: bucketArn, name: name })
|
||||
});
|
||||
const data = await response.json();
|
||||
@@ -433,10 +433,6 @@ function initIcebergTables() {
|
||||
if (!container) return;
|
||||
const bucketArn = container.dataset.bucketArn || '';
|
||||
const namespace = container.dataset.namespace || '';
|
||||
const csrfTokenInput = document.getElementById('icebergTableCsrfToken');
|
||||
if (csrfTokenInput) {
|
||||
csrfTokenInput.value = getCSRFToken();
|
||||
}
|
||||
|
||||
initIcebergDeleteModal();
|
||||
|
||||
@@ -476,14 +472,9 @@ function initIcebergTables() {
|
||||
payload.metadata = metadata;
|
||||
}
|
||||
try {
|
||||
const csrfToken = csrfTokenInput ? csrfTokenInput.value : getCSRFToken();
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (csrfToken) {
|
||||
headers['X-CSRF-Token'] = csrfToken;
|
||||
}
|
||||
const response = await fetch(s3tBasePath('/api/s3tables/tables'), {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
headers: s3tWriteHeaders({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await response.json();
|
||||
@@ -530,7 +521,7 @@ async function deleteS3TablesBucket() {
|
||||
const bucketArn = document.getElementById('deleteS3TablesBucketModal').dataset.bucketArn;
|
||||
if (!bucketArn) return;
|
||||
try {
|
||||
const response = await fetch(s3tBasePath(`/api/s3tables/buckets?bucket=${encodeURIComponent(bucketArn)}`), { method: 'DELETE' });
|
||||
const response = await fetch(s3tBasePath(`/api/s3tables/buckets?bucket=${encodeURIComponent(bucketArn)}`), { method: 'DELETE', headers: s3tWriteHeaders() });
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
alert(data.error || 'Failed to delete bucket');
|
||||
@@ -561,7 +552,7 @@ async function deleteS3TablesBucketPolicy() {
|
||||
const bucketArn = document.getElementById('s3tablesBucketPolicyArn').value;
|
||||
if (!bucketArn) return;
|
||||
try {
|
||||
const response = await fetch(s3tBasePath(`/api/s3tables/bucket-policy?bucket=${encodeURIComponent(bucketArn)}`), { method: 'DELETE' });
|
||||
const response = await fetch(s3tBasePath(`/api/s3tables/bucket-policy?bucket=${encodeURIComponent(bucketArn)}`), { method: 'DELETE', headers: s3tWriteHeaders() });
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
alert(data.error || 'Failed to delete policy');
|
||||
@@ -590,7 +581,7 @@ async function deleteS3TablesTable() {
|
||||
query.set('version', versionToken);
|
||||
}
|
||||
try {
|
||||
const response = await fetch(s3tBasePath(`/api/s3tables/tables?${query.toString()}`), { method: 'DELETE' });
|
||||
const response = await fetch(s3tBasePath(`/api/s3tables/tables?${query.toString()}`), { method: 'DELETE', headers: s3tWriteHeaders() });
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
alert(data.error || 'Failed to delete table');
|
||||
@@ -621,12 +612,7 @@ async function deleteIcebergTable() {
|
||||
query.set('version', versionToken);
|
||||
}
|
||||
try {
|
||||
const csrfToken = getCSRFToken();
|
||||
const requestOptions = { method: 'DELETE' };
|
||||
if (csrfToken) {
|
||||
requestOptions.headers = { 'X-CSRF-Token': csrfToken };
|
||||
}
|
||||
const response = await fetch(s3tBasePath(`/api/s3tables/tables?${query.toString()}`), requestOptions);
|
||||
const response = await fetch(s3tBasePath(`/api/s3tables/tables?${query.toString()}`), { method: 'DELETE', headers: s3tWriteHeaders() });
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
alert(data.error || 'Failed to drop table');
|
||||
@@ -665,7 +651,7 @@ async function deleteS3TablesTablePolicy() {
|
||||
const dataNamespace = dataContainer.dataset.namespace || '';
|
||||
const query = new URLSearchParams({ bucket: dataBucketArn, namespace: dataNamespace, name: document.getElementById('s3tablesTablePolicyName').value });
|
||||
try {
|
||||
const response = await fetch(s3tBasePath(`/api/s3tables/table-policy?${query.toString()}`), { method: 'DELETE' });
|
||||
const response = await fetch(s3tBasePath(`/api/s3tables/table-policy?${query.toString()}`), { method: 'DELETE', headers: s3tWriteHeaders() });
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
alert(data.error || 'Failed to delete policy');
|
||||
@@ -872,7 +858,7 @@ async function updateS3TablesTags(resourceArn, tags) {
|
||||
try {
|
||||
const response = await fetch(s3tBasePath('/api/s3tables/tags'), {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: s3tWriteHeaders({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({ resource_arn: resourceArn, tags: tags })
|
||||
});
|
||||
const data = await response.json();
|
||||
@@ -899,7 +885,7 @@ async function deleteS3TablesTags() {
|
||||
try {
|
||||
const response = await fetch(s3tBasePath('/api/s3tables/tags'), {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: s3tWriteHeaders({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({ resource_arn: resourceArn, tag_keys: tagKeys })
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
Reference in New Issue
Block a user