-
Notifications
You must be signed in to change notification settings - Fork 0
Closed
Labels
Description
Part of: #477
Story: User Management via MCP
Feature Group: F9 - User Management via MCP
📖 Story Overview
User Story
As an admin using Claude, I want to manage team users and their permissions so that I can control access to the CIDX server without direct database access.
Objective
Implement admin-only MCP tools for managing users, including creation, role updates, and deletion, with proper validation and security controls through Claude.
User Value
- 👥 Manage team access through Claude
- 🔐 Control user permissions easily
- 📊 View user activity and status
- 🚫 Revoke access when needed
- 📝 Audit trail for compliance
✅ Acceptance Criteria
Feature: User Management via MCP
Background:
Given user has ADMIN role
And MCP connection established
And UserManager service available
Scenario: List all users
When Claude calls list_users tool
Then response includes all users:
| username | User login name |
| email | User email address |
| role | Current role |
| created_at | Account creation date |
| last_login | Last authentication |
| is_active | Account status |
| repositories_activated | Count of activated repos |
And users sorted by username
And includes usage statistics
Scenario: Filter users by criteria
When Claude calls list_users:
"""
{
"filter": "inactive",
"role": "VIEWER"
}
"""
Then only inactive VIEWER users returned
And filter options include:
| active/inactive | Activity status |
| role | Specific role |
| has_repositories | Has activated repos |
| created_after | Recent accounts |
Scenario: Create new user
When Claude calls create_user:
"""
{
"username": "john.doe",
"email": "john.doe@example.com",
"password": "SecureP@ssw0rd!",
"role": "POWER_USER",
"send_welcome": true
}
"""
Then user account created
And password complexity validated
And role assigned correctly
And welcome email queued if requested
And response includes:
| user_id | Generated user ID |
| username | Confirmed username |
| role | Assigned role |
| temporary_password | If generated |
Scenario: Auto-generate secure password
When Claude calls create_user without password:
"""
{
"username": "jane.smith",
"email": "jane@example.com",
"role": "VIEWER",
"auto_password": true
}
"""
Then secure password generated
And password meets complexity requirements
And temporary password returned
And user marked for password reset
Scenario: Update user role
Given user "developer1" has VIEWER role
When Claude calls update_user_role:
"""
{
"username": "developer1",
"new_role": "POWER_USER"
}
"""
Then user role updated to POWER_USER
And permissions refreshed
And active sessions updated
And audit log created
And response confirms change
Scenario: Deactivate user account
When Claude calls delete_user:
"""
{
"username": "former.employee",
"soft_delete": true
}
"""
Then user account deactivated
And login disabled
And active sessions terminated
And repositories remain for audit
And can be reactivated later
Scenario: Hard delete user
When Claude calls delete_user:
"""
{
"username": "test.account",
"soft_delete": false,
"cleanup_data": true
}
"""
Then user permanently deleted
And all user workspaces removed
And activated repositories cleaned
And audit trail preserved
And cannot be recovered
Scenario: Handle duplicate username
Given "existing.user" already exists
When Claude attempts to create same username
Then error indicates duplicate
And suggests alternative username
And no changes made
Scenario: Validate email format
When Claude creates user with invalid email:
"""
{
"username": "newuser",
"email": "not-an-email"
}
"""
Then validation error returned
And email format requirements shown
And user not created
Scenario: Password complexity validation
When Claude creates user with weak password:
"""
{
"username": "weakuser",
"password": "12345"
}
"""
Then password rejected
And complexity requirements listed:
| Minimum 8 characters |
| Uppercase letter required |
| Lowercase letter required |
| Number required |
| Special character required |
Scenario: Bulk user operations
When Claude calls create_user with multiple users:
"""
{
"users": [
{"username": "user1", "email": "user1@example.com"},
{"username": "user2", "email": "user2@example.com"},
{"username": "user3", "email": "user3@example.com"}
],
"default_role": "VIEWER",
"auto_password": true
}
"""
Then all users created
And passwords generated for each
And summary report returned
And failures don't affect others
Scenario: View user details
When Claude calls list_users:
"""
{
"username": "specific.user",
"include_details": true
}
"""
Then detailed information returned:
| Active repositories |
| Recent activity |
| Permission list |
| Session count |
| Storage usage |
Scenario: Reset user password
When Claude calls update_user_role:
"""
{
"username": "forgetful.user",
"reset_password": true
}
"""
Then new temporary password generated
And user marked for password change
And active sessions terminated
And temporary password returned🔧 Technical Requirements
Tool Definitions
# pseudocode tool registry entries
"list_users": {
"description": "List and filter system users (ADMIN only)",
"parameters": {
"type": "object",
"properties": {
"username": {
"type": "string",
"description": "Specific username to retrieve"
},
"filter": {
"type": "string",
"enum": ["active", "inactive", "all"],
"default": "all"
},
"role": {
"type": "string",
"enum": ["ADMIN", "POWER_USER", "VIEWER"],
"description": "Filter by role"
},
"has_repositories": {
"type": "boolean",
"description": "Filter by repository activation"
},
"include_details": {
"type": "boolean",
"default": false,
"description": "Include detailed information"
},
"sort_by": {
"type": "string",
"enum": ["username", "created_at", "last_login"],
"default": "username"
}
}
},
"required_permission": Permissions.ADMIN
}
"create_user": {
"description": "Create new user account (ADMIN only)",
"parameters": {
"type": "object",
"properties": {
"username": {
"type": "string",
"pattern": "^[a-zA-Z0-9._-]+$",
"minLength": 3,
"maxLength": 32
},
"email": {
"type": "string",
"format": "email"
},
"password": {
"type": "string",
"minLength": 8
},
"auto_password": {
"type": "boolean",
"default": false,
"description": "Generate secure password"
},
"role": {
"type": "string",
"enum": ["ADMIN", "POWER_USER", "VIEWER"],
"default": "VIEWER"
},
"send_welcome": {
"type": "boolean",
"default": false,
"description": "Send welcome email"
},
"users": {
"type": "array",
"description": "Bulk create multiple users",
"items": {
"type": "object",
"properties": {
"username": {"type": "string"},
"email": {"type": "string"}
}
}
}
}
},
"required_permission": Permissions.ADMIN
}
"update_user_role": {
"description": "Update user role or reset password (ADMIN only)",
"parameters": {
"type": "object",
"properties": {
"username": {
"type": "string",
"description": "User to update"
},
"new_role": {
"type": "string",
"enum": ["ADMIN", "POWER_USER", "VIEWER"],
"description": "New role to assign"
},
"reset_password": {
"type": "boolean",
"default": false,
"description": "Generate new password"
}
},
"required": ["username"]
},
"required_permission": Permissions.ADMIN
}
"delete_user": {
"description": "Delete or deactivate user account (ADMIN only)",
"parameters": {
"type": "object",
"properties": {
"username": {
"type": "string",
"description": "User to delete"
},
"soft_delete": {
"type": "boolean",
"default": true,
"description": "Deactivate vs permanent delete"
},
"cleanup_data": {
"type": "boolean",
"default": false,
"description": "Remove user data"
},
"transfer_to": {
"type": "string",
"description": "Transfer repositories to another user"
}
},
"required": ["username"]
},
"required_permission": Permissions.ADMIN
}List Users Implementation
# pseudocode list users handler
async def handle_list_users(params: dict, user: User) -> dict:
# Verify admin role
if not user.has_permission(Permissions.ADMIN):
raise PermissionDeniedError("Admin access required")
# Get users from UserManager
if params.get("username"):
# Get specific user
target_user = await user_manager.get_user(params["username"])
if not target_user:
raise UserNotFoundError(params["username"])
users = [target_user]
else:
# Get all users with filters
users = await user_manager.list_users(
is_active=params.get("filter") == "active",
role=params.get("role"),
has_repositories=params.get("has_repositories")
)
# Enrich with additional data
enriched_users = []
for u in users:
user_data = {
"username": u.username,
"email": u.email,
"role": u.role.value,
"created_at": u.created_at.isoformat(),
"last_login": u.last_login.isoformat() if u.last_login else None,
"is_active": u.is_active
}
# Add repository count
repo_count = await get_user_repository_count(u.id)
user_data["repositories_activated"] = repo_count
# Add detailed information if requested
if params.get("include_details"):
user_data["details"] = await get_user_details(u.id)
enriched_users.append(user_data)
# Sort results
sort_by = params.get("sort_by", "username")
enriched_users.sort(key=lambda x: x[sort_by] or "")
return {
"users": enriched_users,
"total": len(enriched_users),
"timestamp": datetime.utcnow().isoformat()
}Create User Implementation
# pseudocode create user handler
async def handle_create_user(params: dict, user: User) -> dict:
# Verify admin role
if not user.has_permission(Permissions.ADMIN):
raise PermissionDeniedError("Admin access required")
# Handle bulk creation
if params.get("users"):
return await bulk_create_users(params["users"], params, user)
# Single user creation
username = params["username"]
email = params["email"]
# Validate username format
if not re.match(r"^[a-zA-Z0-9._-]+$", username):
raise ValidationError("Invalid username format")
# Check for duplicates
existing = await user_manager.get_user(username)
if existing:
raise DuplicateUserError(f"Username already exists: {username}")
# Validate email
if not is_valid_email(email):
raise ValidationError(f"Invalid email format: {email}")
# Handle password
if params.get("auto_password"):
password = generate_secure_password()
temporary = True
else:
password = params.get("password")
if not password:
raise ValidationError("Password required or set auto_password=true")
# Validate password complexity
validation = validate_password_complexity(password)
if not validation.valid:
raise ValidationError(f"Password requirements: {validation.requirements}")
temporary = False
# Create user
new_user = await user_manager.create_user(
username=username,
email=email,
password=password,
role=UserRole[params.get("role", "VIEWER")],
is_temporary_password=temporary
)
# Send welcome email if requested
if params.get("send_welcome"):
await queue_welcome_email(
email=email,
username=username,
temporary_password=password if temporary else None
)
# Audit log
await log_admin_action(
user=user,
action="create_user",
target=username,
details={"role": params.get("role", "VIEWER")}
)
response = {
"user_id": new_user.id,
"username": new_user.username,
"email": new_user.email,
"role": new_user.role.value,
"created": True
}
if temporary:
response["temporary_password"] = password
response["requires_password_change"] = True
return responseUpdate User Role Implementation
# pseudocode update role handler
async def handle_update_user_role(params: dict, user: User) -> dict:
# Verify admin role
if not user.has_permission(Permissions.ADMIN):
raise PermissionDeniedError("Admin access required")
username = params["username"]
# Get target user
target_user = await user_manager.get_user(username)
if not target_user:
raise UserNotFoundError(username)
changes = []
# Update role if specified
if params.get("new_role"):
old_role = target_user.role
new_role = UserRole[params["new_role"]]
if old_role != new_role:
await user_manager.update_user_role(target_user.id, new_role)
changes.append(f"Role changed from {old_role.value} to {new_role.value}")
# Refresh active sessions
await refresh_user_sessions(target_user.id)
# Reset password if requested
temporary_password = None
if params.get("reset_password"):
temporary_password = generate_secure_password()
await user_manager.reset_password(
target_user.id,
temporary_password,
temporary=True
)
changes.append("Password reset to temporary")
# Terminate active sessions
await terminate_user_sessions(target_user.id)
# Audit log
await log_admin_action(
user=user,
action="update_user",
target=username,
details={"changes": changes}
)
response = {
"username": username,
"changes": changes,
"success": True
}
if temporary_password:
response["temporary_password"] = temporary_password
return responseDelete User Implementation
# pseudocode delete user handler
async def handle_delete_user(params: dict, user: User) -> dict:
# Verify admin role
if not user.has_permission(Permissions.ADMIN):
raise PermissionDeniedError("Admin access required")
username = params["username"]
# Prevent self-deletion
if username == user.username:
raise ValidationError("Cannot delete your own account")
# Get target user
target_user = await user_manager.get_user(username)
if not target_user:
raise UserNotFoundError(username)
# Check for active resources
active_repos = await get_user_repository_count(target_user.id)
if params.get("soft_delete", True):
# Soft delete - deactivate account
await user_manager.deactivate_user(target_user.id)
# Terminate sessions
await terminate_user_sessions(target_user.id)
action = "deactivated"
details = {"method": "soft_delete", "repositories_retained": active_repos}
else:
# Hard delete - permanent removal
if params.get("cleanup_data"):
# Clean up user data
await cleanup_user_workspaces(target_user.id)
await remove_user_indexes(target_user.id)
elif params.get("transfer_to"):
# Transfer repositories to another user
transfer_user = await user_manager.get_user(params["transfer_to"])
if not transfer_user:
raise UserNotFoundError(params["transfer_to"])
await transfer_user_repositories(
from_user=target_user.id,
to_user=transfer_user.id
)
# Delete user record
await user_manager.delete_user(target_user.id)
action = "deleted"
details = {
"method": "hard_delete",
"data_cleaned": params.get("cleanup_data", False),
"transferred_to": params.get("transfer_to")
}
# Audit log
await log_admin_action(
user=user,
action=f"{action}_user",
target=username,
details=details
)
return {
"username": username,
"action": action,
"repositories_affected": active_repos,
"success": True,
"message": f"User {username} {action} successfully"
}Password and Validation Utilities
# pseudocode password utilities
def generate_secure_password(length: int = 16) -> str:
"""Generate cryptographically secure password"""
chars = string.ascii_letters + string.digits + "!@#$%^&*"
password = ''.join(secrets.choice(chars) for _ in range(length))
# Ensure complexity requirements
while not validate_password_complexity(password).valid:
password = ''.join(secrets.choice(chars) for _ in range(length))
return password
def validate_password_complexity(password: str) -> PasswordValidation:
"""Validate password meets complexity requirements"""
requirements = []
valid = True
if len(password) < 8:
requirements.append("Minimum 8 characters")
valid = False
if not re.search(r"[A-Z]", password):
requirements.append("At least one uppercase letter")
valid = False
if not re.search(r"[a-z]", password):
requirements.append("At least one lowercase letter")
valid = False
if not re.search(r"\d", password):
requirements.append("At least one number")
valid = False
if not re.search(r"[!@#$%^&*]", password):
requirements.append("At least one special character")
valid = False
return PasswordValidation(valid=valid, requirements=requirements)
def is_valid_email(email: str) -> bool:
"""Validate email format"""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))Bulk Operations
# pseudocode bulk user creation
async def bulk_create_users(users: list, params: dict, admin: User):
"""Create multiple users in parallel"""
default_role = params.get("default_role", "VIEWER")
auto_password = params.get("auto_password", True)
tasks = []
for user_data in users:
create_params = {
"username": user_data["username"],
"email": user_data["email"],
"role": user_data.get("role", default_role),
"auto_password": auto_password
}
tasks.append(handle_create_user(create_params, admin))
# Execute in parallel
results = await asyncio.gather(*tasks, return_exceptions=True)
# Format results
formatted = []
passwords = {}
for i, result in enumerate(results):
if isinstance(result, Exception):
formatted.append({
"username": users[i]["username"],
"status": "failed",
"error": str(result)
})
else:
formatted.append({
"username": result["username"],
"status": "created",
"role": result["role"]
})
if result.get("temporary_password"):
passwords[result["username"]] = result["temporary_password"]
return {
"results": formatted,
"temporary_passwords": passwords if passwords else None,
"summary": {
"total": len(users),
"created": sum(1 for r in formatted if r["status"] == "created"),
"failed": sum(1 for r in formatted if r["status"] == "failed")
}
}📋 Implementation Checklist
Tool Registration
- Register all 4 user management tools
- Define parameter schemas
- Set ADMIN permission requirement
- Map to handler functions
List Users
- Implement user listing
- Add filtering options
- Include repository counts
- Add detailed view option
- Implement sorting
Create User
- Username validation
- Email validation
- Password complexity check
- Auto-password generation
- Bulk creation support
- Welcome email integration
Update User
- Role update functionality
- Password reset option
- Session refresh logic
- Audit logging
Delete User
- Soft delete (deactivation)
- Hard delete option
- Data cleanup process
- Repository transfer
- Session termination
Security & Validation
- Password complexity rules
- Email format validation
- Username format rules
- Self-deletion prevention
- Admin-only enforcement
🧪 Testing Requirements
Unit Tests
- Test user listing filters
- Test user creation validation
- Test password complexity
- Test role updates
- Test deletion options
- Test bulk operations
Integration Tests
- Test UserManager integration
- Test session management
- Test email queue integration
- Test audit logging
- Test data cleanup
- Test repository transfer
Security Tests
- Test admin-only access
- Test password requirements
- Test injection prevention
- Test session termination
- Test audit trail
Edge Case Tests
- Test duplicate usernames
- Test invalid emails
- Test self-deletion attempt
- Test role escalation
- Test concurrent operations
📝 Definition of Done
- All 4 user management tools implemented
- User listing with filters and sorting
- User creation with validation
- Password generation and complexity
- Role updates working
- Soft and hard delete options
- Bulk operations supported
- Admin-only access enforced
- All unit tests passing (>90% coverage)
- Integration tests complete
- Security audit passed
- Documentation includes examples
- Audit logging comprehensive
- Error messages informative
Story Metadata:
- Type: Feature
- Priority: Medium
- Points: 5
- Components: Users, Security, Admin
- Labels: story, user-management, admin
- Dependencies: Story 2 (MCP Protocol)
- Blocked By: MCP protocol implementation
- Blocks: None
- Restrictions: ADMIN role only