mirror of
https://github.com/versity/versitygw.git
synced 2026-08-29 04:06:56 +00:00
Gates bucket listing behind an identity policy, lets browsers reach the standalone IAM API, and turns the WebUI into a dashboard for S3, IAM, or both.
**Bucket listing.** `ListBuckets` is now gated by the new `s3:ListAllMyBuckets` action, evaluated against `arn:aws:s3:::*`. The request names no bucket, so only identity policies apply — there is no resource policy to combine with, which is the same shape `CreateBucket` already had, so both now share one identity-only evaluation path. Root and admin bypass it, and backends with no identity-policy layer keep listing as before since their listing is already narrowed to the caller's own buckets. The action is IAM-only and is deliberately absent from the bucket-policy action list.
**Fixed bucket ownership.** The standalone IAM client has no per-user ownership to express — accounts are all plain users, cannot be enumerated, and access is decided by policy rather than ACL — so it now implements `auth.FixedBucketOwner` and every bucket is owned by root. Bucket creation stops resolving an owner, `ListBuckets` returns every bucket to every caller (what they may then do with one stays a per-request policy decision), and the admin `ChangeBucketOwner` reports method-not-supported. Other IAM backends are untouched.
**IAM service CORS.** `--cors-allow-origin` now applies to the `iam` command: it answers preflights and stamps the CORS headers, mirroring back the requested method and headers rather than enumerating the SigV4 header set. Without it no browser can reach the IAM API at all, so setting `--webui` without it falls back to `*` with a warning. The chart gets `iamServer.corsAllowOrigin`.
**WebUI.** New IAM pages for users, roles and OIDC providers, signing IAM/STS query-form requests directly from the browser. Navigation is capability-gated rather than role-gated: on sign-in the session probes the S3, admin and IAM endpoints independently and each page shows only what those credentials actually reach, so one build serves an IAM-only dashboard, an S3-only dashboard, and a combined one. The login page takes an optional IAM endpoint, seeded from the new `--webui-iam-gateways` (chart: `webui.iamGateways`) — never auto-detected, since the IAM service is a separate process. The WebUI can also be hosted by `versitygw iam` itself, for deployments with no S3 gateway behind it.
**The admin API is ignored once an IAM endpoint is in play.** The IAM service is then the user directory and bucket ownership is fixed, which leaves the admin API no job: the session is given no admin endpoint at all, its login field is hidden, `users.html` redirects to its IAM counterpart, and every admin-only surface stays off screen. Dashboard and Buckets remain available to any S3 session in such a deployment, running on the S3 and IAM APIs alone and surfacing each denial per action instead of redirecting.
Also fixes two WebUI bugs: embedded assets went out with a zero modification time and no `Cache-Control`, so browsers treated them as fresh for centuries and an upgraded gateway served new HTML against stale JS — they now revalidate against an ETag; and the login page's advanced-options section clipped its last field, since it animated to a height named in the stylesheet rather than the one it measures now.
**Usage**
IAM-only dashboard, served by the IAM service:
versitygw iam --port :7076 --webui :8080 --cors-allow-origin http://localhost:8080/
IAM + S3, dashboard served by the IAM service — point it at the gateway with `--webui-gateways`, and let the gateway accept the dashboard's origin:
versitygw iam --port :7076 --webui :8080 --webui-gateways http://localhost:7070/ --cors-allow-origin http://localhost:8080/
versitygw --port :7070 --cors-allow-origin http://localhost:8080/ posix /data
IAM + S3, dashboard served by the S3 gateway — point it at the IAM service with `--webui-iam-gateways`, and let the IAM service accept the dashboard's origin:
versitygw --port :7070 --webui :8080 --webui-iam-gateways http://localhost:7076/ posix /data
versitygw iam --port :7076 --cors-allow-origin http://localhost:8080/
776 lines
41 KiB
HTML
776 lines
41 KiB
HTML
<!--
|
|
Copyright 2026 Versity Software
|
|
This file is licensed under the Apache License, Version 2.0
|
|
(the "License"); you may not use this file except in compliance
|
|
with the License. You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing,
|
|
software distributed under the License is distributed on an
|
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
KIND, either express or implied. See the License for the
|
|
specific language governing permissions and limitations
|
|
under the License.
|
|
-->
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>VersityGW Admin - IAM Roles</title>
|
|
<script src="assets/js/crypto-js.min.js"></script>
|
|
<script src="assets/js/tailwind.js"></script>
|
|
<script src="assets/css/tailwind-config.js"></script>
|
|
<link rel="stylesheet" href="assets/css/fonts.css">
|
|
<link rel="stylesheet" href="assets/css/theme.css">
|
|
<link rel="icon" type="image/png" href="assets/images/favicon.png">
|
|
</head>
|
|
<body class="min-h-screen bg-surface">
|
|
<script src="js/api.js"></script>
|
|
<script src="js/app.js"></script>
|
|
<script src="js/iam-ui.js"></script>
|
|
|
|
<div class="relative flex h-screen overflow-hidden">
|
|
<input id="sidebar-toggle" type="checkbox" class="peer hidden"/>
|
|
<label for="sidebar-toggle" aria-label="Toggle navigation" class="
|
|
sm:hidden rotate-180 peer-checked:rotate-0 absolute z-20 top-[14px] left-6
|
|
flex justify-center items-center p-2 rounded-lg transition-all
|
|
text-charcoal-300 hover:text-charcoal hover:bg-gray-100
|
|
peer-checked:text-white/70 peer-checked:hover:text-white peer-checked:hover:bg-white/10
|
|
">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="-0.5 0 25 25">
|
|
<path stroke-width="3" stroke-linecap="round" stroke-linejoin="round" d="M7.6728 22L16.1434 13.0294C16.4081 12.75 16.4081 12.3088 16.1434 12.0147L7.65808 3" />
|
|
</svg>
|
|
</label>
|
|
<!-- Sidebar -->
|
|
<aside class="absolute z-10 sm:static -translate-x-60 peer-checked:translate-x-0 sm:!translate-x-0 w-60 h-screen bg-charcoal flex flex-col overflow-auto transition-all">
|
|
<div class="ml-12 sm:ml-0 h-16 flex-shrink-0 flex items-center px-6 border-b border-white/10">
|
|
<a href="https://www.versity.com" target="_blank" rel="noopener noreferrer">
|
|
<img src="assets/images/Versity-logo-white-horizontal.png" alt="Versity" class="h-10 hover:opacity-80 transition-opacity">
|
|
</a>
|
|
</div>
|
|
<nav class="flex-1 py-4">
|
|
<div class="px-6 pt-2 pb-2 text-[11px] font-semibold tracking-wider text-white/40 uppercase" data-management-only>
|
|
Management
|
|
</div>
|
|
<a href="dashboard.html" class="nav-item flex items-center gap-3 px-6 py-3 text-white/70 hover:text-white" data-management-only>
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
|
|
</svg>
|
|
<span class="font-medium">Dashboard</span>
|
|
</a>
|
|
<a href="users.html" class="nav-item flex items-center gap-3 px-6 py-3 text-white/70 hover:text-white" data-admin-only data-admin-users-only>
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
|
|
</svg>
|
|
<span class="font-medium">Users</span>
|
|
</a>
|
|
<a href="buckets.html" class="nav-item flex items-center gap-3 px-6 py-3 text-white/70 hover:text-white" data-management-only>
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"/>
|
|
</svg>
|
|
<span class="font-medium">Buckets</span>
|
|
</a>
|
|
<div class="mx-6 my-2 border-t border-white/10" data-management-only></div>
|
|
<a href="explorer.html" class="nav-item flex items-center gap-3 px-6 py-3 text-white/70 hover:text-white" data-s3-only>
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
|
</svg>
|
|
<span class="font-medium">Explorer</span>
|
|
</a>
|
|
<div class="mx-6 my-2 border-t border-white/10" data-s3-only></div>
|
|
<div class="px-6 pt-2 pb-2 text-[11px] font-semibold tracking-wider text-white/40 uppercase" data-iam-only>
|
|
Identity & Access
|
|
</div>
|
|
<a href="iam.html" class="nav-item flex items-center gap-3 px-6 py-3 text-white/70 hover:text-white" data-iam-only>
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
|
</svg>
|
|
<span class="font-medium">IAM</span>
|
|
</a>
|
|
<div class="mx-6 my-2 border-t border-white/10" data-iam-only></div>
|
|
<div class="px-6 pt-2 pb-2 text-[11px] font-semibold tracking-wider text-white/40 uppercase">
|
|
Resources
|
|
</div>
|
|
<a href="https://github.com/versity/versitygw/wiki" target="_blank" rel="noopener noreferrer" class="nav-item flex items-center gap-3 px-6 py-3 text-white/70 hover:text-white">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
|
|
</svg>
|
|
<span class="font-medium">Documentation</span>
|
|
</a>
|
|
<a href="https://github.com/versity/versitygw" target="_blank" rel="noopener noreferrer" class="nav-item flex items-center gap-3 px-6 py-3 text-white/70 hover:text-white">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
|
|
</svg>
|
|
<span class="font-medium">GitHub</span>
|
|
</a>
|
|
</nav>
|
|
<div class="p-4 border-t border-white/10">
|
|
<div id="user-info" class="flex items-center gap-3 mb-3"></div>
|
|
<button onclick="api.logout(); window.location.href='index.html';" class="w-full flex items-center gap-2 px-3 py-2 text-white/70 hover:text-white hover:bg-white/10 rounded-lg transition-colors text-sm">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
|
|
</svg>
|
|
Sign Out
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main Content -->
|
|
<div class="flex-1 flex flex-col overflow-hidden">
|
|
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6 flex-shrink-0">
|
|
<h1 class="ml-12 sm:ml-0 text-xl font-semibold text-charcoal">VersityGW IAM Roles</h1>
|
|
<button onclick="loadRoles()" class="p-2 text-charcoal-300 hover:text-charcoal hover:bg-gray-100 rounded-lg transition-colors" title="Refresh">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
|
</svg>
|
|
</button>
|
|
</header>
|
|
|
|
<main class="flex-1 overflow-auto p-6">
|
|
<div class="max-w-7xl mx-auto">
|
|
<!-- Page Header -->
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 class="text-2xl font-semibold text-charcoal">Roles</h1>
|
|
<p class="text-charcoal-300 mt-1">Manage IAM roles, trust policies, and inline policies</p>
|
|
</div>
|
|
<button onclick="openCreateRoleModal()" class="flex items-center gap-2 bg-primary hover:bg-primary-600 text-white font-medium py-2.5 px-4 rounded-lg transition-colors">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
|
</svg>
|
|
Create Role
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Filters & Search -->
|
|
<div class="bg-white rounded-xl p-4 shadow-sm border border-gray-100 mb-6">
|
|
<div class="flex flex-wrap items-center gap-4">
|
|
<div class="relative flex-1 min-w-64">
|
|
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-charcoal-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
</svg>
|
|
<input type="text" id="search-input" placeholder="Search by role name..." oninput="filterRoles()" class="w-full pl-10 pr-4 py-2.5 border border-gray-200 rounded-lg text-charcoal placeholder:text-charcoal-300 focus:outline-none focus:border-accent focus:ring-2 focus:ring-accent/20 transition-all">
|
|
</div>
|
|
<div class="relative min-w-56">
|
|
<input type="text" id="path-prefix-input" placeholder="Filter by path prefix..." class="w-full px-4 py-2.5 border border-gray-200 rounded-lg text-charcoal font-mono text-sm placeholder:font-sans placeholder:text-charcoal-300 focus:outline-none focus:border-accent focus:ring-2 focus:ring-accent/20 transition-all">
|
|
</div>
|
|
<button onclick="loadRoles()" class="px-4 py-2.5 border border-gray-200 rounded-lg text-charcoal font-medium hover:bg-gray-50 transition-colors">Apply</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Roles Table -->
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full">
|
|
<thead class="bg-gray-50 border-b border-gray-100">
|
|
<tr>
|
|
<th class="text-left py-4 px-6 text-sm font-semibold text-charcoal">Path</th>
|
|
<th class="text-left py-4 px-6 text-sm font-semibold text-charcoal">Role Name</th>
|
|
<th class="text-left py-4 px-6 text-sm font-semibold text-charcoal">ARN</th>
|
|
<th class="text-left py-4 px-6 text-sm font-semibold text-charcoal">Created</th>
|
|
<th class="text-left py-4 px-6 text-sm font-semibold text-charcoal">Max Session</th>
|
|
<th class="text-right py-4 px-6 text-sm font-semibold text-charcoal">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="roles-table-body"></tbody>
|
|
</table>
|
|
</div>
|
|
<div id="load-more-row" class="hidden border-t border-gray-100 p-4 text-center">
|
|
<button id="load-more-btn" onclick="loadMoreRoles()" class="px-4 py-2.5 border border-gray-200 rounded-lg text-charcoal font-medium hover:bg-gray-50 transition-colors">Load More</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Role Modal (step 1 of 2) -->
|
|
<div id="create-role-modal" class="modal hidden fixed inset-0 z-50">
|
|
<div class="modal-backdrop absolute inset-0" onclick="closeModal('create-role-modal')"></div>
|
|
<div class="absolute inset-0 flex items-center justify-center p-4">
|
|
<div class="bg-white rounded-xl shadow-2xl w-full max-w-lg relative max-h-[90vh] flex flex-col">
|
|
<div class="flex items-center justify-between p-6 border-b border-gray-100 flex-shrink-0">
|
|
<div>
|
|
<h2 class="text-xl font-semibold text-charcoal">Create Role</h2>
|
|
<p class="text-sm text-charcoal-300 mt-1">Step 1 of 2 — Role details</p>
|
|
</div>
|
|
<button onclick="closeModal('create-role-modal')" class="p-2 text-charcoal-300 hover:text-charcoal hover:bg-gray-100 rounded-lg transition-colors">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="flex-1 overflow-auto">
|
|
<form id="create-role-form" class="p-6 space-y-5">
|
|
<div>
|
|
<label class="block text-sm font-medium text-charcoal mb-2">Role Name <span class="text-red-500">*</span></label>
|
|
<input type="text" id="create-role-name" maxlength="64" placeholder="e.g., archive-reader" class="w-full px-4 py-2.5 border-2 border-gray-200 rounded-lg text-charcoal font-mono text-sm placeholder:font-sans placeholder:text-charcoal-300 focus:outline-none focus:border-accent focus:ring-2 focus:ring-accent/20 transition-all">
|
|
<p class="mt-2 text-xs text-charcoal-300">Up to 64 characters. Letters, numbers and + = , . @ _ - only. The name cannot be changed after creation.</p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-charcoal mb-2">Description</label>
|
|
<textarea id="create-role-description" rows="2" maxlength="1000" placeholder="What this role is for" class="w-full px-4 py-2.5 border-2 border-gray-200 rounded-lg text-charcoal text-sm placeholder:text-charcoal-300 focus:outline-none focus:border-accent focus:ring-2 focus:ring-accent/20 transition-all resize-none"></textarea>
|
|
<p class="mt-2 text-xs text-charcoal-300">Up to 1000 characters. Not editable after creation.</p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-charcoal mb-2">Max Session Duration</label>
|
|
<div class="flex items-center gap-4">
|
|
<input type="range" id="create-role-duration-range" min="3600" max="43200" step="900" value="3600" oninput="syncDuration('range')" class="flex-1 accent-accent">
|
|
<div class="w-32">
|
|
<input type="number" id="create-role-duration" min="3600" max="43200" step="1" value="3600" oninput="syncDuration('number')" class="w-full px-3 py-2 border-2 border-gray-200 rounded-lg text-charcoal text-sm focus:outline-none focus:border-accent focus:ring-2 focus:ring-accent/20 transition-all">
|
|
</div>
|
|
</div>
|
|
<p id="duration-note" class="mt-2 text-xs text-charcoal-300">3600 seconds (1h 0m). Valid range 3600–43200 seconds. Not editable after creation.</p>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center justify-between mb-2">
|
|
<label class="block text-sm font-medium text-charcoal">Tags</label>
|
|
<button type="button" onclick="iamAddTagRow('create-role-tags')" class="px-3 py-1.5 text-xs border border-gray-200 hover:bg-gray-50 text-charcoal rounded-lg transition-colors">Add Tag</button>
|
|
</div>
|
|
<div id="create-role-tags" class="space-y-2"></div>
|
|
<p class="mt-2 text-xs text-charcoal-300">Tags are set at creation only.</p>
|
|
</div>
|
|
<details class="group">
|
|
<summary class="flex items-center gap-2 cursor-pointer text-sm font-medium text-charcoal-400 hover:text-charcoal transition-colors list-none">
|
|
<svg class="w-4 h-4 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
</svg>
|
|
Advanced Options
|
|
</summary>
|
|
<div class="mt-4 space-y-4 pl-6">
|
|
<div>
|
|
<label class="block text-sm font-medium text-charcoal mb-2">Path</label>
|
|
<input type="text" id="create-role-path" maxlength="512" placeholder="/" class="w-full px-4 py-2.5 border-2 border-gray-200 rounded-lg text-charcoal font-mono text-sm placeholder:font-sans placeholder:text-charcoal-300 focus:outline-none focus:border-accent focus:ring-2 focus:ring-accent/20 transition-all">
|
|
<p class="mt-2 text-xs text-charcoal-300">Defaults to /. Must start and end with /.</p>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
</form>
|
|
</div>
|
|
<div class="flex items-center justify-end gap-3 p-6 border-t border-gray-100 flex-shrink-0">
|
|
<button onclick="closeModal('create-role-modal')" class="px-4 py-2.5 border border-gray-200 rounded-lg text-charcoal font-medium hover:bg-gray-50 transition-colors">Cancel</button>
|
|
<button onclick="goToTrustPolicyStep()" class="px-4 py-2.5 bg-primary hover:bg-primary-600 text-white font-medium rounded-lg transition-colors">Next: Trust Policy</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Manage Role Modal -->
|
|
<div id="manage-role-modal" class="modal hidden fixed inset-0 z-50">
|
|
<div class="modal-backdrop absolute inset-0" onclick="closeModal('manage-role-modal')"></div>
|
|
<div class="absolute inset-0 flex items-center justify-center p-4">
|
|
<div class="bg-white rounded-xl shadow-2xl w-full max-w-4xl relative max-h-[90vh] flex flex-col">
|
|
<div class="flex items-center justify-between p-6 border-b border-gray-100 flex-shrink-0">
|
|
<div>
|
|
<h2 id="manage-role-title" class="text-xl font-semibold text-charcoal">Role</h2>
|
|
<p class="text-sm text-charcoal-300 mt-1">Only the trust policy and inline policies can be changed after creation</p>
|
|
</div>
|
|
<button onclick="closeModal('manage-role-modal')" class="p-2 text-charcoal-300 hover:text-charcoal hover:bg-gray-100 rounded-lg transition-colors">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="flex-1 overflow-auto">
|
|
<div class="p-6 space-y-6">
|
|
<!-- Read-only details -->
|
|
<div class="bg-surface border border-gray-100 rounded-lg p-4">
|
|
<dl class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<dt class="text-charcoal-300">ARN</dt>
|
|
<dd id="role-detail-arn" class="mt-1">-</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="text-charcoal-300">Role ID</dt>
|
|
<dd id="role-detail-id" class="mt-1 font-mono text-xs text-charcoal">-</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="text-charcoal-300" title="Set at creation, not editable">Path</dt>
|
|
<dd id="role-detail-path" class="mt-1 font-mono text-xs text-charcoal">-</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="text-charcoal-300">Created</dt>
|
|
<dd id="role-detail-created" class="mt-1 text-charcoal">-</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="text-charcoal-300" title="Set at creation, not editable">Max Session Duration</dt>
|
|
<dd id="role-detail-duration" class="mt-1 text-charcoal">-</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="text-charcoal-300" title="Set at creation, not editable">Description</dt>
|
|
<dd id="role-detail-description" class="mt-1 text-charcoal">-</dd>
|
|
</div>
|
|
<div class="sm:col-span-2">
|
|
<dt class="text-charcoal-300">Tags</dt>
|
|
<dd id="role-detail-tags" class="mt-1 flex flex-wrap gap-2">-</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<!-- Trust policy -->
|
|
<div>
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div>
|
|
<h3 class="text-sm font-semibold text-charcoal">Trust Policy</h3>
|
|
<p class="text-xs text-charcoal-300 mt-1">Who may assume this role. Up to 2048 bytes.</p>
|
|
</div>
|
|
<button onclick="openTrustPolicyEditor()" class="px-3 py-1.5 text-xs border border-accent text-accent hover:bg-accent-50 font-medium rounded-lg transition-colors">Edit Trust Policy</button>
|
|
</div>
|
|
<pre id="role-trust-preview" class="border border-gray-100 rounded-lg bg-gray-50 p-4 text-xs font-mono text-charcoal overflow-auto max-h-48">-</pre>
|
|
</div>
|
|
|
|
<!-- Inline policies -->
|
|
<div>
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div>
|
|
<h3 class="text-sm font-semibold text-charcoal">Inline Policies</h3>
|
|
<p id="role-policy-quota-note" class="text-xs text-charcoal-300 mt-1">0 / 10240 bytes used</p>
|
|
</div>
|
|
<button onclick="openRolePolicyEditor()" class="px-3 py-1.5 text-xs border border-accent text-accent hover:bg-accent-50 font-medium rounded-lg transition-colors">Add Policy</button>
|
|
</div>
|
|
<div class="border border-gray-100 rounded-lg overflow-hidden">
|
|
<table class="w-full">
|
|
<thead class="bg-gray-50 border-b border-gray-100">
|
|
<tr>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-charcoal">Policy Name</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-charcoal">Size</th>
|
|
<th class="text-right py-3 px-4 text-xs font-semibold text-charcoal">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="role-policies-table-body"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center justify-end gap-3 p-6 border-t border-gray-100 flex-shrink-0">
|
|
<button onclick="closeModal('manage-role-modal')" class="px-4 py-2.5 bg-primary hover:bg-primary-600 text-white font-medium rounded-lg transition-colors">Done</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
<div id="delete-role-modal" class="modal hidden fixed inset-0 z-50">
|
|
<div class="modal-backdrop absolute inset-0" onclick="closeModal('delete-role-modal')"></div>
|
|
<div class="absolute inset-0 flex items-center justify-center p-4">
|
|
<div class="bg-white rounded-xl shadow-2xl w-full max-w-md relative">
|
|
<div class="p-6">
|
|
<div class="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
|
</svg>
|
|
</div>
|
|
<h3 class="text-lg font-semibold text-charcoal text-center mb-2">Delete Role</h3>
|
|
<p class="text-charcoal-300 text-center mb-4">
|
|
Are you sure you want to delete <span id="delete-role-name" class="font-mono text-charcoal"></span>? This action cannot be undone.
|
|
</p>
|
|
<p class="text-charcoal-300 text-center text-sm mb-6">Inline policies must be removed first. There is no cascade delete.</p>
|
|
<div class="flex items-center justify-center gap-3">
|
|
<button onclick="closeModal('delete-role-modal')" class="px-4 py-2.5 border border-gray-200 rounded-lg text-charcoal font-medium hover:bg-gray-50 transition-colors">Cancel</button>
|
|
<button id="confirm-delete-role-btn" onclick="confirmDeleteRole()" class="px-4 py-2.5 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors">Delete Role</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let allRoles = [];
|
|
let nextMarker = null;
|
|
let currentRole = null;
|
|
let rolePolicySizes = {};
|
|
let roleToDelete = null;
|
|
let pendingRoleDetails = null; // step 1 of the create wizard
|
|
|
|
if (!requireIAM()) {
|
|
// Redirected
|
|
} else {
|
|
initSidebarWithRole();
|
|
updateUserInfo();
|
|
loadRoles();
|
|
}
|
|
|
|
// ============================================
|
|
// List
|
|
// ============================================
|
|
|
|
async function loadRoles() {
|
|
showTableLoading('roles-table-body', 6);
|
|
allRoles = [];
|
|
nextMarker = null;
|
|
await fetchRolePage();
|
|
}
|
|
|
|
async function loadMoreRoles() {
|
|
const btn = document.getElementById('load-more-btn');
|
|
setLoading(btn, true);
|
|
try {
|
|
await fetchRolePage();
|
|
} finally {
|
|
setLoading(btn, false);
|
|
}
|
|
}
|
|
|
|
async function fetchRolePage() {
|
|
const pathPrefix = document.getElementById('path-prefix-input').value.trim();
|
|
try {
|
|
const result = await api.iamListRoles({
|
|
pathPrefix: pathPrefix || undefined,
|
|
marker: nextMarker || undefined,
|
|
maxItems: IAM_LIMITS.listPageSize
|
|
});
|
|
allRoles = allRoles.concat(result.roles);
|
|
nextMarker = result.isTruncated ? result.marker : null;
|
|
document.getElementById('load-more-row').classList.toggle('hidden', !nextMarker);
|
|
filterRoles();
|
|
} catch (error) {
|
|
console.error('Error loading roles:', error);
|
|
document.getElementById('load-more-row').classList.add('hidden');
|
|
if (iamIsAccessDenied(error)) {
|
|
iamShowAccessDenied('roles-table-body', 6, 'You don\u2019t have permission to list roles');
|
|
} else {
|
|
showToast(iamErrorText(error, 'loading roles'), 'error');
|
|
showEmptyState('roles-table-body', 6, 'Error loading roles');
|
|
}
|
|
}
|
|
}
|
|
|
|
function filterRoles() {
|
|
const term = document.getElementById('search-input').value.toLowerCase();
|
|
const filtered = term
|
|
? allRoles.filter(r => (r.RoleName || '').toLowerCase().includes(term))
|
|
: allRoles;
|
|
renderRoles(filtered);
|
|
}
|
|
|
|
function formatDuration(seconds) {
|
|
const value = parseInt(seconds, 10) || IAM_LIMITS.minSessionDuration;
|
|
const hours = Math.floor(value / 3600);
|
|
const minutes = Math.round((value % 3600) / 60);
|
|
return `${hours}h ${minutes}m`;
|
|
}
|
|
|
|
function renderRoles(roles) {
|
|
const tbody = document.getElementById('roles-table-body');
|
|
tbody.innerHTML = '';
|
|
|
|
if (roles.length === 0) {
|
|
showEmptyState('roles-table-body', 6, 'No roles found');
|
|
return;
|
|
}
|
|
|
|
roles.forEach(role => {
|
|
const name = role.RoleName || '';
|
|
const row = document.createElement('tr');
|
|
row.className = 'border-b border-gray-50 hover:bg-gray-50 transition-colors';
|
|
row.innerHTML = `
|
|
<td class="py-4 px-6"><span class="font-mono text-sm text-charcoal">${escapeHtml(role.Path || '/')}</span></td>
|
|
<td class="py-4 px-6"><span class="font-mono text-sm text-charcoal">${escapeHtml(name)}</span></td>
|
|
<td class="py-4 px-6">${iamArnCell(role.Arn)}</td>
|
|
<td class="py-4 px-6 text-sm text-charcoal">${escapeHtml(iamFormatDate(role.CreateDate))}</td>
|
|
<td class="py-4 px-6 text-sm text-charcoal">${escapeHtml(formatDuration(role.MaxSessionDuration))}</td>
|
|
<td class="py-4 px-6 text-right">
|
|
<div class="flex items-center justify-end gap-2">
|
|
<button onclick="openManageRoleModal('${escapeHtml(name)}')" class="px-3 py-1.5 text-xs border border-gray-200 hover:bg-gray-50 text-charcoal font-medium rounded-lg transition-colors">Manage</button>
|
|
<button onclick="openDeleteRoleModal('${escapeHtml(name)}')" class="p-2 text-charcoal-300 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors" title="Delete">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
|
</button>
|
|
</div>
|
|
</td>`;
|
|
tbody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
// ============================================
|
|
// Create wizard
|
|
// ============================================
|
|
|
|
function openCreateRoleModal() {
|
|
document.getElementById('create-role-name').value = '';
|
|
document.getElementById('create-role-description').value = '';
|
|
document.getElementById('create-role-path').value = '';
|
|
document.getElementById('create-role-tags').innerHTML = '';
|
|
document.getElementById('create-role-duration').value = IAM_LIMITS.minSessionDuration;
|
|
document.getElementById('create-role-duration-range').value = IAM_LIMITS.minSessionDuration;
|
|
syncDuration('number');
|
|
pendingRoleDetails = null;
|
|
openModal('create-role-modal');
|
|
}
|
|
|
|
function syncDuration(source) {
|
|
const number = document.getElementById('create-role-duration');
|
|
const range = document.getElementById('create-role-duration-range');
|
|
let value = parseInt(source === 'range' ? range.value : number.value, 10);
|
|
if (isNaN(value)) value = IAM_LIMITS.minSessionDuration;
|
|
value = Math.min(IAM_LIMITS.maxSessionDuration, Math.max(IAM_LIMITS.minSessionDuration, value));
|
|
if (source === 'range') number.value = value;
|
|
else range.value = value;
|
|
document.getElementById('duration-note').textContent =
|
|
`${value} seconds (${formatDuration(value)}). Valid range 3600\u201343200 seconds. Not editable after creation.`;
|
|
}
|
|
|
|
function goToTrustPolicyStep() {
|
|
const roleName = document.getElementById('create-role-name').value.trim();
|
|
const description = document.getElementById('create-role-description').value.trim();
|
|
const path = document.getElementById('create-role-path').value.trim();
|
|
const duration = parseInt(document.getElementById('create-role-duration').value, 10) || IAM_LIMITS.minSessionDuration;
|
|
const tags = iamCollectTags('create-role-tags');
|
|
|
|
const nameError = iamValidateName(roleName, 'Role name');
|
|
if (nameError) { showToast(nameError, 'error'); return; }
|
|
const pathError = iamValidatePath(path);
|
|
if (pathError) { showToast(pathError, 'error'); return; }
|
|
if (description.length > IAM_LIMITS.roleDescriptionChars) {
|
|
showToast(`Description must be ${IAM_LIMITS.roleDescriptionChars} characters or fewer.`, 'error');
|
|
return;
|
|
}
|
|
|
|
pendingRoleDetails = { roleName, description, path, duration, tags };
|
|
closeModal('create-role-modal');
|
|
|
|
iamPolicyEditor.open({
|
|
variant: 'trust',
|
|
title: 'Create Role',
|
|
subtitle: `Step 2 of 2 \u2014 Trust policy for ${roleName}`,
|
|
saveLabel: 'Create Role',
|
|
document: '',
|
|
maxBytes: IAM_LIMITS.trustPolicyBytes,
|
|
onSave: async ({ document: doc }) => {
|
|
await api.iamCreateRole(pendingRoleDetails.roleName, doc, {
|
|
path: pendingRoleDetails.path || undefined,
|
|
description: pendingRoleDetails.description || undefined,
|
|
maxSessionDuration: pendingRoleDetails.duration,
|
|
tags: pendingRoleDetails.tags
|
|
});
|
|
showToast('Role created successfully', 'success');
|
|
pendingRoleDetails = null;
|
|
loadRoles();
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============================================
|
|
// Manage
|
|
// ============================================
|
|
|
|
async function openManageRoleModal(roleName) {
|
|
currentRole = allRoles.find(r => r.RoleName === roleName) || { RoleName: roleName };
|
|
rolePolicySizes = {};
|
|
|
|
document.getElementById('manage-role-title').textContent = roleName;
|
|
renderRoleDetails(currentRole);
|
|
openModal('manage-role-modal');
|
|
|
|
// Refresh from the server so the trust document is current
|
|
try {
|
|
currentRole = await api.iamGetRole(roleName);
|
|
renderRoleDetails(currentRole);
|
|
} catch (error) {
|
|
if (!iamIsAccessDenied(error)) {
|
|
showToast(iamErrorText(error, 'loading role'), 'error');
|
|
}
|
|
}
|
|
loadRolePolicies();
|
|
}
|
|
|
|
function renderRoleDetails(role) {
|
|
document.getElementById('role-detail-arn').innerHTML = iamArnCell(role.Arn);
|
|
document.getElementById('role-detail-id').textContent = role.RoleId || '-';
|
|
document.getElementById('role-detail-path').textContent = role.Path || '/';
|
|
document.getElementById('role-detail-created').textContent = iamFormatDate(role.CreateDate);
|
|
document.getElementById('role-detail-duration').textContent = role.MaxSessionDuration
|
|
? `${role.MaxSessionDuration} seconds (${formatDuration(role.MaxSessionDuration)})`
|
|
: '-';
|
|
document.getElementById('role-detail-description').textContent = role.Description || '-';
|
|
|
|
const tagsEl = document.getElementById('role-detail-tags');
|
|
const tags = Array.isArray(role.Tags) ? role.Tags : (role.Tags ? [role.Tags] : []);
|
|
tagsEl.innerHTML = tags.length === 0
|
|
? '<span class="text-charcoal-300">-</span>'
|
|
: tags.map(tag => `<span class="px-2 py-0.5 bg-gray-100 text-charcoal text-xs font-mono rounded">${escapeHtml(tag.Key)}=${escapeHtml(tag.Value || '')}</span>`).join('');
|
|
|
|
const preview = document.getElementById('role-trust-preview');
|
|
const trust = role.AssumeRolePolicyDocument || '';
|
|
if (!trust) {
|
|
preview.textContent = '-';
|
|
} else {
|
|
try {
|
|
preview.textContent = JSON.stringify(JSON.parse(trust), null, 2);
|
|
} catch (e) {
|
|
preview.textContent = trust;
|
|
}
|
|
}
|
|
}
|
|
|
|
function openTrustPolicyEditor() {
|
|
if (!currentRole) return;
|
|
let documentText = currentRole.AssumeRolePolicyDocument || '';
|
|
try {
|
|
documentText = JSON.stringify(JSON.parse(documentText), null, 2);
|
|
} catch (e) {
|
|
// Leave as-is
|
|
}
|
|
iamPolicyEditor.open({
|
|
variant: 'trust',
|
|
title: 'Trust Policy',
|
|
subtitle: `Role ${currentRole.RoleName}`,
|
|
document: documentText,
|
|
maxBytes: IAM_LIMITS.trustPolicyBytes,
|
|
onSave: async ({ document: doc }) => {
|
|
await api.iamUpdateAssumeRolePolicy(currentRole.RoleName, doc);
|
|
showToast('Trust policy updated successfully', 'success');
|
|
currentRole.AssumeRolePolicyDocument = doc;
|
|
renderRoleDetails(currentRole);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function loadRolePolicies() {
|
|
const tbody = document.getElementById('role-policies-table-body');
|
|
showTableLoading('role-policies-table-body', 3);
|
|
rolePolicySizes = {};
|
|
try {
|
|
const { policyNames } = await api.iamListRolePolicies(currentRole.RoleName);
|
|
if (policyNames.length === 0) {
|
|
showEmptyState('role-policies-table-body', 3, 'No inline policies');
|
|
updateRolePolicyQuotaNote();
|
|
return;
|
|
}
|
|
await Promise.all(policyNames.map(async name => {
|
|
try {
|
|
const policy = await api.iamGetRolePolicy(currentRole.RoleName, name);
|
|
rolePolicySizes[name] = iamByteLength(policy.policyDocument);
|
|
} catch (error) {
|
|
rolePolicySizes[name] = null;
|
|
}
|
|
}));
|
|
tbody.innerHTML = '';
|
|
policyNames.forEach(name => {
|
|
const size = rolePolicySizes[name];
|
|
const row = document.createElement('tr');
|
|
row.className = 'border-b border-gray-50';
|
|
row.innerHTML = `
|
|
<td class="py-3 px-4"><span class="font-mono text-xs text-charcoal">${escapeHtml(name)}</span></td>
|
|
<td class="py-3 px-4 text-xs text-charcoal">${size === null ? '-' : size + ' bytes'}</td>
|
|
<td class="py-3 px-4 text-right">
|
|
<div class="flex items-center justify-end gap-2">
|
|
<button onclick="openRolePolicyEditor('${escapeHtml(name)}')" class="px-2.5 py-1 text-xs border border-gray-200 hover:bg-gray-50 text-charcoal rounded-lg transition-colors">Edit</button>
|
|
<button onclick="deleteRolePolicy('${escapeHtml(name)}')" class="p-1.5 text-charcoal-300 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors" title="Delete">
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
|
</button>
|
|
</div>
|
|
</td>`;
|
|
tbody.appendChild(row);
|
|
});
|
|
updateRolePolicyQuotaNote();
|
|
} catch (error) {
|
|
console.error('Error loading inline policies:', error);
|
|
if (iamIsAccessDenied(error)) {
|
|
iamShowAccessDenied('role-policies-table-body', 3, 'You don\u2019t have permission to list this role\u2019s inline policies');
|
|
} else {
|
|
showEmptyState('role-policies-table-body', 3, 'Error loading inline policies: ' + iamShortError(error));
|
|
}
|
|
}
|
|
}
|
|
|
|
function totalRolePolicyBytes(excludeName) {
|
|
return Object.entries(rolePolicySizes).reduce((sum, [name, size]) => {
|
|
if (name === excludeName || size === null) return sum;
|
|
return sum + size;
|
|
}, 0);
|
|
}
|
|
|
|
function updateRolePolicyQuotaNote() {
|
|
const used = totalRolePolicyBytes();
|
|
const note = document.getElementById('role-policy-quota-note');
|
|
note.textContent = `${used} / ${IAM_LIMITS.rolePolicyBytes} bytes used across this role's inline policies`;
|
|
note.className = used > IAM_LIMITS.rolePolicyBytes ? 'text-xs text-red-600 font-medium mt-1' : 'text-xs text-charcoal-300 mt-1';
|
|
}
|
|
|
|
async function openRolePolicyEditor(policyName) {
|
|
const isNew = !policyName;
|
|
let documentText = '';
|
|
if (!isNew) {
|
|
try {
|
|
const policy = await api.iamGetRolePolicy(currentRole.RoleName, policyName);
|
|
documentText = policy.policyDocument;
|
|
try {
|
|
documentText = JSON.stringify(JSON.parse(documentText), null, 2);
|
|
} catch (e) {
|
|
// Leave the server's text as-is if it is not valid JSON
|
|
}
|
|
} catch (error) {
|
|
showToast(iamErrorText(error, 'loading policy'), 'error');
|
|
return;
|
|
}
|
|
}
|
|
|
|
iamPolicyEditor.open({
|
|
variant: 'identity',
|
|
title: isNew ? 'Add Inline Policy' : 'Inline Policy',
|
|
subtitle: `Role ${currentRole.RoleName}${isNew ? '' : ' \u2014 ' + policyName}`,
|
|
policyName: policyName || '',
|
|
nameEditable: isNew,
|
|
document: documentText,
|
|
quota: { otherBytes: totalRolePolicyBytes(policyName), max: IAM_LIMITS.rolePolicyBytes },
|
|
maxBytes: IAM_LIMITS.policyDocumentBytes,
|
|
showDelete: !isNew,
|
|
onSave: async ({ policyName: name, document: doc }) => {
|
|
await api.iamPutRolePolicy(currentRole.RoleName, name, doc);
|
|
showToast('Policy saved successfully', 'success');
|
|
loadRolePolicies();
|
|
},
|
|
onDelete: async () => {
|
|
await api.iamDeleteRolePolicy(currentRole.RoleName, policyName);
|
|
showToast('Policy deleted successfully', 'success');
|
|
loadRolePolicies();
|
|
}
|
|
});
|
|
}
|
|
|
|
function deleteRolePolicy(policyName) {
|
|
confirm(`Delete inline policy ${policyName}? This action cannot be undone.`, async () => {
|
|
try {
|
|
await api.iamDeleteRolePolicy(currentRole.RoleName, policyName);
|
|
showToast('Policy deleted successfully', 'success');
|
|
loadRolePolicies();
|
|
} catch (error) {
|
|
showToast(iamErrorText(error, 'deleting policy'), 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============================================
|
|
// Delete
|
|
// ============================================
|
|
|
|
function openDeleteRoleModal(roleName) {
|
|
roleToDelete = roleName;
|
|
document.getElementById('delete-role-name').textContent = roleName;
|
|
openModal('delete-role-modal');
|
|
}
|
|
|
|
async function confirmDeleteRole() {
|
|
if (!roleToDelete) return;
|
|
const btn = document.getElementById('confirm-delete-role-btn');
|
|
setLoading(btn, true);
|
|
try {
|
|
await api.iamDeleteRole(roleToDelete);
|
|
showToast('Role deleted successfully', 'success');
|
|
closeModal('delete-role-modal');
|
|
roleToDelete = null;
|
|
loadRoles();
|
|
} catch (error) {
|
|
console.error('Error deleting role:', error);
|
|
showToast(iamErrorText(error, 'deleting role'), 'error');
|
|
} finally {
|
|
setLoading(btn, false);
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|