Role-based access control for Greek accounting firms. Login, role hierarchy, per-client permissions, session management, audit logging.
---
name: user-authentication-system
description: Role-based access control for Greek accounting firms. Login, role hierarchy, per-client permissions, session management, audit logging.
version: 1.0.0
author: openclaw-greek-accounting
homepage: https://github.com/satoshistackalotto/openclaw-greek-accounting
tags: ["greek", "accounting", "authentication", "rbac", "security"]
metadata: {"openclaw": {"requires": {"bins": ["jq", "openssl", "openclaw"], "env": ["OPENCLAW_DATA_DIR"]}, "notes": "Manages local user accounts and role-based access. Credentials are stored as salted SHA-256 hashes in OPENCLAW_DATA_DIR/auth/. 2FA uses SHA-256 TOTP. No external auth services — fully local.", "path_prefix": "/data/ in examples refers to $OPENCLAW_DATA_DIR (default: /data/)"}}
---
# User Authentication System
This skill provides a complete authentication and authorization system for Greek accounting firm operations through OpenClaw. It manages user identities, role-based permissions, per-client access controls, and session security for multi-user accounting environments.
## Setup
```bash
export OPENCLAW_DATA_DIR="/data"
which jq openssl || sudo apt install jq openssl
mkdir -p $OPENCLAW_DATA_DIR/auth
chmod 700 $OPENCLAW_DATA_DIR/auth
```
No external auth services. User credentials are stored as salted SHA-256 hashes locally. 2FA uses SHA-256 TOTP generated by openssl.
## Core Philosophy
- **Role-Based Access**: Hierarchical permissions matching real accounting firm structures
- **Per-Client Authorization**: Granular control over which users access which client data
- **Session Security**: Secure session management with timeout and device tracking
- **Audit Integration**: Every authentication and authorization event logged
- **OpenClaw Artifact Ready**: File-based auth suitable for OpenClaw deployment
## OpenClaw Commands
### User Management
```bash
openclaw auth user-create --username "maria.g" --role assistant --full-name "Maria Georgiou" --email "maria@firm.gr"
openclaw auth user-update --username "maria.g" --role accountant --effective-date 2026-03-01
openclaw auth user-deactivate --username "maria.g" --reason "resignation" --revoke-sessions
openclaw auth user-list --active --role assistant --format table
openclaw auth password-reset --username "maria.g" --send-reset-link
openclaw auth password-policy --min-length 12 --require-special --max-age-days 90
```
### Role & Permission Management
```bash
openclaw auth role-list --include-permissions
openclaw auth role-create --name "tax_specialist" --base-role accountant --add-permissions "tax_filing,tax_optimization"
openclaw auth assign-clients --username "maria.g" --clients EL123456789,EL987654321
openclaw auth assign-clients --username "maria.g" --all-clients
openclaw auth check-access --username "maria.g" --client EL123456789 --action "view_financials"
openclaw auth access-matrix --all-users --all-clients --format xlsx
```
### Security & Audit
```bash
openclaw auth security-log --last-24h --include-failures
openclaw auth failed-logins --threshold 3 --lockout-duration 30m
openclaw auth audit-report --user "maria.g" --period last-30-days
openclaw auth audit-report --client EL123456789 --who-accessed --period last-week
openclaw auth 2fa-enable --username "maria.g" --method totp
openclaw auth sessions-list --active --format table
openclaw auth session-revoke --username "maria.g" --all-devices
```
## File System Architecture
```yaml
Auth_File_Structure:
user_data:
- /data/auth/users/{username}/profile.json
- /data/auth/users/{username}/credentials.json
- /data/auth/users/{username}/permissions.json
- /data/auth/users/{username}/sessions/
- /data/auth/users/{username}/2fa/
role_definitions:
- /data/auth/roles/senior_accountant.json
- /data/auth/roles/accountant.json
- /data/auth/roles/assistant.json
- /data/auth/roles/viewer.json
- /data/auth/roles/custom/
access_control:
- /data/auth/access/client_assignments.json
- /data/auth/access/policies.json
- /data/auth/access/ip_whitelist.json
security_logs:
- /data/auth/logs/logins/
- /data/auth/logs/access/
- /data/auth/logs/admin/
- /data/auth/logs/security/
```
## Role Hierarchy & Permissions
### Role Definitions
```yaml
Roles:
senior_accountant:
description: "Senior accountant - full system access"
level: 4
inherits: "accountant"
permissions:
- all_client_access
- user_management
- role_assignment
- system_configuration
- data_export_all
- compliance_override
- audit_log_access
- gdpr_operations
- billing_management
- skill_configuration
client_access: "all"
accountant:
description: "Accountant - broad access to assigned clients"
level: 3
inherits: "assistant"
permissions:
- client_data_full_access
- tax_filing_submit
- tax_optimization
- compliance_management
- financial_reporting
- efka_submissions
- banking_reconciliation
- deadline_management
- client_communication
client_access: "assigned_only"
restrictions:
- cannot_manage_users
- cannot_change_system_config
assistant:
description: "Accountant assistant - operational access"
level: 2
inherits: "viewer"
permissions:
- document_upload
- document_processing
- data_entry
- email_processing
- dashboard_access
- basic_reporting
- client_data_edit_basic
- alert_acknowledgement
- ocr_processing
client_access: "assigned_only"
restrictions:
- cannot_submit_tax_filings
- cannot_export_sensitive_data
- cannot_modify_financial_records
viewer:
description: "Read-only access to assigned client data"
level: 1
permissions:
- dashboard_view
- client_data_view
- report_view
- deadline_view
- document_view
client_access: "assigned_only"
restrictions:
- read_only
- no_data_modification
- no_data_export
```
### Permission Matrix
```yaml
Permission_Matrix:
view_dashboard: "viewer"
configure_dashboard: "accountant"
view_client_profile: "viewer"
edit_client_profile: "assistant"
create_client: "accountant"
delete_client: "senior_accountant"
export_client_data: "accountant"
gdpr_operations: "senior_accountant"
view_documents: "viewer"
upload_documents: "assistant"
process_documents: "assistant"
delete_documents: "accountant"
view_financials: "viewer"
enter_financial_data: "assistant"
modify_financial_records: "accountant"
submit_tax_filings: "accountant"
view_compliance_status: "viewer"
manage_compliance: "accountant"
override_compliance: "senior_accountant"
view_employee_data: "viewer"
manage_employees: "accountant"
submit_efka: "accountant"
view_transactions: "viewer"
reconcile_transactions: "assistant"
configure_banking: "accountant"
manage_users: "senior_accountant"
manage_roles: "senior_accountant"
view_audit_logs: "senior_accountant"
system_configuration: "senior_accountant"
```
## Authentication Engine
### Core Authentication
```python
class AuthenticationEngine:
"""Handles user authentication, sessions, and credential management."""
def __init__(self):
self.session_timeout = 30 * 60 # 30 minutes
self.idle_timeout = 15 * 60 # 15 minutes
self.max_failed_attempts = 5
self.lockout_duration = 30 * 60 # 30 minutes
def authenticate(self, username, password, device_info=None):
"""Authenticate user and create session."""
if self.is_account_locked(username):
self.log_auth_event(username, 'login_blocked', 'account_locked')
return {'success': False, 'error': 'Account is locked. Contact administrator.'}
user = self.load_user(username)
if not user:
self.log_auth_event(username, 'login_failed', 'user_not_found')
return {'success': False, 'error': 'Invalid credentials'}
if not self.verify_password(password, user['password_hash']):
self.record_failed_attempt(username)
self.log_auth_event(username, 'login_failed', 'wrong_password')
return {'success': False, 'error': 'Invalid credentials'}
if user['status'] != 'active':
self.log_auth_event(username, 'login_failed', f'account_{user["status"]}')
return {'success': False, 'error': 'Account is not active'}
if user.get('2fa_enabled', False):
return {'success': False, 'requires_2fa': True,
'session_pending': self.create_pending_session(username)}
session = self.create_session(username, device_info)
self.clear_failed_attempts(username)
self.log_auth_event(username, 'login_success', device_info=device_info)
return {'success': True, 'session': session, 'user': self.get_user_summary(username)}
def create_session(self, username, device_info=None):
"""Create a new authenticated session.
Security: The raw session token is returned to the user exactly once.
Only the salted SHA-256 hash is stored on disk. Validation compares
the hash of the incoming token against the stored hash.
"""
raw_token = generate_secure_token(64)
token_hash = hash_session_token(raw_token) # SHA-256 salted hash
session = {
'session_id': token_hash,
'username': username,
'created_at': current_timestamp(),
'expires_at': current_timestamp() + self.session_timeout,
'last_activity': current_timestamp(),
'device_info': device_info,
'ip_address': get_request_ip(),
'role': self.get_user_role(username),
'client_access': self.get_user_client_access(username)
}
session_path = f"/data/auth/users/{username}/sessions/{token_hash}.json"
write_json(session_path, session)
# Return raw token to user — this is the only time it exists in cleartext
session['bearer_token'] = raw_token
return session
def validate_session(self, session_id):
"""Validate an existing session."""
session = self.find_session(session_id)
if not session:
return {'valid': False, 'reason': 'session_not_found'}
if current_timestamp() > session['expires_at']:
self.destroy_session(session_id)
return {'valid': False, 'reason': 'session_expired'}
if current_timestamp() - session['last_activity'] > self.idle_timeout:
self.destroy_session(session_id)
return {'valid': False, 'reason': 'idle_timeout'}
session['last_activity'] = current_timestamp()
self.update_session(session)
return {'valid': True, 'session': session}
def hash_password(self, password):
"""Hash password using bcrypt with salt."""
return bcrypt_hash(password, rounds=12)
def validate_password_strength(self, password):
"""Check password meets policy requirements."""
errors = []
if len(password) < 12:
errors.append("Password must be at least 12 characters")
if not any(c.isupper() for c in password):
errors.append("Must contain uppercase letter")
if not any(c.islower() for c in password):
errors.append("Must contain lowercase letter")
if not any(c.isdigit() for c in password):
errors.append("Must contain digit")
if not any(c in '!@#$%^&*()_+-=' for c in password):
errors.append("Must contain special character")
return {'valid': len(errors) == 0, 'errors': errors}
```
### Authorization Engine
```python
class AuthorizationEngine:
"""Handles permission checks and access control decisions."""
def __init__(self):
self.roles = self.load_roles()
self.access_matrix = self.load_access_matrix()
def check_permission(self, session, action, client_vat=None):
"""Check if user has permission to perform an action."""
username = session['username']
user_role = session['role']
required_role = self.access_matrix.get(action)
if not required_role:
self.log_authorization(username, action, client_vat, 'denied', 'unknown_action')
return {'allowed': False, 'reason': f'Unknown action: {action}'}
user_level = self.roles[user_role]['level']
required_level = self.roles[required_role]['level']
if user_level < required_level:
self.log_authorization(username, action, client_vat, 'denied', 'insufficient_role')
return {'allowed': False,
'reason': f'Requires {required_role} role (you have {user_role})'}
# Check client-specific access
if client_vat:
client_access = session.get('client_access', [])
if 'all' not in client_access and client_vat not in client_access:
self.log_authorization(username, action, client_vat, 'denied', 'no_client_access')
return {'allowed': False,
'reason': f'Not authorized for client {AFM}'}
self.log_authorization(username, action, client_vat, 'allowed')
return {'allowed': True}
def resolve_permissions(self, role_name):
"""Resolve all permissions including inherited ones."""
role = self.roles.get(role_name, {})
permissions = set(role.get('permissions', []))
parent = role.get('inherits')
if parent:
permissions.update(self.resolve_permissions(parent))
return permissions
def get_accessible_clients(self, username):
"""Get list of clients this user can access."""
assignments = read_json("/data/auth/access/client_assignments.json")
user_entry = assignments.get(username, {})
if user_entry.get('all_clients', False):
return {'type': 'all', 'clients': 'all'}
return {'type': 'specific', 'clients': user_entry.get('clients', [])}
```
### User Management
```python
class UserManager:
"""Manages user lifecycle operations."""
def __init__(self):
self.auth_engine = AuthenticationEngine()
self.authz_engine = AuthorizationEngine()
def create_user(self, admin_session, user_data):
"""Create a new user account."""
# Verify admin has permission
perm_check = self.authz_engine.check_permission(admin_session, 'manage_users')
if not perm_check['allowed']:
return {'success': False, 'error': perm_check['reason']}
username = user_data['username']
# Check for duplicate
if self.user_exists(username):
return {'success': False, 'error': f'Username {username} already exists'}
# Validate role
if user_data['role'] not in self.authz_engine.roles:
return {'success': False, 'error': f'Invalid role: {user_data["role"]}'}
# Create user profile
profile = {
'username': username,
'full_name': user_data['full_name'],
'email': user_data.get('email'),
'role': user_data['role'],
'status': 'active',
'created_at': current_timestamp(),
'created_by': admin_session['username'],
'password_change_required': True,
'2fa_enabled': False
}
# Generate temporary password
temp_password = generate_secure_password()
credentials = {
'password_hash': self.auth_engine.hash_password(temp_password),
'password_set_at': current_timestamp(),
'password_change_required': True
}
# Create user directory and files
user_dir = f"/data/auth/users/{username}"
create_directory(user_dir)
create_directory(f"{user_dir}/sessions")
create_directory(f"{user_dir}/2fa")
write_json(f"{user_dir}/profile.json", profile)
write_json(f"{user_dir}/credentials.json", credentials)
write_json(f"{user_dir}/permissions.json", {'role': user_data['role'], 'custom': []})
# Audit log
self.log_admin_action(admin_session['username'], 'user_created', {
'new_user': username, 'role': user_data['role']
})
return {
'success': True,
'username': username,
'temporary_password': temp_password,
'message': f'User {username} created with role {user_data["role"]}. '
f'Password change required on first login.'
}
def assign_clients(self, admin_session, username, client_vats):
"""Assign client access to a user."""
perm_check = self.authz_engine.check_permission(admin_session, 'manage_users')
if not perm_check['allowed']:
return {'success': False, 'error': perm_check['reason']}
assignments_path = "/data/auth/access/client_assignments.json"
assignments = read_json(assignments_path) if file_exists(assignments_path) else {}
if client_vats == 'all':
assignments[username] = {'all_clients': True, 'clients': []}
else:
current = assignments.get(username, {'all_clients': False, 'clients': []})
current['clients'] = list(set(current.get('clients', []) + client_vats))
assignments[username] = current
write_json(assignments_path, assignments)
self.log_admin_action(admin_session['username'], 'clients_assigned', {
'user': username, 'clients': client_vats
})
return {'success': True, 'username': username, 'client_access': assignments[username]}
```
## Security Features
### File System Permissions (Production Hardening)
The `/data/auth/` directory contains sensitive credential and session data. In production, OS-level file permissions must be hardened:
```bash
# Restrict the entire auth directory
chmod 700 /data/auth/
chown -R openclaw:openclaw /data/auth/
# Credential files must be read-only to the service user
chmod 600 /data/auth/users/*/credentials.json
# Session files
chmod 600 /data/auth/users/*/sessions/*.json
# Role definitions (read by all skills for auth checks, writable only by admin)
chmod 644 /data/auth/roles/*.json
# Audit logs (append-only in production if OS supports it)
chmod 600 /data/auth/logs/**/*.json
```
**Note**: The OpenClaw agent and all skills share the same file system context. Without OS-level permission restrictions, any skill could read credential hashes. These permissions ensure that only the authentication skill's process can access sensitive auth data.
### Account Lockout
```yaml
Lockout_Policy:
max_failed_attempts: 5
lockout_duration: "30 minutes"
lockout_escalation:
- "5 failures: 30 minute lockout"
- "10 failures: 2 hour lockout"
- "15 failures: account disabled, admin notification"
notification: "email admin on 3+ consecutive failures"
```
### Two-Factor Authentication
```yaml
2FA_Configuration:
methods: ["totp"]
mandatory_for: ["senior_accountant"]
optional_for: ["accountant", "assistant"]
recovery_codes: 10
totp_settings:
algorithm: "SHA256"
digits: 6
period: 30
```
### Password Policy
```yaml
Password_Policy:
min_length: 12
require_uppercase: true
require_lowercase: true
require_digit: true
require_special: true
max_age_days: 90
history_count: 5
common_password_check: true
```
### IP & Device Security
```yaml
Security_Controls:
ip_whitelist:
enabled: true
allowed_ranges: ["office_ip_range"]
action_on_violation: "block_and_alert"
device_tracking:
track_devices: true
alert_new_device: true
max_concurrent_sessions: 3
session_controls:
absolute_timeout: "8 hours"
idle_timeout: "15 minutes"
single_session_per_device: true
```
## Integration with Other Skills
```yaml
Skill_Integration:
dashboard_greek_accounting:
provides: ["user_session", "role_permissions", "accessible_clients"]
enforces: "dashboard view permissions per user role"
client_data_management:
provides: ["access_decisions", "user_identity"]
enforces: "per-client data access based on user assignments"
integration: "authorization check before every data operation"
greek_compliance_aade:
enforces: "only accountant+ can submit filings"
efka_api_integration:
enforces: "only accountant+ can submit EFKA data"
all_skills:
provides: "user context for audit trail entries"
enforces: "role-based action restrictions across all operations"
```
## Audit & Compliance Reporting
### Audit Log Structure
```yaml
Audit_Events:
authentication:
- login_success
- login_failed
- login_blocked
- logout
- session_expired
- session_revoked
- 2fa_success
- 2fa_failed
- password_changed
- password_reset
authorization:
- access_granted
- access_denied
- client_access_checked
- permission_checked
administration:
- user_created
- user_updated
- user_deactivated
- role_changed
- clients_assigned
- clients_revoked
- policy_changed
Audit_Entry_Format:
timestamp: "ISO 8601 with timezone"
event_type: "category.action"
username: "acting user"
target: "affected resource"
client_vat: "if client-specific"
result: "success/failure"
details: "additional context"
ip_address: "source IP"
session_id: "active session"
```
## Success Metrics
A successful authentication system deployment should achieve:
- ✅ **Secure Authentication**: bcrypt password hashing, optional 2FA, session management
- ✅ **Role Hierarchy**: Four-level role system matching accounting firm structures
- ✅ **Per-Client Access**: Granular client data access assignment per user
- ✅ **Session Security**: Timeout, idle detection, concurrent session limits
- ✅ **Complete Audit Trail**: Every auth/authz event logged with context
- ✅ **Account Protection**: Lockout policy, password requirements, brute-force prevention
- ✅ **Cross-Skill Enforcement**: Authorization integrated into all data operations
- ✅ **Admin Tools**: User management, access matrix, security reporting
- ✅ **GDPR Compatible**: Access controls support data protection requirements
- ✅ **Scalable**: Handle 50+ users across 500+ clients
Remember: The authentication system is the security foundation for the entire Greek accounting platform. Every data access must pass through authorization checks, and every action must leave an audit trail.
don't have the plugin yet? install it then click "run inline in claude" again.