Created
July 24, 2025 23:45
-
-
Save RajChowdhury240/a6fab24d8013cb5477abc854792bfee7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState, useEffect } from 'react'; | |
import { ChevronDown, ChevronUp, Plus, Trash2, Check, Clock, AlertCircle, User, Shield, Globe } from 'lucide-react'; | |
const IAMRoleApp = () => { | |
const [activeTab, setActiveTab] = useState('create'); | |
const [formData, setFormData] = useState({ | |
roleName: '', | |
description: '', | |
trustRelationship: { | |
type: 'service', | |
principals: [], | |
conditions: [] | |
}, | |
permissions: { | |
services: [], | |
actions: [], | |
resources: ['*'] | |
}, | |
tags: [], | |
permissionBoundary: '', | |
deploymentMethod: 'terraform', | |
crossAccount: { | |
enabled: false, | |
targetAccount: '', | |
externalId: '' | |
}, | |
oktaIntegration: { | |
enabled: false, | |
groups: [] | |
}, | |
approval: { | |
urgency: 'normal', | |
justification: '', | |
businessNeed: '', | |
approvalSystem: 'servicenow' | |
} | |
}); | |
const [expandedSections, setExpandedSections] = useState({ | |
basic: true, | |
trust: false, | |
permissions: false, | |
crossAccount: false, | |
okta: false, | |
approval: false | |
}); | |
const [requests, setRequests] = useState([ | |
{ | |
id: 'REQ-001', | |
roleName: 'DataAnalyst-CrossAccount', | |
status: 'pending_security', | |
submittedBy: '[email protected]', | |
submittedAt: '2025-01-20T10:30:00Z', | |
approvers: ['security-team', 'manager'], | |
ticketId: 'INC0012345', | |
ticketSystem: 'servicenow' | |
}, | |
{ | |
id: 'REQ-002', | |
roleName: 'LambdaExecutionRole', | |
status: 'approved', | |
submittedBy: '[email protected]', | |
submittedAt: '2025-01-19T14:15:00Z', | |
approvers: ['manager'], | |
ticketId: 'PROJ-456', | |
ticketSystem: 'jira' | |
} | |
]); | |
const awsServices = [ | |
{ name: 'ec2', label: 'Amazon EC2' }, | |
{ name: 'lambda', label: 'AWS Lambda' }, | |
{ name: 's3', label: 'Amazon S3' }, | |
{ name: 'rds', label: 'Amazon RDS' }, | |
{ name: 'dynamodb', label: 'Amazon DynamoDB' }, | |
{ name: 'iam', label: 'AWS IAM' }, | |
{ name: 'cloudwatch', label: 'Amazon CloudWatch' } | |
]; | |
const principalTypes = [ | |
{ value: 'service', label: 'AWS Service' }, | |
{ value: 'account', label: 'AWS Account' }, | |
{ value: 'federated', label: 'Federated (Okta)' }, | |
{ value: 'user', label: 'IAM User' } | |
]; | |
const oktaGroups = [ | |
'DevOps-Team', | |
'Data-Engineers', | |
'Security-Analysts', | |
'Platform-Engineering', | |
'ML-Engineers' | |
]; | |
const toggleSection = (section) => { | |
setExpandedSections(prev => ({ | |
...prev, | |
[section]: !prev[section] | |
})); | |
}; | |
const addPrincipal = () => { | |
setFormData(prev => ({ | |
...prev, | |
trustRelationship: { | |
...prev.trustRelationship, | |
principals: [...prev.trustRelationship.principals, { type: 'service', value: '' }] | |
} | |
})); | |
}; | |
const removePrincipal = (index) => { | |
setFormData(prev => ({ | |
...prev, | |
trustRelationship: { | |
...prev.trustRelationship, | |
principals: prev.trustRelationship.principals.filter((_, i) => i !== index) | |
} | |
})); | |
}; | |
const addTag = () => { | |
setFormData(prev => ({ | |
...prev, | |
tags: [...prev.tags, { key: '', value: '' }] | |
})); | |
}; | |
const removeTag = (index) => { | |
setFormData(prev => ({ | |
...prev, | |
tags: prev.tags.filter((_, i) => i !== index) | |
})); | |
}; | |
const handleSubmit = (e) => { | |
e.preventDefault(); | |
console.log('Submitting IAM role request:', formData); | |
// Here you would send the data to your backend | |
alert('IAM Role request submitted for approval!'); | |
}; | |
const getStatusIcon = (status) => { | |
switch (status) { | |
case 'pending_security': | |
case 'pending_manager': | |
return <Clock className="w-4 h-4 text-yellow-500" />; | |
case 'approved': | |
return <Check className="w-4 h-4 text-green-500" />; | |
case 'rejected': | |
return <AlertCircle className="w-4 h-4 text-red-500" />; | |
default: | |
return <Clock className="w-4 h-4 text-gray-500" />; | |
} | |
}; | |
const SectionHeader = ({ title, section, icon: Icon }) => ( | |
<div | |
className="flex items-center justify-between p-4 bg-gray-50 border-b cursor-pointer hover:bg-gray-100" | |
onClick={() => toggleSection(section)} | |
> | |
<div className="flex items-center space-x-2"> | |
<Icon className="w-5 h-5 text-blue-600" /> | |
<h3 className="font-medium text-gray-900">{title}</h3> | |
</div> | |
{expandedSections[section] ? | |
<ChevronUp className="w-5 h-5 text-gray-400" /> : | |
<ChevronDown className="w-5 h-5 text-gray-400" /> | |
} | |
</div> | |
); | |
return ( | |
<div className="min-h-screen bg-gray-100"> | |
<div className="bg-white shadow-sm border-b"> | |
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
<div className="flex items-center justify-between h-16"> | |
<h1 className="text-xl font-semibold text-gray-900">IAM Role Self-Service Portal</h1> | |
<div className="flex space-x-4"> | |
<button | |
onClick={() => setActiveTab('create')} | |
className={`px-4 py-2 rounded-md ${activeTab === 'create' ? 'bg-blue-600 text-white' : 'text-gray-600 hover:text-gray-900'}`} | |
> | |
Create Role | |
</button> | |
<button | |
onClick={() => setActiveTab('requests')} | |
className={`px-4 py-2 rounded-md ${activeTab === 'requests' ? 'bg-blue-600 text-white' : 'text-gray-600 hover:text-gray-900'}`} | |
> | |
My Requests | |
</button> | |
<button | |
onClick={() => setActiveTab('approvals')} | |
className={`px-4 py-2 rounded-md ${activeTab === 'approvals' ? 'bg-blue-600 text-white' : 'text-gray-600 hover:text-gray-900'}`} | |
> | |
Pending Approvals | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> | |
{activeTab === 'create' && ( | |
<div className="bg-white rounded-lg shadow"> | |
<div className="p-6 border-b"> | |
<h2 className="text-lg font-medium text-gray-900">Create New IAM Role</h2> | |
<p className="mt-1 text-sm text-gray-600">Fill out the form below to request a new IAM role. All requests require approval.</p> | |
</div> | |
<div> | |
{/* Basic Information */} | |
<div className="border-b"> | |
<SectionHeader title="Basic Information" section="basic" icon={Shield} /> | |
{expandedSections.basic && ( | |
<div className="p-6 space-y-4"> | |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-2">Role Name *</label> | |
<input | |
type="text" | |
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" | |
value={formData.roleName} | |
onChange={(e) => setFormData(prev => ({ ...prev, roleName: e.target.value }))} | |
placeholder="e.g., DataAnalyst-ReadOnly" | |
required | |
/> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-2">Deployment Method</label> | |
<select | |
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" | |
value={formData.deploymentMethod} | |
onChange={(e) => setFormData(prev => ({ ...prev, deploymentMethod: e.target.value }))} | |
> | |
<option value="terraform">Terraform</option> | |
<option value="cloudformation">CloudFormation</option> | |
</select> | |
</div> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-2">Description</label> | |
<textarea | |
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" | |
rows={3} | |
value={formData.description} | |
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))} | |
placeholder="Describe the purpose of this role..." | |
/> | |
</div> | |
</div> | |
)} | |
</div> | |
{/* Cross-Account Configuration */} | |
<div className="border-b"> | |
<SectionHeader title="Cross-Account Configuration" section="crossAccount" icon={Globe} /> | |
{expandedSections.crossAccount && ( | |
<div className="p-6 space-y-4"> | |
<div className="flex items-center"> | |
<input | |
type="checkbox" | |
id="crossAccount" | |
checked={formData.crossAccount.enabled} | |
onChange={(e) => setFormData(prev => ({ | |
...prev, | |
crossAccount: { ...prev.crossAccount, enabled: e.target.checked } | |
}))} | |
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" | |
/> | |
<label htmlFor="crossAccount" className="ml-2 text-sm text-gray-700"> | |
Enable cross-account role access | |
</label> | |
</div> | |
{formData.crossAccount.enabled && ( | |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4"> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-2">Target Account ID</label> | |
<input | |
type="text" | |
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" | |
value={formData.crossAccount.targetAccount} | |
onChange={(e) => setFormData(prev => ({ | |
...prev, | |
crossAccount: { ...prev.crossAccount, targetAccount: e.target.value } | |
}))} | |
placeholder="123456789012" | |
/> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-2">External ID (Optional)</label> | |
<input | |
type="text" | |
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" | |
value={formData.crossAccount.externalId} | |
onChange={(e) => setFormData(prev => ({ | |
...prev, | |
crossAccount: { ...prev.crossAccount, externalId: e.target.value } | |
}))} | |
placeholder="unique-external-id" | |
/> | |
</div> | |
</div> | |
)} | |
</div> | |
)} | |
</div> | |
{/* Trust Relationship */} | |
<div className="border-b"> | |
<SectionHeader title="Trust Relationship" section="trust" icon={User} /> | |
{expandedSections.trust && ( | |
<div className="p-6 space-y-4"> | |
<div className="flex items-center justify-between"> | |
<h4 className="text-sm font-medium text-gray-700">Principals</h4> | |
<button | |
type="button" | |
onClick={addPrincipal} | |
className="inline-flex items-center px-3 py-1 border border-transparent text-sm leading-4 font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200" | |
> | |
<Plus className="w-4 h-4 mr-1" /> | |
Add Principal | |
</button> | |
</div> | |
{formData.trustRelationship.principals.map((principal, index) => ( | |
<div key={index} className="grid grid-cols-1 md:grid-cols-3 gap-4 items-end"> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-2">Type</label> | |
<select | |
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" | |
value={principal.type} | |
onChange={(e) => { | |
const newPrincipals = [...formData.trustRelationship.principals]; | |
newPrincipals[index].type = e.target.value; | |
setFormData(prev => ({ | |
...prev, | |
trustRelationship: { ...prev.trustRelationship, principals: newPrincipals } | |
})); | |
}} | |
> | |
{principalTypes.map(type => ( | |
<option key={type.value} value={type.value}>{type.label}</option> | |
))} | |
</select> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-2">Value</label> | |
<input | |
type="text" | |
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" | |
value={principal.value} | |
onChange={(e) => { | |
const newPrincipals = [...formData.trustRelationship.principals]; | |
newPrincipals[index].value = e.target.value; | |
setFormData(prev => ({ | |
...prev, | |
trustRelationship: { ...prev.trustRelationship, principals: newPrincipals } | |
})); | |
}} | |
placeholder={principal.type === 'service' ? 'ec2.amazonaws.com' : 'arn:aws:iam::account:root'} | |
/> | |
</div> | |
<div> | |
<button | |
type="button" | |
onClick={() => removePrincipal(index)} | |
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200" | |
> | |
<Trash2 className="w-4 h-4" /> | |
</button> | |
</div> | |
</div> | |
))} | |
</div> | |
)} | |
</div> | |
{/* Okta Integration */} | |
<div className="border-b"> | |
<SectionHeader title="Okta Integration" section="okta" icon={User} /> | |
{expandedSections.okta && ( | |
<div className="p-6 space-y-4"> | |
<div className="flex items-center"> | |
<input | |
type="checkbox" | |
id="oktaIntegration" | |
checked={formData.oktaIntegration.enabled} | |
onChange={(e) => setFormData(prev => ({ | |
...prev, | |
oktaIntegration: { ...prev.oktaIntegration, enabled: e.target.checked } | |
}))} | |
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" | |
/> | |
<label htmlFor="oktaIntegration" className="ml-2 text-sm text-gray-700"> | |
Enable Okta group-based access | |
</label> | |
</div> | |
{formData.oktaIntegration.enabled && ( | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-2">Authorized Okta Groups</label> | |
<div className="space-y-2"> | |
{oktaGroups.map(group => ( | |
<label key={group} className="inline-flex items-center mr-4"> | |
<input | |
type="checkbox" | |
checked={formData.oktaIntegration.groups.includes(group)} | |
onChange={(e) => { | |
const newGroups = e.target.checked | |
? [...formData.oktaIntegration.groups, group] | |
: formData.oktaIntegration.groups.filter(g => g !== group); | |
setFormData(prev => ({ | |
...prev, | |
oktaIntegration: { ...prev.oktaIntegration, groups: newGroups } | |
})); | |
}} | |
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" | |
/> | |
<span className="ml-2 text-sm text-gray-700">{group}</span> | |
</label> | |
))} | |
</div> | |
</div> | |
)} | |
</div> | |
)} | |
</div> | |
{/* Permissions */} | |
<div className="border-b"> | |
<SectionHeader title="Permissions" section="permissions" icon={Shield} /> | |
{expandedSections.permissions && ( | |
<div className="p-6 space-y-4"> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-2">AWS Services</label> | |
<div className="grid grid-cols-2 md:grid-cols-3 gap-2"> | |
{awsServices.map(service => ( | |
<label key={service.name} className="inline-flex items-center"> | |
<input | |
type="checkbox" | |
checked={formData.permissions.services.includes(service.name)} | |
onChange={(e) => { | |
const newServices = e.target.checked | |
? [...formData.permissions.services, service.name] | |
: formData.permissions.services.filter(s => s !== service.name); | |
setFormData(prev => ({ | |
...prev, | |
permissions: { ...prev.permissions, services: newServices } | |
})); | |
}} | |
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" | |
/> | |
<span className="ml-2 text-sm text-gray-700">{service.label}</span> | |
</label> | |
))} | |
</div> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-2">Permission Boundary (Optional)</label> | |
<input | |
type="text" | |
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" | |
value={formData.permissionBoundary} | |
onChange={(e) => setFormData(prev => ({ ...prev, permissionBoundary: e.target.value }))} | |
placeholder="arn:aws:iam::account:policy/BoundaryPolicy" | |
/> | |
</div> | |
</div> | |
)} | |
</div> | |
{/* Approval & Justification */} | |
<div className="border-b"> | |
<SectionHeader title="Approval & Justification" section="approval" icon={Check} /> | |
{expandedSections.approval && ( | |
<div className="p-6 space-y-4"> | |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-2">Approval System</label> | |
<select | |
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" | |
value={formData.approval.approvalSystem} | |
onChange={(e) => setFormData(prev => ({ | |
...prev, | |
approval: { ...prev.approval, approvalSystem: e.target.value } | |
}))} | |
> | |
<option value="servicenow">ServiceNow</option> | |
<option value="jira">Jira</option> | |
</select> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-2">Urgency</label> | |
<select | |
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" | |
value={formData.approval.urgency} | |
onChange={(e) => setFormData(prev => ({ | |
...prev, | |
approval: { ...prev.approval, urgency: e.target.value } | |
}))} | |
> | |
<option value="low">Low</option> | |
<option value="normal">Normal</option> | |
<option value="high">High</option> | |
<option value="emergency">Emergency</option> | |
</select> | |
</div> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-2">Business Justification *</label> | |
<textarea | |
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" | |
rows={3} | |
value={formData.approval.businessNeed} | |
onChange={(e) => setFormData(prev => ({ | |
...prev, | |
approval: { ...prev.approval, businessNeed: e.target.value } | |
}))} | |
placeholder="Explain the business need for this role..." | |
required | |
/> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-2">Technical Justification</label> | |
<textarea | |
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" | |
rows={3} | |
value={formData.approval.justification} | |
onChange={(e) => setFormData(prev => ({ | |
...prev, | |
approval: { ...prev.approval, justification: e.target.value } | |
}))} | |
placeholder="Provide technical details about how this role will be used..." | |
/> | |
</div> | |
</div> | |
)} | |
</div> | |
<div className="p-6"> | |
<button | |
type="button" | |
onClick={handleSubmit} | |
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" | |
> | |
Submit for Approval | |
</button> | |
</div> | |
</div> | |
</div> | |
)} | |
{activeTab === 'requests' && ( | |
<div className="bg-white rounded-lg shadow"> | |
<div className="p-6 border-b"> | |
<h2 className="text-lg font-medium text-gray-900">My Role Requests</h2> | |
</div> | |
<div className="overflow-x-auto"> | |
<table className="min-w-full divide-y divide-gray-200"> | |
<thead className="bg-gray-50"> | |
<tr> | |
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Request ID</th> | |
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Role Name</th> | |
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th> | |
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ticket</th> | |
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Submitted</th> | |
</tr> | |
</thead> | |
<tbody className="bg-white divide-y divide-gray-200"> | |
{requests.map((request) => ( | |
<tr key={request.id}> | |
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{request.id}</td> | |
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{request.roleName}</td> | |
<td className="px-6 py-4 whitespace-nowrap"> | |
<div className="flex items-center"> | |
{getStatusIcon(request.status)} | |
<span className="ml-2 text-sm text-gray-900 capitalize">{request.status.replace('_', ' ')}</span> | |
</div> | |
</td> | |
<td className="px-6 py-4 whitespace-nowrap"> | |
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"> | |
{request.ticketSystem.toUpperCase()}: {request.ticketId} | |
</span> | |
</td> | |
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> | |
{new Date(request.submittedAt).toLocaleDateString()} | |
</td> | |
</tr> | |
))} | |
</tbody> | |
</table> | |
</div> | |
</div> | |
)} | |
{activeTab === 'approvals' && ( | |
<div className="bg-white rounded-lg shadow"> | |
<div className="p-6 border-b"> | |
<h2 className="text-lg font-medium text-gray-900">Pending Approvals</h2> | |
<p className="mt-1 text-sm text-gray-600">IAM role requests awaiting your approval</p> | |
</div> | |
<div className="p-6"> | |
<div className="text-center py-8"> | |
<Clock className="mx-auto h-12 w-12 text-gray-400" /> | |
<h3 className="mt-2 text-sm font-medium text-gray-900">No pending approvals</h3> | |
<p className="mt-1 text-sm text-gray-500">All requests have been processed.</p> | |
</div> | |
</div> | |
</div> | |
)} | |
</div> | |
</div> | |
); | |
}; | |
export default IAMRoleApp; |
Author
RajChowdhury240
commented
Jul 24, 2025
# Create a new React app
npx create-react-app iam-role-webapp
cd iam-role-webapp
# Install additional dependencies (Lucide React for icons)
npm install lucide-react
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment