55983d0088
- 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>
229 lines
6.2 KiB
TypeScript
229 lines
6.2 KiB
TypeScript
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; |