Add s3tables shell and admin UI (#8172)
* Add shared s3tables manager * Add s3tables shell commands * Add s3tables admin API * Add s3tables admin UI * Fix admin s3tables namespace create * Rename table buckets menu * Centralize s3tables tag validation * Reuse s3tables manager in admin * Extract s3tables list limit * Add s3tables bucket ARN helper * Remove write middleware from s3tables APIs * Fix bucket link and policy hint * Fix table tag parsing and nav link * Disable namespace table link on invalid ARN * Improve s3tables error decode * Return flag parse errors for s3tables tag * Accept query params for namespace create * Bind namespace create form data * Read s3tables JS data from DOM * s3tables: allow empty region ARN * shell: pass s3tables account id * shell: require account for table buckets * shell: use bucket name for namespaces * shell: use bucket name for tables * shell: use bucket name for tags * admin: add table buckets links in file browser * s3api: reuse s3tables tag validation * admin: harden s3tables UI handlers * fix admin list table buckets * allow admin s3tables access * validate s3tables bucket tags * log s3tables bucket metadata errors * rollback table bucket on owner failure * show s3tables bucket owner * add s3tables iam conditions * Add s3tables user permissions UI * Authorize s3tables using identity actions * Add s3tables permissions to user modal * Disambiguate bucket scope in user permissions * Block table bucket names that match S3 buckets * Pretty-print IAM identity JSON * Include tags in s3tables permission context * admin: refactor S3 Tables inline JavaScript into a separate file * s3tables: extend IAM policy condition operators support * shell: use LookupEntry wrapper for s3tables bucket conflict check * admin: handle buildBucketPermissions validation in create/update flows
This commit is contained in:
@@ -220,6 +220,30 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
<option value="GetBucketObjectLockConfiguration">Get Bucket Object Lock Configuration</option>
|
||||
<option value="PutBucketObjectLockConfiguration">Put Bucket Object Lock Configuration</option>
|
||||
</optgroup>
|
||||
<optgroup label="S3 Tables Permissions">
|
||||
<option value="S3TablesAdmin">S3 Tables Admin (Full Access)</option>
|
||||
<option value="CreateTableBucket">Create Table Bucket</option>
|
||||
<option value="GetTableBucket">Get Table Bucket</option>
|
||||
<option value="ListTableBuckets">List Table Buckets</option>
|
||||
<option value="DeleteTableBucket">Delete Table Bucket</option>
|
||||
<option value="PutTableBucketPolicy">Put Table Bucket Policy</option>
|
||||
<option value="GetTableBucketPolicy">Get Table Bucket Policy</option>
|
||||
<option value="DeleteTableBucketPolicy">Delete Table Bucket Policy</option>
|
||||
<option value="CreateNamespace">Create Namespace</option>
|
||||
<option value="GetNamespace">Get Namespace</option>
|
||||
<option value="ListNamespaces">List Namespaces</option>
|
||||
<option value="DeleteNamespace">Delete Namespace</option>
|
||||
<option value="CreateTable">Create Table</option>
|
||||
<option value="GetTable">Get Table</option>
|
||||
<option value="ListTables">List Tables</option>
|
||||
<option value="DeleteTable">Delete Table</option>
|
||||
<option value="PutTablePolicy">Put Table Policy</option>
|
||||
<option value="GetTablePolicy">Get Table Policy</option>
|
||||
<option value="DeleteTablePolicy">Delete Table Policy</option>
|
||||
<option value="TagResource">Tag Resource</option>
|
||||
<option value="ListTagsForResource">List Tags</option>
|
||||
<option value="UntagResource">Untag Resource</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<small class="form-text text-muted">Hold Ctrl/Cmd to select multiple permissions</small>
|
||||
</div>
|
||||
@@ -304,6 +328,30 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
<option value="GetBucketObjectLockConfiguration">Get Bucket Object Lock Configuration</option>
|
||||
<option value="PutBucketObjectLockConfiguration">Put Bucket Object Lock Configuration</option>
|
||||
</optgroup>
|
||||
<optgroup label="S3 Tables Permissions">
|
||||
<option value="S3TablesAdmin">S3 Tables Admin (Full Access)</option>
|
||||
<option value="CreateTableBucket">Create Table Bucket</option>
|
||||
<option value="GetTableBucket">Get Table Bucket</option>
|
||||
<option value="ListTableBuckets">List Table Buckets</option>
|
||||
<option value="DeleteTableBucket">Delete Table Bucket</option>
|
||||
<option value="PutTableBucketPolicy">Put Table Bucket Policy</option>
|
||||
<option value="GetTableBucketPolicy">Get Table Bucket Policy</option>
|
||||
<option value="DeleteTableBucketPolicy">Delete Table Bucket Policy</option>
|
||||
<option value="CreateNamespace">Create Namespace</option>
|
||||
<option value="GetNamespace">Get Namespace</option>
|
||||
<option value="ListNamespaces">List Namespaces</option>
|
||||
<option value="DeleteNamespace">Delete Namespace</option>
|
||||
<option value="CreateTable">Create Table</option>
|
||||
<option value="GetTable">Get Table</option>
|
||||
<option value="ListTables">List Tables</option>
|
||||
<option value="DeleteTable">Delete Table</option>
|
||||
<option value="PutTablePolicy">Put Table Policy</option>
|
||||
<option value="GetTablePolicy">Get Table Policy</option>
|
||||
<option value="DeleteTablePolicy">Delete Table Policy</option>
|
||||
<option value="TagResource">Tag Resource</option>
|
||||
<option value="ListTagsForResource">List Tags</option>
|
||||
<option value="UntagResource">Untag Resource</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
@@ -457,6 +505,32 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
// Global variable to store available buckets
|
||||
var availableBuckets = [];
|
||||
var bucketPermissionCounter = 0;
|
||||
const s3TablesPermissions = new Set([
|
||||
'CreateTableBucket',
|
||||
'GetTableBucket',
|
||||
'ListTableBuckets',
|
||||
'DeleteTableBucket',
|
||||
'PutTableBucketPolicy',
|
||||
'GetTableBucketPolicy',
|
||||
'DeleteTableBucketPolicy',
|
||||
'CreateNamespace',
|
||||
'GetNamespace',
|
||||
'ListNamespaces',
|
||||
'DeleteNamespace',
|
||||
'CreateTable',
|
||||
'GetTable',
|
||||
'ListTables',
|
||||
'DeleteTable',
|
||||
'PutTablePolicy',
|
||||
'GetTablePolicy',
|
||||
'DeleteTablePolicy',
|
||||
'TagResource',
|
||||
'ListTagsForResource',
|
||||
'UntagResource'
|
||||
]);
|
||||
function isS3TablesPermission(permission) {
|
||||
return permission === 'S3TablesAdmin' || s3TablesPermissions.has(permission);
|
||||
}
|
||||
|
||||
// Load buckets
|
||||
async function loadBuckets() {
|
||||
@@ -464,10 +538,8 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
const response = await fetch('/api/s3/buckets');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
availableBuckets = data.buckets || [];
|
||||
availableBuckets = (data.buckets || []).map(bucket => ({ name: bucket.name, type: 's3' }));
|
||||
console.log('Loaded', availableBuckets.length, 'buckets');
|
||||
// Populate bucket selection dropdowns
|
||||
populateBucketSelections();
|
||||
} else {
|
||||
console.warn('Failed to load buckets');
|
||||
availableBuckets = [];
|
||||
@@ -476,6 +548,20 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
console.error('Error loading buckets:', error);
|
||||
availableBuckets = [];
|
||||
}
|
||||
try {
|
||||
const response = await fetch('/api/s3tables/buckets');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const tableBuckets = (data.buckets || data.tableBuckets || []).map(bucket => ({ name: bucket.name, type: 's3tables' }));
|
||||
availableBuckets = availableBuckets.concat(tableBuckets);
|
||||
} else {
|
||||
console.warn('Failed to load table buckets');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error loading table buckets:', error);
|
||||
}
|
||||
// Populate bucket selection dropdowns
|
||||
populateBucketSelections();
|
||||
}
|
||||
|
||||
// Load policies
|
||||
@@ -556,8 +642,8 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
select.innerHTML = '';
|
||||
availableBuckets.forEach(bucket => {
|
||||
const option = document.createElement('option');
|
||||
option.value = bucket.name;
|
||||
option.textContent = bucket.name;
|
||||
option.value = bucket.type + ':' + bucket.name;
|
||||
option.textContent = bucket.type === 's3tables' ? `Table: ${bucket.name}` : bucket.name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
@@ -584,11 +670,25 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
const globalBucketPerms = [];
|
||||
|
||||
actions.forEach(action => {
|
||||
if (action.includes(':')) {
|
||||
if (action.startsWith('s3tables:')) {
|
||||
const actionValue = action.slice('s3tables:'.length);
|
||||
if (actionValue === '*') {
|
||||
globalBucketPerms.push('S3TablesAdmin');
|
||||
return;
|
||||
}
|
||||
const parts = actionValue.split(':');
|
||||
const perm = parts[0];
|
||||
const bucket = parts.length > 1 ? parts.slice(1).join(':').replace(/\/\*$/, '') : '';
|
||||
if (bucket) {
|
||||
bucketActions.push({ permission: perm, bucketId: 's3tables:' + bucket });
|
||||
} else {
|
||||
globalBucketPerms.push(perm);
|
||||
}
|
||||
} else if (action.includes(':')) {
|
||||
const parts = action.split(':');
|
||||
const perm = parts[0];
|
||||
const bucket = parts.slice(1).join(':').replace(/\/\*$/, '');
|
||||
bucketActions.push({ permission: perm, bucket: bucket });
|
||||
bucketActions.push({ permission: perm, bucketId: 's3:' + bucket });
|
||||
} else {
|
||||
globalBucketPerms.push(action);
|
||||
}
|
||||
@@ -601,7 +701,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
} else if (bucketActions.length > 0) {
|
||||
// Get unique permissions and buckets
|
||||
const perms = [...new Set(bucketActions.map(ba => ba.permission))];
|
||||
const buckets = [...new Set(bucketActions.map(ba => ba.bucket))];
|
||||
const buckets = [...new Set(bucketActions.map(ba => ba.bucketId))];
|
||||
|
||||
result.permissions = perms;
|
||||
result.applyToAll = false;
|
||||
@@ -611,6 +711,16 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseBucketOptionValue(value) {
|
||||
if (value.startsWith('s3tables:')) {
|
||||
return { type: 's3tables', name: value.slice('s3tables:'.length) };
|
||||
}
|
||||
if (value.startsWith('s3:')) {
|
||||
return { type: 's3', name: value.slice('s3:'.length) };
|
||||
}
|
||||
return { type: 's3', name: value };
|
||||
}
|
||||
|
||||
// Build bucket permission action strings using original permissions dropdown
|
||||
/**
|
||||
* Builds bucket permission strings based on selected permissions and bucket scope.
|
||||
@@ -627,10 +737,8 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
// Get selected permissions from the original multi-select
|
||||
const selectedPerms = Array.from(permSelect.selectedOptions).map(opt => opt.value);
|
||||
|
||||
// If Admin is selected, return just Admin (it overrides everything)
|
||||
if (selectedPerms.includes('Admin')) {
|
||||
return ['Admin'];
|
||||
}
|
||||
const hasAdmin = selectedPerms.includes('Admin');
|
||||
const hasS3TablesAdmin = selectedPerms.includes('S3TablesAdmin');
|
||||
|
||||
if (selectedPerms.length === 0) {
|
||||
return [];
|
||||
@@ -663,13 +771,30 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
|
||||
if (applyToAll) {
|
||||
// Return global permissions (no bucket specification)
|
||||
return selectedPerms;
|
||||
const actions = [];
|
||||
if (hasAdmin) {
|
||||
actions.push('Admin');
|
||||
}
|
||||
if (hasS3TablesAdmin) {
|
||||
actions.push('s3tables:*');
|
||||
}
|
||||
selectedPerms.forEach(perm => {
|
||||
if (perm === 'Admin' || perm === 'S3TablesAdmin') {
|
||||
return;
|
||||
}
|
||||
if (isS3TablesPermission(perm)) {
|
||||
actions.push('s3tables:' + perm);
|
||||
} else {
|
||||
actions.push(perm);
|
||||
}
|
||||
});
|
||||
return actions;
|
||||
} else {
|
||||
// Get selected specific buckets
|
||||
const bucketSelect = document.getElementById(mode === 'edit' ? 'editSelectedBuckets' : 'selectedBuckets');
|
||||
if (!bucketSelect) return null;
|
||||
|
||||
const selectedBuckets = Array.from(bucketSelect.selectedOptions).map(opt => opt.value);
|
||||
const selectedBuckets = [...new Set(Array.from(bucketSelect.selectedOptions).map(opt => opt.value))];
|
||||
|
||||
// Return null to signal validation failure if no buckets selected
|
||||
if (selectedBuckets.length === 0) {
|
||||
@@ -678,13 +803,29 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
|
||||
// Build bucket-scoped permissions
|
||||
const actions = [];
|
||||
if (hasAdmin) {
|
||||
actions.push('Admin');
|
||||
}
|
||||
if (hasS3TablesAdmin) {
|
||||
actions.push('s3tables:*');
|
||||
}
|
||||
selectedPerms.forEach(perm => {
|
||||
if (perm === 'Admin' || perm === 'S3TablesAdmin') {
|
||||
return;
|
||||
}
|
||||
selectedBuckets.forEach(bucket => {
|
||||
actions.push(perm + ':' + bucket);
|
||||
const bucketInfo = parseBucketOptionValue(bucket);
|
||||
if (isS3TablesPermission(perm)) {
|
||||
if (bucketInfo.type === 's3tables') {
|
||||
actions.push('s3tables:' + perm + ':' + bucketInfo.name);
|
||||
}
|
||||
} else if (bucketInfo.type === 's3') {
|
||||
actions.push(perm + ':' + bucketInfo.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return actions;
|
||||
return [...new Set(actions)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -834,6 +975,16 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
// Get permissions with bucket scope applied
|
||||
const allActions = buildBucketPermissions('create');
|
||||
|
||||
if (allActions === null) {
|
||||
showAlert('Please select at least one bucket when using specific bucket permissions', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!allActions || allActions.length === 0) {
|
||||
showAlert('At least one permission must be selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const userData = {
|
||||
username: formData.get('username'),
|
||||
email: formData.get('email'),
|
||||
@@ -887,15 +1038,15 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
// Get permissions with bucket scope applied
|
||||
const allActions = buildBucketPermissions('edit');
|
||||
|
||||
// Validate that permissions are not empty
|
||||
if (!allActions || allActions.length === 0) {
|
||||
showAlert('At least one permission must be selected', 'error');
|
||||
// Check for null (validation failure from buildBucketPermissions)
|
||||
if (allActions === null) {
|
||||
showAlert('Please select at least one bucket when using specific bucket permissions', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for null (validation failure from buildBucketPermissionsNew)
|
||||
if (allActions === null) {
|
||||
showAlert('Please select at least one bucket when using specific bucket permissions', 'error');
|
||||
// Validate that permissions are not empty
|
||||
if (!allActions || allActions.length === 0) {
|
||||
showAlert('At least one permission must be selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1154,4 +1305,4 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
}
|
||||
|
||||
// Helper functions for template
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user