* Add IAM gRPC service definition - Add GetConfiguration/PutConfiguration for config management - Add CreateUser/GetUser/UpdateUser/DeleteUser/ListUsers for user management - Add CreateAccessKey/DeleteAccessKey/GetUserByAccessKey for access key management - Methods mirror existing IAM HTTP API functionality * Add IAM gRPC handlers on filer server - Implement IamGrpcServer with CredentialManager integration - Handle configuration get/put operations - Handle user CRUD operations - Handle access key create/delete operations - All methods delegate to CredentialManager for actual storage * Wire IAM gRPC service to filer server - Add CredentialManager field to FilerOption and FilerServer - Import credential store implementations in filer command - Initialize CredentialManager from credential.toml if available - Register IAM gRPC service on filer gRPC server - Enable credential management via gRPC alongside existing filer services * Regenerate IAM protobuf with gRPC service methods * fix: compilation error in DeleteUser * fix: address code review comments for IAM migration * feat: migrate policies to multi-file layout and fix identity duplicated content * refactor: remove configuration.json and migrate Service Accounts to multi-file layout * refactor: standardize Service Accounts as distinct store entities and fix Admin Server persistence * config: set ServiceAccountsDirectory to /etc/iam/service_accounts * Fix Chrome dialog auto-dismiss with Bootstrap modals - Add modal-alerts.js library with Bootstrap modal replacements - Replace all 15 confirm() calls with showConfirm/showDeleteConfirm - Auto-override window.alert() for all alert() calls - Fixes Chrome 132+ aggressively blocking native dialogs * Upgrade Bootstrap from 5.3.2 to 5.3.8 * Fix syntax error in object_store_users.templ - remove duplicate closing braces * create policy * display errors * migrate to multi-file policies * address PR feedback: use showDeleteConfirm and showErrorMessage in policies.templ, refine migration check * Update policies_templ.go * add service account to iam grpc * iam: fix potential path traversal in policy names by validating name pattern * iam: add GetServiceAccountByAccessKey to CredentialStore interface * iam: implement service account support for PostgresStore Includes full CRUD operations and efficient lookup by access key. * iam: implement GetServiceAccountByAccessKey for filer_etc, grpc, and memory stores Provides efficient lookup of service accounts by access key where possible, with linear scan fallbacks for file-based stores. * iam: remove filer_multiple support Deleted its implementation and references in imports, scaffold config, and core interface constants. Redundant with filer_etc. * clear comment * dash: robustify service account construction - Guard against nil sa.Credential when constructing responses - Fix Expiration logic to only set if > 0, avoiding Unix epoch 1970 - Ensure consistency across Get, Create, and Update handlers * credential/filer_etc: improve error propagation in configuration handlers - Return error from loadServiceAccountsFromMultiFile to callers - Ensure listEntries errors in SaveConfiguration (cleanup logic) are propagated unless they are "not found" failures. - Fixes potential silent failures during IAM configuration sync. * credential/filer_etc: add existence check to CreateServiceAccount Ensures consistency with other stores by preventing accidental overwrite of existing service accounts during creation. * credential/memory: improve store robustness and Reset logic - Enforce ID immutability in UpdateServiceAccount to prevent orphans - Update Reset() to also clear the policies map, ensuring full state cleanup for tests. * dash: improve service account robustness and policy docs - Wrap parent user lookup errors to preserve context - Strictly validate Status field in UpdateServiceAccount - Add deprecation comments to legacy policy management methods * credential/filer_etc: protect against path traversal in service accounts Implemented ID validation (alphanumeric, underscores, hyphens) and applied it to Get, Save, and Delete operations to ensure no directory traversal via saId.json filenames. * credential/postgres: improve robustness and cleanup comments - Removed brainstorming comments in GetServiceAccountByAccessKey - Added missing rows.Err() check during iteration - Properly propagate Scan and Unmarshal errors instead of swallowing them * admin: unify UI alerts and confirmations using Bootstrap modals - Updated modal-alerts.js with improved automated alert type detection - Replaced native alert() and confirm() with showAlert(), showConfirm(), and showDeleteConfirm() across various Templ components - Improved UX for delete operations by providing better context and styling - Ensured consistent error reporting across IAM and Maintenance views * admin: additional UI consistency fixes for alerts and confirmations - Replaced native alert() and confirm() with Bootstrap modals in: - EC volumes (repair flow) - Collection details (repair flow) - File browser (properties and delete) - Maintenance config schema (save and reset) - Improved delete confirmation in file browser with item context - Ensured consistent success/error/info styling for all feedbacks * make * iam: add GetServiceAccountByAccessKey RPC and update GetConfiguration * iam: implement GetServiceAccountByAccessKey on server and client * iam: centralize policy and service account validation * iam: optimize MemoryStore service account lookups with indexing * iam: fix postgres service_accounts table and optimize lookups * admin: refactor modal alerts and clean up dashboard logic * admin: fix EC shards table layout mismatch * admin: URL-encode IAM path parameters for safety * admin: implement pauseWorker logic in maintenance view * iam: add rows.Err() check to postgres ListServiceAccounts * iam: standardize ErrServiceAccountNotFound across credential stores * iam: map ErrServiceAccountNotFound to codes.NotFound in DeleteServiceAccount * iam: refine service account store logic, errors and schema * iam: add validation to GetServiceAccountByAccessKey * admin: refine modal titles and ensure URL safety * admin: address bot review comments for alerts and async usage * iam: fix syntax error by restoring missing function declaration * [FilerEtcStore] improve error handling in CreateServiceAccount Refine error handling to provide clearer messages when checking for existing service accounts. * [PostgresStore] add nil guards and validation to service account methods Ensure input parameters are not nil and required IDs are present to prevent runtime panics and ensure data integrity. * [JS] add shared IAM utility script Consolidate common IAM operations like deleteUser and deleteAccessKey into a shared utility script for better maintainability. * [View] include shared IAM utilities in layout Include iam-utils.js in the main layout to make IAM functions available across all administrative pages. * [View] refactor IAM logic and restore async in EC Shards view Remove redundant local IAM functions and ensure that delete confirmation callbacks are properly marked as async. * [View] consolidate IAM logic in Object Store Users view Remove redundant local definitions of deleteUser and deleteAccessKey, relying on the shared utilities instead. * [View] update generated templ files for UI consistency * credential/postgres: remove redundant name column from service_accounts table The id is already used as the unique identifier and was being copied to the name column. This removes the name column from the schema and updates the INSERT/UPDATE queries. * credential/filer_etc: improve logging for policy migration failures Added Errorf log if AtomicRenameEntry fails during migration to ensure visibility of common failure points. * credential: allow uppercase characters in service account ID username Updated ServiceAccountIdPattern to allow [A-Za-z0-9_-]+ for the username component, matching the actual service account creation logic which uses the parent user name directly. * Update object_store_users_templ.go * admin: fix ec_shards pagination to handle numeric page arguments Updated goToPage in cluster_ec_shards.templ to accept either an Event or a numeric page argument. This prevents errors when goToPage(1) is called directly. Corrected both the .templ source and generated Go code. * credential/filer_etc: improve service account storage robustness Added nil guard to saveServiceAccount, updated GetServiceAccount to return ErrServiceAccountNotFound for empty data, and improved deleteServiceAccount to handle response-level Filer errors.
675 lines
32 KiB
Plaintext
675 lines
32 KiB
Plaintext
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
|
)
|
|
|
|
templ ServiceAccounts(data dash.ServiceAccountsData) {
|
|
<div class="container-fluid">
|
|
<!-- Page Header -->
|
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
|
<div>
|
|
<h1 class="h3 mb-0 text-gray-800">
|
|
<i class="fas fa-robot me-2"></i>Service Accounts
|
|
</h1>
|
|
<p class="mb-0 text-muted">Manage application credentials for automated processes</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button type="button" class="btn btn-primary"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#createServiceAccountModal">
|
|
<i class="fas fa-plus me-1"></i>Create Service Account
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-xl-3 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 Service Accounts
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{fmt.Sprintf("%d", data.TotalAccounts)}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-id-card fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-3 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 Accounts
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{fmt.Sprintf("%d", data.ActiveAccounts)}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-check-circle fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-3 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="h6 mb-0 font-weight-bold text-gray-800">
|
|
{data.LastUpdated.Format("15:04")}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-clock fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Service Accounts 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="fas fa-robot me-2"></i>Service Accounts
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover" width="100%" cellspacing="0" id="serviceAccountsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Parent User</th>
|
|
<th>Access Key</th>
|
|
<th>Status</th>
|
|
<th>Created</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
for _, sa := range data.ServiceAccounts {
|
|
<tr>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<i class="fas fa-robot me-2 text-muted"></i>
|
|
<code>{sa.ID}</code>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<i class="fas fa-user me-1 text-muted"></i>
|
|
{sa.ParentUser}
|
|
</td>
|
|
<td>
|
|
<code class="text-muted">{sa.AccessKeyId}</code>
|
|
</td>
|
|
<td>
|
|
if sa.Status == "Active" {
|
|
<span class="badge bg-success">Active</span>
|
|
} else {
|
|
<span class="badge bg-secondary">Inactive</span>
|
|
}
|
|
</td>
|
|
<td>{sa.CreateDate.Format("2006-01-02")}</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm" role="group">
|
|
<button type="button" class="btn btn-outline-info"
|
|
data-action="show-sa-details" data-sa-id={ sa.ID }>
|
|
<i class="fas fa-info-circle"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary"
|
|
data-action="toggle-sa-status" data-sa-id={ sa.ID } data-current-status={ sa.Status }>
|
|
if sa.Status == "Active" {
|
|
<i class="fas fa-pause"></i>
|
|
} else {
|
|
<i class="fas fa-play"></i>
|
|
}
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger"
|
|
data-action="delete-sa" data-sa-id={ sa.ID }>
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
if len(data.ServiceAccounts) == 0 {
|
|
<tr>
|
|
<td colspan="6" class="text-center text-muted py-4">
|
|
<i class="fas fa-robot fa-3x mb-3 text-muted"></i>
|
|
<div>
|
|
<h5>No service accounts found</h5>
|
|
<p>Create your first service account for automated processes.</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Last Updated -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<small class="text-muted">
|
|
<i class="fas fa-clock me-1"></i>
|
|
Last updated: {data.LastUpdated.Format("2006-01-02 15:04:05")}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Service Account Modal -->
|
|
<div class="modal fade" id="createServiceAccountModal" tabindex="-1" role="dialog">
|
|
<div class="modal-dialog" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-plus me-2"></i>Create Service Account
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="createSAForm">
|
|
<div class="mb-3">
|
|
<label for="parentUser" class="form-label">Parent User *</label>
|
|
<select class="form-select" id="parentUser" name="parent_user" required>
|
|
<option value="">-- Select a user --</option>
|
|
for _, user := range data.AvailableUsers {
|
|
<option value={ user }>{ user }</option>
|
|
}
|
|
</select>
|
|
<small class="form-text text-muted">The service account will inherit permissions from this user</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="description" class="form-label">Description</label>
|
|
<textarea class="form-control" id="description" name="description" rows="2"
|
|
placeholder="What is this service account used for?"></textarea>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="expiration" class="form-label">Expiration (optional)</label>
|
|
<input type="datetime-local" class="form-control" id="expiration" name="expiration">
|
|
<small class="form-text text-muted">Leave empty for no expiration</small>
|
|
</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="handleCreateServiceAccount()">Create</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Service Account Details Modal -->
|
|
<div class="modal fade" id="saDetailsModal" tabindex="-1" role="dialog">
|
|
<div class="modal-dialog modal-lg" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-robot me-2"></i>Service Account Details
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="saDetailsContent">
|
|
<!-- Content will be loaded dynamically -->
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Credentials Display Modal -->
|
|
<div class="modal fade" id="credentialsModal" tabindex="-1" role="dialog" data-bs-backdrop="static" data-bs-keyboard="false">
|
|
<div class="modal-dialog modal-lg" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-success text-white">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-check-circle me-2"></i>Service Account Created Successfully
|
|
</h5>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-warning">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
<strong>Important:</strong> This is the only time you will see the secret access key. Please save it securely.
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<h6 class="text-muted mb-3">AWS CLI Configuration</h6>
|
|
<p class="text-muted small">Use these credentials to configure AWS CLI or SDKs:</p>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">AWS_ACCESS_KEY_ID</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control font-monospace" id="displayAccessKey" readonly>
|
|
<button class="btn btn-outline-secondary" type="button" onclick="copyCredentialToClipboard(this, 'displayAccessKey')">
|
|
<i class="fas fa-copy"></i> Copy
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">AWS_SECRET_ACCESS_KEY</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control font-monospace" id="displaySecretKey" readonly>
|
|
<button class="btn btn-outline-secondary" type="button" onclick="copyCredentialToClipboard(this, 'displaySecretKey')">
|
|
<i class="fas fa-copy"></i> Copy
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-light p-3 rounded">
|
|
<h6 class="text-muted mb-2">Example AWS CLI Usage:</h6>
|
|
<div class="font-monospace small">
|
|
<div>export AWS_ACCESS_KEY_ID=<span id="exampleAccessKey"></span></div>
|
|
<div>export AWS_SECRET_ACCESS_KEY=<span id="exampleSecretKey"></span></div>
|
|
<div>export AWS_ENDPOINT_URL=http://localhost:8333</div>
|
|
<div class="mt-2"># List buckets</div>
|
|
<div>aws s3 ls</div>
|
|
<div class="mt-2"># Upload a file</div>
|
|
<div>aws s3 cp myfile.txt s3://mybucket/</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-primary" onclick="closeCredentialsModal()">
|
|
<i class="fas fa-check me-1"></i>I have saved the credentials
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- JavaScript for service account management -->
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
document.addEventListener('click', function(e) {
|
|
const button = e.target.closest('[data-action]');
|
|
if (!button) return;
|
|
|
|
const action = button.getAttribute('data-action');
|
|
const saId = button.getAttribute('data-sa-id');
|
|
|
|
switch (action) {
|
|
case 'show-sa-details':
|
|
showSADetails(saId);
|
|
break;
|
|
case 'toggle-sa-status':
|
|
toggleSAStatus(saId, button.getAttribute('data-current-status'));
|
|
break;
|
|
case 'delete-sa':
|
|
deleteSA(saId);
|
|
break;
|
|
}
|
|
});
|
|
});
|
|
|
|
async function showSADetails(id) {
|
|
try {
|
|
const encodedId = encodeURIComponent(id);
|
|
const response = await fetch(`/api/service-accounts/${encodedId}`);
|
|
if (response.ok) {
|
|
const sa = await response.json();
|
|
document.getElementById('saDetailsContent').innerHTML = createSADetailsContent(sa);
|
|
const modal = new bootstrap.Modal(document.getElementById('saDetailsModal'));
|
|
modal.show();
|
|
} else {
|
|
showErrorMessage('Failed to load service account details');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading service account details:', error);
|
|
showErrorMessage('Failed to load service account details');
|
|
}
|
|
}
|
|
|
|
async function toggleSAStatus(id, currentStatus) {
|
|
const newStatus = currentStatus === 'Active' ? 'Inactive' : 'Active';
|
|
try {
|
|
const encodedId = encodeURIComponent(id);
|
|
const response = await fetch(`/api/service-accounts/${encodedId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ status: newStatus })
|
|
});
|
|
|
|
if (response.ok) {
|
|
showSuccessMessage(`Service account ${newStatus === 'Active' ? 'activated' : 'deactivated'}`);
|
|
setTimeout(() => window.location.reload(), 1000);
|
|
} else {
|
|
const error = await response.json();
|
|
showErrorMessage('Failed to update status: ' + (error.error || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating status:', error);
|
|
showErrorMessage('Failed to update status: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function deleteSA(id) {
|
|
showDeleteConfirm(id, async function() {
|
|
try {
|
|
const encodedId = encodeURIComponent(id);
|
|
const response = await fetch(`/api/service-accounts/${encodedId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (response.ok) {
|
|
showSuccessMessage('Service account deleted successfully');
|
|
setTimeout(() => window.location.reload(), 1000);
|
|
} else {
|
|
const error = await response.json();
|
|
showErrorMessage('Failed to delete: ' + (error.error || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting service account:', error);
|
|
showErrorMessage('Failed to delete: ' + error.message);
|
|
}
|
|
}, 'Are you sure you want to delete this service account? This action cannot be undone.');
|
|
}
|
|
|
|
async function handleCreateServiceAccount() {
|
|
const form = document.getElementById('createSAForm');
|
|
const formData = new FormData(form);
|
|
|
|
const saData = {
|
|
parent_user: formData.get('parent_user'),
|
|
description: formData.get('description')
|
|
};
|
|
|
|
// Handle expiration if set
|
|
const expiration = formData.get('expiration');
|
|
if (expiration) {
|
|
// Validate the date before using it
|
|
const date = new Date(expiration);
|
|
const now = new Date();
|
|
|
|
if (isNaN(date.getTime())) {
|
|
showErrorMessage('Invalid expiration date format');
|
|
return;
|
|
}
|
|
|
|
// Ensure expiration is in the future
|
|
if (date <= now) {
|
|
showErrorMessage('Expiration date must be in the future');
|
|
return;
|
|
}
|
|
|
|
saData.expiration = date.toISOString();
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/service-accounts', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(saData)
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
|
|
// Hide create modal
|
|
const createModal = bootstrap.Modal.getInstance(document.getElementById('createServiceAccountModal'));
|
|
createModal.hide();
|
|
form.reset();
|
|
|
|
// Show credentials if returned
|
|
if (result.service_account && result.service_account.secret_access_key) {
|
|
showCredentials(result.service_account);
|
|
} else {
|
|
showSuccessMessage('Service account created successfully');
|
|
setTimeout(() => window.location.reload(), 1000);
|
|
}
|
|
} else {
|
|
const error = await response.json();
|
|
showErrorMessage('Failed to create service account: ' + (error.error || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating service account:', error);
|
|
showErrorMessage('Failed to create service account: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function showCredentials(serviceAccount) {
|
|
// Populate the credentials modal
|
|
document.getElementById('displayAccessKey').value = serviceAccount.access_key_id;
|
|
document.getElementById('displaySecretKey').value = serviceAccount.secret_access_key;
|
|
document.getElementById('exampleAccessKey').textContent = serviceAccount.access_key_id;
|
|
document.getElementById('exampleSecretKey').textContent = serviceAccount.secret_access_key;
|
|
|
|
// Show the modal
|
|
const credentialsModal = new bootstrap.Modal(document.getElementById('credentialsModal'));
|
|
credentialsModal.show();
|
|
}
|
|
|
|
function closeCredentialsModal() {
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('credentialsModal'));
|
|
modal.hide();
|
|
// Reload to show the new service account in the list
|
|
setTimeout(() => window.location.reload(), 500);
|
|
}
|
|
|
|
function copyCredentialToClipboard(button, elementId) {
|
|
const element = document.getElementById(elementId);
|
|
const textToCopy = element.value;
|
|
|
|
// Use modern Clipboard API if available
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
navigator.clipboard.writeText(textToCopy).then(() => {
|
|
showSuccessMessage('Copied to clipboard!');
|
|
}).catch(err => {
|
|
console.warn('Clipboard API failed:', err);
|
|
// Fallback
|
|
fallbackCopyTextToClipboard(element);
|
|
});
|
|
} else {
|
|
// Fallback for older browsers or non-secure contexts
|
|
fallbackCopyTextToClipboard(element);
|
|
}
|
|
|
|
// Visual feedback
|
|
const originalHTML = button.innerHTML;
|
|
|
|
button.innerHTML = '<i class="fas fa-check"></i>';
|
|
button.classList.remove('btn-outline-secondary');
|
|
button.classList.add('btn-success');
|
|
|
|
setTimeout(() => {
|
|
button.innerHTML = originalHTML;
|
|
button.classList.remove('btn-success');
|
|
button.classList.add('btn-outline-secondary');
|
|
}, 1000);
|
|
}
|
|
|
|
function fallbackCopyTextToClipboard(element) {
|
|
element.select();
|
|
element.setSelectionRange(0, 99999); // For mobile devices
|
|
|
|
try {
|
|
const successful = document.execCommand('copy');
|
|
if (successful) {
|
|
showSuccessMessage('Copied to clipboard!');
|
|
} else {
|
|
showErrorMessage('Failed to copy to clipboard');
|
|
}
|
|
} catch (err) {
|
|
console.error('Fallback copy failed:', err);
|
|
showErrorMessage('Failed to copy to clipboard');
|
|
}
|
|
|
|
// Clear selection
|
|
window.getSelection().removeAllRanges();
|
|
}
|
|
|
|
|
|
function createSADetailsContent(sa) {
|
|
// Create DOM elements safely to prevent XSS
|
|
const container = document.createElement('div');
|
|
container.className = 'row';
|
|
|
|
// Basic Information column
|
|
const col1 = document.createElement('div');
|
|
col1.className = 'col-md-6';
|
|
|
|
const h6_1 = document.createElement('h6');
|
|
h6_1.className = 'text-muted';
|
|
h6_1.textContent = 'Basic Information';
|
|
col1.appendChild(h6_1);
|
|
|
|
const table1 = document.createElement('table');
|
|
table1.className = 'table table-sm';
|
|
|
|
// ID row
|
|
const idRow = document.createElement('tr');
|
|
idRow.innerHTML = '<td><strong>ID:</strong></td><td><code></code></td>';
|
|
idRow.querySelector('code').textContent = sa.id || '';
|
|
table1.appendChild(idRow);
|
|
|
|
// Parent User row
|
|
const parentRow = document.createElement('tr');
|
|
parentRow.innerHTML = '<td><strong>Parent User:</strong></td><td></td>';
|
|
parentRow.querySelectorAll('td')[1].textContent = sa.parent_user || '';
|
|
table1.appendChild(parentRow);
|
|
|
|
// Access Key row
|
|
const keyRow = document.createElement('tr');
|
|
keyRow.innerHTML = '<td><strong>Access Key:</strong></td><td><code></code></td>';
|
|
keyRow.querySelector('code').textContent = sa.access_key_id || '';
|
|
table1.appendChild(keyRow);
|
|
|
|
// Status row
|
|
const statusRow = document.createElement('tr');
|
|
const statusTd1 = document.createElement('td');
|
|
statusTd1.innerHTML = '<strong>Status:</strong>';
|
|
const statusTd2 = document.createElement('td');
|
|
const statusBadge = document.createElement('span');
|
|
statusBadge.className = sa.status === 'Active' ? 'badge bg-success' : 'badge bg-secondary';
|
|
statusBadge.textContent = sa.status || 'Unknown';
|
|
statusTd2.appendChild(statusBadge);
|
|
statusRow.appendChild(statusTd1);
|
|
statusRow.appendChild(statusTd2);
|
|
table1.appendChild(statusRow);
|
|
|
|
col1.appendChild(table1);
|
|
container.appendChild(col1);
|
|
|
|
// Details column
|
|
const col2 = document.createElement('div');
|
|
col2.className = 'col-md-6';
|
|
|
|
const h6_2 = document.createElement('h6');
|
|
h6_2.className = 'text-muted';
|
|
h6_2.textContent = 'Details';
|
|
col2.appendChild(h6_2);
|
|
|
|
const table2 = document.createElement('table');
|
|
table2.className = 'table table-sm';
|
|
|
|
// Description row
|
|
const descRow = document.createElement('tr');
|
|
descRow.innerHTML = '<td><strong>Description:</strong></td><td></td>';
|
|
descRow.querySelectorAll('td')[1].textContent = sa.description || 'Not set';
|
|
table2.appendChild(descRow);
|
|
|
|
// Created row
|
|
const createdRow = document.createElement('tr');
|
|
createdRow.innerHTML = '<td><strong>Created:</strong></td><td></td>';
|
|
try {
|
|
createdRow.querySelectorAll('td')[1].textContent = new Date(sa.create_date).toLocaleString();
|
|
} catch (e) {
|
|
createdRow.querySelectorAll('td')[1].textContent = 'Invalid date';
|
|
}
|
|
table2.appendChild(createdRow);
|
|
|
|
// Expiration row
|
|
const expRow = document.createElement('tr');
|
|
expRow.innerHTML = '<td><strong>Expires:</strong></td><td></td>';
|
|
expRow.querySelectorAll('td')[1].textContent = sa.expiration || 'Never';
|
|
table2.appendChild(expRow);
|
|
|
|
col2.appendChild(table2);
|
|
container.appendChild(col2);
|
|
|
|
return container.outerHTML;
|
|
}
|
|
|
|
|
|
function showSuccessMessage(message) {
|
|
showToast(message, 'success');
|
|
}
|
|
|
|
function showErrorMessage(message) {
|
|
showToast(message, 'danger');
|
|
}
|
|
|
|
function showToast(message, type) {
|
|
// Create toast container if it doesn't exist
|
|
let toastContainer = document.getElementById('toastContainer');
|
|
if (!toastContainer) {
|
|
toastContainer = document.createElement('div');
|
|
toastContainer.id = 'toastContainer';
|
|
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
|
|
toastContainer.style.zIndex = '9999';
|
|
document.body.appendChild(toastContainer);
|
|
}
|
|
|
|
// Create toast element
|
|
const toastId = 'toast-' + Date.now();
|
|
const toastHTML = `
|
|
<div id="${toastId}" class="toast align-items-center text-white bg-${type} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
|
<div class="d-flex">
|
|
<div class="toast-body">
|
|
${escapeHtml(message)}
|
|
</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
toastContainer.insertAdjacentHTML('beforeend', toastHTML);
|
|
const toastElement = document.getElementById(toastId);
|
|
const toast = new bootstrap.Toast(toastElement, { autohide: true, delay: 5000 });
|
|
toast.show();
|
|
|
|
// Remove toast element after it's hidden
|
|
toastElement.addEventListener('hidden.bs.toast', () => {
|
|
toastElement.remove();
|
|
});
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
</script>
|
|
}
|