- Rename from @lossless.zone/s3container to @lossless.zone/objectstorage - Replace @push.rocks/smarts3 with @push.rocks/smartstorage - Change env var prefix from S3_ to OBJST_ - Rename S3Container class to ObjectStorageContainer - Update web component prefix from s3c- to objst- - Update UI labels, CLI flags, documentation, and Docker config
395 lines
14 KiB
TypeScript
395 lines
14 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
import * as appstate from '../appstate.js';
|
|
import * as shared from './shared/index.js';
|
|
import * as interfaces from '../../ts_interfaces/index.js';
|
|
|
|
import {
|
|
DeesElement,
|
|
customElement,
|
|
html,
|
|
state,
|
|
css,
|
|
cssManager,
|
|
type TemplateResult,
|
|
} from '@design.estate/dees-element';
|
|
import { DeesModal } from '@design.estate/dees-catalog';
|
|
|
|
@customElement('objst-view-policies')
|
|
export class ObjstViewPolicies extends DeesElement {
|
|
@state()
|
|
accessor policiesState: appstate.IPoliciesState = { policies: [] };
|
|
|
|
constructor() {
|
|
super();
|
|
const sub = appstate.policiesStatePart
|
|
.select((s) => s)
|
|
.subscribe((policiesState) => {
|
|
this.policiesState = policiesState;
|
|
});
|
|
this.rxSubscriptions.push(sub);
|
|
}
|
|
|
|
async connectedCallback() {
|
|
super.connectedCallback();
|
|
appstate.policiesStatePart.dispatchAction(appstate.fetchPoliciesAction, null);
|
|
}
|
|
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
shared.viewHostCss,
|
|
];
|
|
|
|
public render(): TemplateResult {
|
|
return html`
|
|
<objst-sectionheading>Policies</objst-sectionheading>
|
|
<dees-table
|
|
heading1="Named Policies"
|
|
heading2="Create reusable policies and attach them to buckets"
|
|
.data=${this.policiesState.policies}
|
|
.dataName=${'policy'}
|
|
.displayFunction=${(item: any) => ({
|
|
Name: item.name,
|
|
Description: item.description,
|
|
Statements: String(item.statements.length),
|
|
Updated: new Date(item.updatedAt).toLocaleDateString(),
|
|
})}
|
|
.dataActions=${[
|
|
{
|
|
name: 'Create Policy',
|
|
iconName: 'lucide:plus',
|
|
type: ['header'] as any[],
|
|
actionFunc: async () => {
|
|
await this.showCreatePolicyModal();
|
|
},
|
|
},
|
|
{
|
|
name: 'Edit',
|
|
iconName: 'lucide:pencil',
|
|
type: ['inRow'] as any[],
|
|
actionFunc: async (args: any) => {
|
|
await this.showEditPolicyModal(args.item);
|
|
},
|
|
},
|
|
{
|
|
name: 'Buckets',
|
|
iconName: 'lucide:database',
|
|
type: ['inRow'] as any[],
|
|
actionFunc: async (args: any) => {
|
|
await this.showPolicyBucketsModal(args.item);
|
|
},
|
|
},
|
|
{
|
|
name: 'Delete',
|
|
iconName: 'lucide:trash2',
|
|
type: ['inRow', 'contextmenu'] as any[],
|
|
actionFunc: async (args: any) => {
|
|
await this.showDeletePolicyModal(args.item);
|
|
},
|
|
},
|
|
]}
|
|
></dees-table>
|
|
`;
|
|
}
|
|
|
|
private getDefaultStatements(): interfaces.data.IObjstStatement[] {
|
|
return [
|
|
{
|
|
Sid: 'PublicRead',
|
|
Effect: 'Allow',
|
|
Principal: '*',
|
|
Action: 's3:GetObject',
|
|
Resource: 'arn:aws:s3:::${bucket}/*',
|
|
},
|
|
];
|
|
}
|
|
|
|
private async showCreatePolicyModal(): Promise<void> {
|
|
const defaultJson = JSON.stringify(this.getDefaultStatements(), null, 2);
|
|
await DeesModal.createAndShow({
|
|
heading: 'Create Policy',
|
|
content: html`
|
|
<style>
|
|
.policy-form { display: flex; flex-direction: column; gap: 12px; }
|
|
.policy-form label { font-size: 13px; font-weight: 600; color: ${cssManager.bdTheme('#333', '#ccc')}; }
|
|
.policy-form input, .policy-form textarea {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#444')};
|
|
border-radius: 6px;
|
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
|
color: ${cssManager.bdTheme('#333', '#eee')};
|
|
font-size: 14px;
|
|
box-sizing: border-box;
|
|
}
|
|
.policy-form textarea {
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
min-height: 200px;
|
|
resize: vertical;
|
|
}
|
|
.policy-form input:focus, .policy-form textarea:focus {
|
|
outline: none;
|
|
border-color: ${cssManager.bdTheme('#2196f3', '#64b5f6')};
|
|
}
|
|
.policy-error { color: #ef5350; font-size: 13px; min-height: 20px; }
|
|
.policy-hint { color: ${cssManager.bdTheme('#666', '#999')}; font-size: 12px; }
|
|
</style>
|
|
<div class="policy-form">
|
|
<div>
|
|
<label>Name</label>
|
|
<input type="text" class="policy-name" placeholder="e.g. Public Read Access" />
|
|
</div>
|
|
<div>
|
|
<label>Description</label>
|
|
<input type="text" class="policy-description" placeholder="e.g. Allow anonymous read access to objects" />
|
|
</div>
|
|
<div>
|
|
<label>Statements (JSON array)</label>
|
|
<p class="policy-hint">Use \${bucket} in Resource ARNs — it will be replaced with the actual bucket name when applied.</p>
|
|
<textarea class="policy-statements">${defaultJson}</textarea>
|
|
</div>
|
|
<div class="policy-error"></div>
|
|
</div>
|
|
`,
|
|
menuOptions: [
|
|
{ name: 'Cancel', action: async (modal: any) => { modal.destroy(); } },
|
|
{
|
|
name: 'Create',
|
|
action: async (modal: any) => {
|
|
const root = modal.shadowRoot;
|
|
const name = (root.querySelector('.policy-name') as HTMLInputElement)?.value?.trim();
|
|
const description = (root.querySelector('.policy-description') as HTMLInputElement)?.value?.trim();
|
|
const statementsText = (root.querySelector('.policy-statements') as HTMLTextAreaElement)?.value?.trim();
|
|
const errorDiv = root.querySelector('.policy-error') as HTMLElement;
|
|
|
|
if (!name) { errorDiv.textContent = 'Name is required.'; return; }
|
|
if (!statementsText) { errorDiv.textContent = 'Statements are required.'; return; }
|
|
|
|
let statements: interfaces.data.IObjstStatement[];
|
|
try {
|
|
statements = JSON.parse(statementsText);
|
|
if (!Array.isArray(statements)) throw new Error('Must be an array');
|
|
} catch (err: any) {
|
|
errorDiv.textContent = `Invalid JSON: ${err.message}`;
|
|
return;
|
|
}
|
|
|
|
await appstate.policiesStatePart.dispatchAction(appstate.createPolicyAction, {
|
|
name,
|
|
description: description || '',
|
|
statements,
|
|
});
|
|
modal.destroy();
|
|
},
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
private async showEditPolicyModal(policy: interfaces.data.INamedPolicy): Promise<void> {
|
|
const statementsJson = JSON.stringify(policy.statements, null, 2);
|
|
await DeesModal.createAndShow({
|
|
heading: `Edit Policy: ${policy.name}`,
|
|
content: html`
|
|
<style>
|
|
.policy-form { display: flex; flex-direction: column; gap: 12px; }
|
|
.policy-form label { font-size: 13px; font-weight: 600; color: ${cssManager.bdTheme('#333', '#ccc')}; }
|
|
.policy-form input, .policy-form textarea {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#444')};
|
|
border-radius: 6px;
|
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
|
color: ${cssManager.bdTheme('#333', '#eee')};
|
|
font-size: 14px;
|
|
box-sizing: border-box;
|
|
}
|
|
.policy-form textarea {
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
min-height: 200px;
|
|
resize: vertical;
|
|
}
|
|
.policy-form input:focus, .policy-form textarea:focus {
|
|
outline: none;
|
|
border-color: ${cssManager.bdTheme('#2196f3', '#64b5f6')};
|
|
}
|
|
.policy-error { color: #ef5350; font-size: 13px; min-height: 20px; }
|
|
.policy-hint { color: ${cssManager.bdTheme('#666', '#999')}; font-size: 12px; }
|
|
</style>
|
|
<div class="policy-form">
|
|
<div>
|
|
<label>Name</label>
|
|
<input type="text" class="policy-name" .value=${policy.name} />
|
|
</div>
|
|
<div>
|
|
<label>Description</label>
|
|
<input type="text" class="policy-description" .value=${policy.description} />
|
|
</div>
|
|
<div>
|
|
<label>Statements (JSON array)</label>
|
|
<p class="policy-hint">Use \${bucket} in Resource ARNs — it will be replaced with the actual bucket name when applied.</p>
|
|
<textarea class="policy-statements">${statementsJson}</textarea>
|
|
</div>
|
|
<div class="policy-error"></div>
|
|
</div>
|
|
`,
|
|
menuOptions: [
|
|
{ name: 'Cancel', action: async (modal: any) => { modal.destroy(); } },
|
|
{
|
|
name: 'Save',
|
|
action: async (modal: any) => {
|
|
const root = modal.shadowRoot;
|
|
const name = (root.querySelector('.policy-name') as HTMLInputElement)?.value?.trim();
|
|
const description = (root.querySelector('.policy-description') as HTMLInputElement)?.value?.trim();
|
|
const statementsText = (root.querySelector('.policy-statements') as HTMLTextAreaElement)?.value?.trim();
|
|
const errorDiv = root.querySelector('.policy-error') as HTMLElement;
|
|
|
|
if (!name) { errorDiv.textContent = 'Name is required.'; return; }
|
|
if (!statementsText) { errorDiv.textContent = 'Statements are required.'; return; }
|
|
|
|
let statements: interfaces.data.IObjstStatement[];
|
|
try {
|
|
statements = JSON.parse(statementsText);
|
|
if (!Array.isArray(statements)) throw new Error('Must be an array');
|
|
} catch (err: any) {
|
|
errorDiv.textContent = `Invalid JSON: ${err.message}`;
|
|
return;
|
|
}
|
|
|
|
await appstate.policiesStatePart.dispatchAction(appstate.updatePolicyAction, {
|
|
policyId: policy.id,
|
|
name,
|
|
description: description || '',
|
|
statements,
|
|
});
|
|
modal.destroy();
|
|
},
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
private async showDeletePolicyModal(policy: interfaces.data.INamedPolicy): Promise<void> {
|
|
await DeesModal.createAndShow({
|
|
heading: 'Delete Policy',
|
|
content: html`
|
|
<p>Are you sure you want to delete policy <strong>${policy.name}</strong>?</p>
|
|
<p style="color: ${cssManager.bdTheme('#666', '#999')}; font-size: 13px;">
|
|
This will also detach the policy from all buckets it is currently applied to.
|
|
</p>
|
|
`,
|
|
menuOptions: [
|
|
{ name: 'Cancel', action: async (modal: any) => { modal.destroy(); } },
|
|
{
|
|
name: 'Delete',
|
|
action: async (modal: any) => {
|
|
await appstate.policiesStatePart.dispatchAction(appstate.deletePolicyAction, {
|
|
policyId: policy.id,
|
|
});
|
|
modal.destroy();
|
|
},
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
private async showPolicyBucketsModal(policy: interfaces.data.INamedPolicy): Promise<void> {
|
|
const data = await appstate.getPolicyBuckets(policy.id);
|
|
let modalRef: any = null;
|
|
|
|
const renderContent = (attached: string[], available: string[]) => html`
|
|
<style>
|
|
.bucket-lists { display: flex; flex-direction: column; gap: 16px; }
|
|
.bucket-section h4 {
|
|
margin: 0 0 8px 0;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
|
}
|
|
.bucket-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 8px 12px;
|
|
border-radius: 6px;
|
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a2e')};
|
|
margin-bottom: 4px;
|
|
font-size: 14px;
|
|
color: ${cssManager.bdTheme('#333', '#eee')};
|
|
}
|
|
.bucket-item button {
|
|
padding: 4px 12px;
|
|
border-radius: 4px;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
}
|
|
.btn-detach {
|
|
background: ${cssManager.bdTheme('#ffebee', '#3e1a1a')};
|
|
color: ${cssManager.bdTheme('#c62828', '#ef5350')};
|
|
}
|
|
.btn-detach:hover { opacity: 0.8; }
|
|
.btn-attach {
|
|
background: ${cssManager.bdTheme('#e3f2fd', '#1a2a3e')};
|
|
color: ${cssManager.bdTheme('#1565c0', '#64b5f6')};
|
|
}
|
|
.btn-attach:hover { opacity: 0.8; }
|
|
.empty-note {
|
|
color: ${cssManager.bdTheme('#999', '#666')};
|
|
font-size: 13px;
|
|
font-style: italic;
|
|
padding: 8px 0;
|
|
}
|
|
</style>
|
|
<div class="bucket-lists">
|
|
<div class="bucket-section">
|
|
<h4>Attached Buckets (${attached.length})</h4>
|
|
${attached.length > 0
|
|
? attached.map((bucket) => html`
|
|
<div class="bucket-item">
|
|
<span>${bucket}</span>
|
|
<button class="btn-detach" @click=${async (e: Event) => {
|
|
(e.target as HTMLButtonElement).disabled = true;
|
|
await appstate.detachPolicyFromBucket(policy.id, bucket);
|
|
const fresh = await appstate.getPolicyBuckets(policy.id);
|
|
if (modalRef) {
|
|
modalRef.content = renderContent(fresh.attachedBuckets, fresh.availableBuckets);
|
|
}
|
|
}}>Detach</button>
|
|
</div>
|
|
`)
|
|
: html`<p class="empty-note">No buckets attached to this policy.</p>`}
|
|
</div>
|
|
<div class="bucket-section">
|
|
<h4>Available Buckets (${available.length})</h4>
|
|
${available.length > 0
|
|
? available.map((bucket) => html`
|
|
<div class="bucket-item">
|
|
<span>${bucket}</span>
|
|
<button class="btn-attach" @click=${async (e: Event) => {
|
|
(e.target as HTMLButtonElement).disabled = true;
|
|
await appstate.attachPolicyToBucket(policy.id, bucket);
|
|
const fresh = await appstate.getPolicyBuckets(policy.id);
|
|
if (modalRef) {
|
|
modalRef.content = renderContent(fresh.attachedBuckets, fresh.availableBuckets);
|
|
}
|
|
}}>Attach</button>
|
|
</div>
|
|
`)
|
|
: html`<p class="empty-note">All buckets already have this policy.</p>`}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
modalRef = await DeesModal.createAndShow({
|
|
heading: `Buckets for: ${policy.name}`,
|
|
content: renderContent(data.attachedBuckets, data.availableBuckets),
|
|
menuOptions: [
|
|
{ name: 'Done', action: async (modal: any) => { modal.destroy(); } },
|
|
],
|
|
});
|
|
}
|
|
}
|