feat: Implement user and policy management UI
- Add comprehensive user management with CRUD operations - Implement policy management with templates and custom JSON - Create reusable components (ConfirmDialog, LoadingState, EmptyState) - Add user creation with password validation and credential copying - Implement policy creation with built-in templates - Add policy attachment to users functionality - Include search, filtering, and status management - Add proper error handling and validation - Implement Material-UI based responsive design 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
import axios from 'axios';
|
||||
import { getAuthHeaders } from '../utils/auth';
|
||||
|
||||
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8080';
|
||||
|
||||
export interface Policy {
|
||||
name: string;
|
||||
policy: string;
|
||||
type?: 'built-in' | 'custom';
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface CreatePolicyRequest {
|
||||
name: string;
|
||||
policy: string;
|
||||
}
|
||||
|
||||
export interface AttachPolicyRequest {
|
||||
policyName: string;
|
||||
userName: string;
|
||||
}
|
||||
|
||||
export interface PolicyListResponse {
|
||||
policies: Policy[];
|
||||
}
|
||||
|
||||
export interface PolicyTemplate {
|
||||
name: string;
|
||||
description: string;
|
||||
generatePolicy: (bucketName: string) => object;
|
||||
}
|
||||
|
||||
export const policyTemplates: Record<string, PolicyTemplate> = {
|
||||
bucketFullAccess: {
|
||||
name: 'Bucket Full Access',
|
||||
description: 'Grants full access to a specific bucket',
|
||||
generatePolicy: (bucketName: string) => ({
|
||||
Version: '2012-10-17',
|
||||
Statement: [
|
||||
{
|
||||
Effect: 'Allow',
|
||||
Action: ['s3:*'],
|
||||
Resource: [
|
||||
`arn:aws:s3:::${bucketName}`,
|
||||
`arn:aws:s3:::${bucketName}/*`
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
bucketReadOnly: {
|
||||
name: 'Bucket Read Only',
|
||||
description: 'Grants read-only access to a specific bucket',
|
||||
generatePolicy: (bucketName: string) => ({
|
||||
Version: '2012-10-17',
|
||||
Statement: [
|
||||
{
|
||||
Effect: 'Allow',
|
||||
Action: [
|
||||
's3:GetObject',
|
||||
's3:ListBucket',
|
||||
's3:GetBucketLocation',
|
||||
],
|
||||
Resource: [
|
||||
`arn:aws:s3:::${bucketName}`,
|
||||
`arn:aws:s3:::${bucketName}/*`
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
bucketWriteOnly: {
|
||||
name: 'Bucket Write Only',
|
||||
description: 'Grants write-only access to a specific bucket',
|
||||
generatePolicy: (bucketName: string) => ({
|
||||
Version: '2012-10-17',
|
||||
Statement: [
|
||||
{
|
||||
Effect: 'Allow',
|
||||
Action: [
|
||||
's3:PutObject',
|
||||
's3:DeleteObject',
|
||||
's3:ListBucket',
|
||||
],
|
||||
Resource: [
|
||||
`arn:aws:s3:::${bucketName}`,
|
||||
`arn:aws:s3:::${bucketName}/*`
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
class PolicyService {
|
||||
private baseURL = `${API_BASE_URL}/api`;
|
||||
|
||||
async listPolicies(): Promise<Policy[]> {
|
||||
try {
|
||||
const response = await axios.get<PolicyListResponse>(`${this.baseURL}/policies`, {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
return response.data.policies || [];
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new Error(error.response?.data?.error || 'Failed to fetch policies');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createPolicy(policyData: CreatePolicyRequest): Promise<void> {
|
||||
try {
|
||||
await axios.post(`${this.baseURL}/policies`, policyData, {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new Error(error.response?.data?.error || 'Failed to create policy');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deletePolicy(policyName: string): Promise<void> {
|
||||
try {
|
||||
await axios.delete(`${this.baseURL}/policies/${policyName}`, {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new Error(error.response?.data?.error || 'Failed to delete policy');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getPolicy(policyName: string): Promise<Policy> {
|
||||
try {
|
||||
const response = await axios.get<Policy>(`${this.baseURL}/policies/${policyName}`, {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new Error(error.response?.data?.error || 'Failed to fetch policy');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async attachPolicy(data: AttachPolicyRequest): Promise<void> {
|
||||
try {
|
||||
await axios.post(`${this.baseURL}/policies/attach`, data, {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new Error(error.response?.data?.error || 'Failed to attach policy');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async detachPolicy(policyName: string, userName: string): Promise<void> {
|
||||
try {
|
||||
await axios.post(`${this.baseURL}/policies/detach`,
|
||||
{ policyName, userName },
|
||||
{
|
||||
headers: getAuthHeaders(),
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new Error(error.response?.data?.error || 'Failed to detach policy');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Policy validation helper
|
||||
validatePolicyJSON(policyString: string): { valid: boolean; error?: string } {
|
||||
try {
|
||||
const policy = JSON.parse(policyString);
|
||||
|
||||
// Basic validation
|
||||
if (!policy.Version) {
|
||||
return { valid: false, error: 'Policy must have a Version field' };
|
||||
}
|
||||
if (!policy.Statement || !Array.isArray(policy.Statement)) {
|
||||
return { valid: false, error: 'Policy must have a Statement array' };
|
||||
}
|
||||
if (policy.Statement.length === 0) {
|
||||
return { valid: false, error: 'Policy must have at least one statement' };
|
||||
}
|
||||
|
||||
// Validate each statement
|
||||
for (let i = 0; i < policy.Statement.length; i++) {
|
||||
const stmt = policy.Statement[i];
|
||||
if (!stmt.Effect || !['Allow', 'Deny'].includes(stmt.Effect)) {
|
||||
return { valid: false, error: `Statement ${i + 1}: Effect must be 'Allow' or 'Deny'` };
|
||||
}
|
||||
if (!stmt.Action) {
|
||||
return { valid: false, error: `Statement ${i + 1}: Action is required` };
|
||||
}
|
||||
if (!stmt.Resource) {
|
||||
return { valid: false, error: `Statement ${i + 1}: Resource is required` };
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
} catch (e) {
|
||||
return { valid: false, error: 'Invalid JSON format' };
|
||||
}
|
||||
}
|
||||
|
||||
// Generate policy from template
|
||||
generatePolicyFromTemplate(templateKey: string, bucketName: string): string {
|
||||
const template = policyTemplates[templateKey];
|
||||
if (!template) {
|
||||
throw new Error('Invalid template');
|
||||
}
|
||||
const policy = template.generatePolicy(bucketName);
|
||||
return JSON.stringify(policy, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
const policyService = new PolicyService();
|
||||
export default policyService;
|
||||
Reference in New Issue
Block a user