mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-06 14:31:57 +02:00
746 lines
36 KiB
Templ
746 lines
36 KiB
Templ
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
|
)
|
|
|
|
templ Policies(data dash.PoliciesData) {
|
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
|
<h1 class="h2">
|
|
<i class="bi bi-shield me-2"></i>IAM Policies
|
|
</h1>
|
|
<div class="btn-toolbar mb-2 mb-md-0">
|
|
<div class="btn-group me-2">
|
|
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#createPolicyModal">
|
|
<i class="bi bi-plus me-1"></i>Create Policy
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="policies-content">
|
|
<!-- Summary Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-xl-4 col-md-6 mb-4">
|
|
<div class="card border-left-primary shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
|
|
Total Policies
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{fmt.Sprintf("%d", data.TotalPolicies)}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="bi bi-shield icon-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-4 col-md-6 mb-4">
|
|
<div class="card border-left-success shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
|
|
Active Policies
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{fmt.Sprintf("%d", data.TotalPolicies)}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="bi bi-check-circle icon-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-4 col-md-6 mb-4">
|
|
<div class="card border-left-info shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
|
|
Last Updated
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{data.LastUpdated.Format("15:04")}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="bi bi-clock icon-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Policies Table -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="bi bi-shield me-2"></i>IAM Policies
|
|
</h6>
|
|
<div class="dropdown no-arrow">
|
|
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-three-dots-vertical icon-sm icon-fixed-width text-gray-400"></i>
|
|
</a>
|
|
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in">
|
|
<div class="dropdown-header">Actions:</div>
|
|
<a class="dropdown-item" href="#">
|
|
<i class="bi bi-download me-2"></i>Export List
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover" width="100%" cellspacing="0">
|
|
<thead>
|
|
<tr>
|
|
<th>Policy Name</th>
|
|
<th>Version</th>
|
|
<th>Statements</th>
|
|
<th>Created</th>
|
|
<th>Updated</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
for _, policy := range data.Policies {
|
|
<tr>
|
|
<td>
|
|
<strong>{policy.Name}</strong>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-info">{policy.Document.Version}</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-secondary">{fmt.Sprintf("%d statements", len(policy.Document.Statement))}</span>
|
|
</td>
|
|
<td>
|
|
<small class="text-muted">{policy.CreatedAt.Format("2006-01-02 15:04")}</small>
|
|
</td>
|
|
<td>
|
|
<small class="text-muted">{policy.UpdatedAt.Format("2006-01-02 15:04")}</small>
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm" role="group">
|
|
<button type="button" class="btn btn-outline-info view-policy-btn" title="View Policy" data-policy-name={policy.Name}>
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary edit-policy-btn" title="Edit Policy" data-policy-name={policy.Name}>
|
|
<i class="bi bi-pencil-square"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger delete-policy-btn" title="Delete Policy" data-policy-name={policy.Name}>
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
if len(data.Policies) == 0 {
|
|
<tr>
|
|
<td colspan="6" class="text-center text-muted py-4">
|
|
<i class="bi bi-shield icon-3x mb-3 text-muted"></i>
|
|
<div>
|
|
<h5>No IAM policies found</h5>
|
|
<p>Create your first policy to manage access permissions.</p>
|
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createPolicyModal">
|
|
<i class="bi bi-plus me-1"></i>Create Policy
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@PolicyDatalists()
|
|
|
|
<!-- Create Policy Modal -->
|
|
<div class="modal fade" id="createPolicyModal" tabindex="-1" aria-labelledby="createPolicyModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="createPolicyModalLabel">
|
|
<i class="bi bi-shield me-2"></i>Create IAM Policy
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="createPolicyForm">
|
|
<div class="mb-3">
|
|
<label for="policyName" class="form-label">Policy Name</label>
|
|
<input type="text" class="form-control" id="policyName" name="name" required placeholder="e.g., S3ReadOnlyPolicy">
|
|
<div class="form-text">Enter a unique name for this policy (alphanumeric and underscores only)</div>
|
|
</div>
|
|
|
|
<ul class="nav nav-tabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="createPolicyEditorTabBtn" type="button" data-bs-toggle="tab" data-bs-target="#createPolicyEditorTab">Editor</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="createPolicyJsonTabBtn" type="button" data-bs-toggle="tab" data-bs-target="#createPolicyJsonTab">JSON</button>
|
|
</li>
|
|
</ul>
|
|
<div class="tab-content border border-top-0 rounded-bottom p-3 mb-3">
|
|
<div class="tab-pane fade show active" id="createPolicyEditorTab">
|
|
<div id="createPolicyEditorBody"></div>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="createPolicyAddStatementBtn">
|
|
<i class="bi bi-plus me-1"></i>Add statement
|
|
</button>
|
|
</div>
|
|
<div class="tab-pane fade" id="createPolicyJsonTab">
|
|
<textarea class="form-control" id="policyDocument" name="document" rows="15" placeholder="Enter IAM policy JSON document..."></textarea>
|
|
<div class="form-text">Enter the policy document in AWS IAM JSON format</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3 d-flex justify-content-between">
|
|
<button type="button" class="btn btn-outline-info btn-sm" onclick="insertSamplePolicy('create')">
|
|
<i class="bi bi-file-earmark-text me-1"></i>Use Sample Policy
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="validatePolicyDocument('create')">
|
|
<i class="bi bi-check me-1"></i>Validate
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="createPolicy()">
|
|
<i class="bi bi-plus me-1"></i>Create Policy
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- View Policy Modal -->
|
|
<div class="modal fade" id="viewPolicyModal" tabindex="-1" aria-labelledby="viewPolicyModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="viewPolicyModalLabel">
|
|
<i class="bi bi-eye me-2"></i>View IAM Policy
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="viewPolicyContent">
|
|
<div class="text-center">
|
|
<div class="spinner-border" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-2">Loading policy...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-primary" id="editFromViewBtn">
|
|
<i class="bi bi-pencil-square me-1"></i>Edit Policy
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Policy Modal -->
|
|
<div class="modal fade" id="editPolicyModal" tabindex="-1" aria-labelledby="editPolicyModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="editPolicyModalLabel">
|
|
<i class="bi bi-pencil-square me-2"></i>Edit IAM Policy
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="editPolicyForm">
|
|
<div class="mb-3">
|
|
<label for="editPolicyName" class="form-label">Policy Name</label>
|
|
<input type="text" class="form-control" id="editPolicyName" name="name" readonly>
|
|
<div class="form-text">Policy name cannot be changed</div>
|
|
</div>
|
|
|
|
<ul class="nav nav-tabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="editPolicyEditorTabBtn" type="button" data-bs-toggle="tab" data-bs-target="#editPolicyEditorTab">Editor</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="editPolicyJsonTabBtn" type="button" data-bs-toggle="tab" data-bs-target="#editPolicyJsonTab">JSON</button>
|
|
</li>
|
|
</ul>
|
|
<div class="tab-content border border-top-0 rounded-bottom p-3 mb-3">
|
|
<div class="tab-pane fade show active" id="editPolicyEditorTab">
|
|
<div id="editPolicyEditorBody"></div>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="editPolicyAddStatementBtn">
|
|
<i class="bi bi-plus me-1"></i>Add statement
|
|
</button>
|
|
</div>
|
|
<div class="tab-pane fade" id="editPolicyJsonTab">
|
|
<textarea class="form-control" id="editPolicyDocument" name="document" rows="15"></textarea>
|
|
<div class="form-text">Edit the policy document in AWS IAM JSON format</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3 d-flex justify-content-between">
|
|
<button type="button" class="btn btn-outline-info btn-sm" onclick="insertSamplePolicy('edit')">
|
|
<i class="bi bi-file-earmark-text me-1"></i>Reset to Sample
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="validatePolicyDocument('edit')">
|
|
<i class="bi bi-check me-1"></i>Validate
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="updatePolicy()">
|
|
<i class="bi bi-floppy me-1"></i>Save Changes
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- JavaScript for Policy Management -->
|
|
<script>
|
|
// Current policy being viewed/edited
|
|
let currentPolicy = null;
|
|
// Drop edit-modal GET responses superseded by a later editPolicy call,
|
|
// so a stalled load for one policy can't populate the editor while the
|
|
// name field already says another. Same pattern as the bucket and
|
|
// S3 Tables policy dialogs.
|
|
let editPolicyRequestSeq = 0;
|
|
|
|
// Event listeners for policy actions
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Register the two editor instances used on this page. Config
|
|
// overrides here preserve the pre-extraction ids exactly, so this
|
|
// page's markup did not need to change. This has to run after
|
|
// DOMContentLoaded (not at the top of this script) because
|
|
// policy_editor.js - which defines registerPolicyEditor - is
|
|
// loaded by layout.templ's script bundle at the end of <body>,
|
|
// i.e. after this page's own content (including this inline
|
|
// script) has already been parsed and run.
|
|
registerPolicyEditor('create', { textareaId: 'policyDocument' });
|
|
registerPolicyEditor('edit', { textareaId: 'editPolicyDocument' });
|
|
setupPolicyEditor('create');
|
|
setupPolicyEditor('edit');
|
|
|
|
// View policy buttons
|
|
document.querySelectorAll('.view-policy-btn').forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
const policyName = this.getAttribute('data-policy-name');
|
|
viewPolicy(policyName);
|
|
});
|
|
});
|
|
|
|
// Edit policy buttons
|
|
document.querySelectorAll('.edit-policy-btn').forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
const policyName = this.getAttribute('data-policy-name');
|
|
editPolicy(policyName);
|
|
});
|
|
});
|
|
|
|
// Delete policy buttons
|
|
document.querySelectorAll('.delete-policy-btn').forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
const policyName = this.getAttribute('data-policy-name');
|
|
deletePolicy(policyName);
|
|
});
|
|
});
|
|
|
|
// Edit from view button
|
|
document.getElementById('editFromViewBtn').addEventListener('click', function() {
|
|
if (currentPolicy) {
|
|
const viewModal = bootstrap.Modal.getInstance(document.getElementById('viewPolicyModal'));
|
|
if (viewModal) viewModal.hide();
|
|
editPolicy(currentPolicy.name);
|
|
}
|
|
});
|
|
|
|
// Reset the create modal's editor each time it's opened, so leftover
|
|
// state from a previous (possibly cancelled) create doesn't leak in.
|
|
document.getElementById('createPolicyModal').addEventListener('show.bs.modal', function() {
|
|
document.getElementById('createPolicyForm').reset();
|
|
policyEditors.create = { version: '2012-10-17', statements: [], otherFields: {} };
|
|
renderPolicyEditor('create');
|
|
const editorTab = bootstrap.Tab.getOrCreateInstance(document.getElementById('createPolicyEditorTabBtn'));
|
|
editorTab.show();
|
|
});
|
|
});
|
|
|
|
function createPolicy() {
|
|
if (!commitPolicyActiveTab('create')) return;
|
|
if (!confirmPolicyFieldDiscard('create')) return;
|
|
|
|
const policyName = document.getElementById('policyName').value;
|
|
const policyDocumentText = document.getElementById('policyDocument').value;
|
|
|
|
if (!policyName || !policyDocumentText) {
|
|
showAlert('Please fill in all required fields', 'warning');
|
|
return;
|
|
}
|
|
|
|
let policyDocument;
|
|
try {
|
|
policyDocument = JSON.parse(policyDocumentText);
|
|
} catch (e) {
|
|
showAlert('Invalid JSON in policy document: ' + e.message, 'error');
|
|
return;
|
|
}
|
|
|
|
const requestData = {
|
|
name: policyName,
|
|
document: policyDocument
|
|
};
|
|
|
|
validatePolicyJSON(policyDocumentText, true).then(valid => {
|
|
if (!valid) return;
|
|
|
|
fetch(basePath('/api/object-store/policies'), {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(requestData)
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Policy created successfully!', 'success');
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('createPolicyModal'));
|
|
if (modal) modal.hide();
|
|
location.reload(); // Refresh the page to show the new policy
|
|
} else {
|
|
showAlert('Error creating policy: ' + (data.error || 'Unknown error'), 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showAlert('Error creating policy: ' + error.message, 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
function viewPolicy(policyName) {
|
|
// Show the modal first
|
|
const modal = new bootstrap.Modal(document.getElementById('viewPolicyModal'));
|
|
modal.show();
|
|
|
|
// Reset content to loading state
|
|
document.getElementById('viewPolicyContent').innerHTML = `
|
|
<div class="text-center">
|
|
<div class="spinner-border" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-2">Loading policy...</p>
|
|
</div>
|
|
`;
|
|
|
|
// Fetch policy data
|
|
fetch(basePath('/api/object-store/policies/' + encodeURIComponent(policyName)))
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Policy not found');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(policy => {
|
|
currentPolicy = policy;
|
|
displayPolicyDetails(policy);
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
document.getElementById('viewPolicyContent').innerHTML = `
|
|
<div class="alert alert-danger" role="alert">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
Error loading policy: ${error.message}
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
function displayPolicyDetails(policy) {
|
|
const content = document.getElementById('viewPolicyContent');
|
|
|
|
// Escapes a policy value for display, whether it's carried as a
|
|
// single value or a list, and whatever type it turns out to be.
|
|
function escapedJoin(list) {
|
|
if (list === undefined || list === null) return '';
|
|
return (Array.isArray(list) ? list : [list]).map(function(v) { return escapeHtml(String(v)); }).join(', ');
|
|
}
|
|
|
|
// Summarizes a Principal/NotPrincipal value for read-only display,
|
|
// regardless of shape (bare string/array, or a {"Type": ...} object -
|
|
// possibly with several type keys), since the View modal must stay
|
|
// correct even for forms the structured editor doesn't model.
|
|
function principalSummary(value) {
|
|
if (value === undefined || value === null) return '';
|
|
if (Array.isArray(value)) return escapedJoin(value);
|
|
if (typeof value === 'object') {
|
|
return Object.keys(value).map(function(key) {
|
|
const v = value[key];
|
|
return escapeHtml(key) + ': ' + escapedJoin(v);
|
|
}).join('; ');
|
|
}
|
|
return escapeHtml(String(value));
|
|
}
|
|
|
|
let statementsHtml = '';
|
|
if (policy.document && policy.document.Statement) {
|
|
statementsHtml = policy.document.Statement.map((stmt, index) => `
|
|
<div class="card mb-2">
|
|
<div class="card-header py-2">
|
|
<h6 class="mb-0">Statement ${index + 1}${stmt.Sid ? ' — ' + escapeHtml(stmt.Sid) : ''}</h6>
|
|
</div>
|
|
<div class="card-body py-2">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<strong>Effect:</strong>
|
|
<span class="badge ${stmt.Effect === 'Allow' ? 'bg-success' : 'bg-danger'}">${escapedJoin(stmt.Effect)}</span>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<strong>Actions:</strong> ${escapedJoin(stmt.Action)}
|
|
</div>
|
|
</div>
|
|
<div class="row mt-2">
|
|
<div class="col-12">
|
|
${stmt.NotResource
|
|
? '<strong>NotResource (excluded):</strong> ' + escapedJoin(stmt.NotResource)
|
|
: '<strong>Resource:</strong> ' + escapedJoin(stmt.Resource)}
|
|
</div>
|
|
</div>
|
|
${(stmt.Principal || stmt.NotPrincipal)
|
|
? '<div class="row mt-2"><div class="col-12">' +
|
|
(stmt.NotPrincipal
|
|
? '<strong>NotPrincipal (excluded):</strong> ' + principalSummary(stmt.NotPrincipal)
|
|
: '<strong>Principal:</strong> ' + principalSummary(stmt.Principal)) +
|
|
'</div></div>'
|
|
: ''}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
content.innerHTML = `
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<strong>Policy Name:</strong> ${escapedJoin(policy.name || 'Unknown')}
|
|
</div>
|
|
<div class="col-md-6">
|
|
<strong>Version:</strong> <span class="badge bg-info">${escapedJoin(policy.document?.Version || 'Unknown')}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<strong>Statements:</strong>
|
|
<div class="mt-2">
|
|
${statementsHtml || '<p class="text-muted">No statements found</p>'}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<strong>Raw Policy Document:</strong>
|
|
<pre class="bg-light p-3 border rounded mt-2"><code>${escapeHtml(JSON.stringify(policy.document, null, 2))}</code></pre>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function editPolicy(policyName) {
|
|
// Show the modal first
|
|
const modal = new bootstrap.Modal(document.getElementById('editPolicyModal'));
|
|
modal.show();
|
|
|
|
// Set policy name
|
|
document.getElementById('editPolicyName').value = policyName;
|
|
document.getElementById('editPolicyDocument').value = 'Loading...';
|
|
policyEditors.edit = { version: '2012-10-17', statements: [], otherFields: {} };
|
|
renderPolicyEditor('edit');
|
|
|
|
// Fetch policy data; drop the response if another editPolicy call
|
|
// superseded this one meanwhile.
|
|
const requestSeq = ++editPolicyRequestSeq;
|
|
fetch(basePath('/api/object-store/policies/' + encodeURIComponent(policyName)))
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Policy not found');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(policy => {
|
|
if (requestSeq !== editPolicyRequestSeq) return;
|
|
currentPolicy = policy;
|
|
document.getElementById('editPolicyDocument').value = JSON.stringify(policy.document, null, 2);
|
|
let state;
|
|
try {
|
|
state = policyDocToEditorState('edit', policy.document);
|
|
} catch (e) {
|
|
// Valid JSON the structured editor can't model. The JSON tab
|
|
// exists for exactly this, so hand the document over to it
|
|
// rather than closing the modal on the only view of it.
|
|
policyEditors.edit = { version: '2012-10-17', statements: [], otherFields: {}, unparsed: true };
|
|
renderPolicyEditor('edit');
|
|
showAlert(e.message + '. ' + POLICY_JSON_TAB_ONLY_MESSAGE, 'error');
|
|
bootstrap.Tab.getOrCreateInstance(document.getElementById('editPolicyJsonTabBtn')).show();
|
|
return;
|
|
}
|
|
policyEditors.edit = state;
|
|
renderPolicyEditor('edit');
|
|
const editorTab = bootstrap.Tab.getOrCreateInstance(document.getElementById('editPolicyEditorTabBtn'));
|
|
editorTab.show();
|
|
})
|
|
.catch(error => {
|
|
if (requestSeq !== editPolicyRequestSeq) return;
|
|
console.error('Error:', error);
|
|
showAlert('Error loading policy for editing: ' + error.message, 'error');
|
|
const editModal = bootstrap.Modal.getInstance(document.getElementById('editPolicyModal'));
|
|
if (editModal) editModal.hide();
|
|
});
|
|
}
|
|
|
|
function updatePolicy() {
|
|
if (!commitPolicyActiveTab('edit')) return;
|
|
if (!confirmPolicyFieldDiscard('edit')) return;
|
|
|
|
const policyName = document.getElementById('editPolicyName').value;
|
|
const policyDocumentText = document.getElementById('editPolicyDocument').value;
|
|
|
|
if (!policyName || !policyDocumentText) {
|
|
showAlert('Please fill in all required fields', 'warning');
|
|
return;
|
|
}
|
|
|
|
let policyDocument;
|
|
try {
|
|
policyDocument = JSON.parse(policyDocumentText);
|
|
} catch (e) {
|
|
showAlert('Invalid JSON in policy document: ' + e.message, 'error');
|
|
return;
|
|
}
|
|
|
|
const requestData = {
|
|
document: policyDocument
|
|
};
|
|
|
|
validatePolicyJSON(policyDocumentText, true).then(valid => {
|
|
if (!valid) return;
|
|
|
|
fetch(basePath('/api/object-store/policies/' + encodeURIComponent(policyName)), {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(requestData)
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Policy updated successfully!', 'success');
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('editPolicyModal'));
|
|
if (modal) modal.hide();
|
|
location.reload(); // Refresh the page to show the updated policy
|
|
} else {
|
|
showAlert('Error updating policy: ' + (data.error || 'Unknown error'), 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showAlert('Error updating policy: ' + error.message, 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
function validatePolicyDocument(which) {
|
|
if (!commitPolicyActiveTab(which)) return;
|
|
const policyText = document.getElementById(policyTextareaId(which)).value;
|
|
validatePolicyJSON(policyText);
|
|
}
|
|
|
|
// Runs a policy document past the server's validator. Resolves to true
|
|
// when it passes, and to false after alerting the user when it doesn't.
|
|
// `quiet` suppresses the success alert for callers that go on to report
|
|
// an outcome of their own.
|
|
function validatePolicyJSON(policyText, quiet) {
|
|
if (!policyText) {
|
|
showAlert('Please enter a policy document first', 'warning');
|
|
return Promise.resolve(false);
|
|
}
|
|
|
|
let policyDocument;
|
|
try {
|
|
policyDocument = JSON.parse(policyText);
|
|
} catch (e) {
|
|
showAlert('Invalid JSON: ' + e.message, 'error');
|
|
return Promise.resolve(false);
|
|
}
|
|
|
|
return fetch(basePath('/api/object-store/policies/validate'), {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ document: policyDocument })
|
|
})
|
|
.then(response => response.json().then(data => ({ ok: response.ok, data })))
|
|
.then(({ ok, data }) => {
|
|
if (ok && data.valid) {
|
|
if (!quiet) {
|
|
showAlert(data.message || 'Policy document is valid!', 'success');
|
|
}
|
|
return true;
|
|
}
|
|
showAlert('Invalid policy: ' + (data.error || 'unknown error'), 'error');
|
|
return false;
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showAlert('Error validating policy: ' + error.message, 'error');
|
|
return false;
|
|
});
|
|
}
|
|
|
|
function deletePolicy(policyName) {
|
|
showDeleteConfirm(policyName, function() {
|
|
fetch(basePath('/api/object-store/policies/' + encodeURIComponent(policyName)), {
|
|
method: 'DELETE'
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Policy deleted successfully!', 'success');
|
|
location.reload();
|
|
} else {
|
|
showAlert('Error deleting policy: ' + (data.error || 'Unknown error'), 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showAlert('Error deleting policy: ' + error.message, 'error');
|
|
});
|
|
});
|
|
}
|
|
</script>
|
|
}
|