add api
This commit is contained in:
239
guacamole_test_11_26/generate_guacamole_user.py
Executable file
239
guacamole_test_11_26/generate_guacamole_user.py
Executable file
@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
SQL generator for creating Guacamole user with custom password
|
||||
|
||||
Uses same hashing algorithm as Guacamole:
|
||||
- SHA-256(password_bytes + salt_bytes)
|
||||
- Random 32-byte salt
|
||||
|
||||
Usage:
|
||||
python generate_guacamole_user.py --username admin --password MySecurePass123
|
||||
python generate_guacamole_user.py --username admin --password MySecurePass123 --admin
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import secrets
|
||||
import argparse
|
||||
import sys
|
||||
import io
|
||||
|
||||
# Fix Windows encoding issues
|
||||
if sys.platform == 'win32':
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||
|
||||
|
||||
def generate_guacamole_password_hash(password: str) -> tuple[bytes, bytes]:
|
||||
"""
|
||||
Generate hash and salt for Guacamole password
|
||||
|
||||
CORRECT ALGORITHM (verified 2025-10-29):
|
||||
Guacamole uses: SHA-256(password_string + salt_hex_string)
|
||||
IMPORTANT: Salt converted to HEX string BEFORE hashing!
|
||||
|
||||
Args:
|
||||
password: Password in plain text
|
||||
|
||||
Returns:
|
||||
Tuple (password_hash, password_salt) as bytes for PostgreSQL
|
||||
"""
|
||||
# Generate random 32-byte salt
|
||||
salt_bytes = secrets.token_bytes(32)
|
||||
|
||||
# CRITICAL: Convert salt to HEX STRING (uppercase)
|
||||
# Guacamole hashes: password + hex(salt), NOT password + binary(salt)!
|
||||
salt_hex_string = salt_bytes.hex().upper()
|
||||
|
||||
# Compute SHA-256(password_string + salt_hex_string)
|
||||
# Concatenate password STRING + salt HEX STRING, then encode to UTF-8
|
||||
hash_input = password + salt_hex_string
|
||||
password_hash = hashlib.sha256(hash_input.encode('utf-8')).digest()
|
||||
|
||||
return password_hash, salt_bytes
|
||||
|
||||
|
||||
def bytes_to_postgres_hex(data: bytes) -> str:
|
||||
"""
|
||||
Convert bytes to PostgreSQL hex format for decode()
|
||||
|
||||
Args:
|
||||
data: Bytes to convert
|
||||
|
||||
Returns:
|
||||
String in 'HEXSTRING' format for use in decode('...', 'hex')
|
||||
"""
|
||||
return data.hex().upper()
|
||||
|
||||
|
||||
def generate_sql(username: str, password: str, is_admin: bool = False) -> str:
|
||||
"""
|
||||
Generate SQL for creating Guacamole user
|
||||
|
||||
Args:
|
||||
username: Username
|
||||
password: Password
|
||||
is_admin: If True, grant full administrator privileges
|
||||
|
||||
Returns:
|
||||
SQL script to execute
|
||||
"""
|
||||
password_hash, password_salt = generate_guacamole_password_hash(password)
|
||||
|
||||
hash_hex = bytes_to_postgres_hex(password_hash)
|
||||
salt_hex = bytes_to_postgres_hex(password_salt)
|
||||
|
||||
sql = f"""-- Generated Guacamole user creation SQL
|
||||
-- Username: {username}
|
||||
-- Password: {'*' * len(password)} (length: {len(password)})
|
||||
-- Generated with: generate_guacamole_user.py
|
||||
|
||||
-- Create user entity
|
||||
INSERT INTO guacamole_entity (name, type)
|
||||
VALUES ('{username}', 'USER');
|
||||
|
||||
-- Create user with password hash
|
||||
INSERT INTO guacamole_user (entity_id, password_hash, password_salt, password_date)
|
||||
SELECT
|
||||
entity_id,
|
||||
decode('{hash_hex}', 'hex'),
|
||||
decode('{salt_hex}', 'hex'),
|
||||
CURRENT_TIMESTAMP
|
||||
FROM guacamole_entity
|
||||
WHERE name = '{username}' AND guacamole_entity.type = 'USER';
|
||||
"""
|
||||
|
||||
if is_admin:
|
||||
sql += f"""
|
||||
-- Grant all system permissions (administrator)
|
||||
INSERT INTO guacamole_system_permission (entity_id, permission)
|
||||
SELECT entity_id, permission::guacamole_system_permission_type
|
||||
FROM (
|
||||
VALUES
|
||||
('{username}', 'CREATE_CONNECTION'),
|
||||
('{username}', 'CREATE_CONNECTION_GROUP'),
|
||||
('{username}', 'CREATE_SHARING_PROFILE'),
|
||||
('{username}', 'CREATE_USER'),
|
||||
('{username}', 'CREATE_USER_GROUP'),
|
||||
('{username}', 'ADMINISTER')
|
||||
) permissions (username, permission)
|
||||
JOIN guacamole_entity ON permissions.username = guacamole_entity.name AND guacamole_entity.type = 'USER';
|
||||
|
||||
-- Grant permission to read/update/administer self
|
||||
INSERT INTO guacamole_user_permission (entity_id, affected_user_id, permission)
|
||||
SELECT guacamole_entity.entity_id, guacamole_user.user_id, permission::guacamole_object_permission_type
|
||||
FROM (
|
||||
VALUES
|
||||
('{username}', '{username}', 'READ'),
|
||||
('{username}', '{username}', 'UPDATE'),
|
||||
('{username}', '{username}', 'ADMINISTER')
|
||||
) permissions (username, affected_username, permission)
|
||||
JOIN guacamole_entity ON permissions.username = guacamole_entity.name AND guacamole_entity.type = 'USER'
|
||||
JOIN guacamole_entity affected ON permissions.affected_username = affected.name AND guacamole_entity.type = 'USER'
|
||||
JOIN guacamole_user ON guacamole_user.entity_id = affected.entity_id;
|
||||
"""
|
||||
|
||||
return sql
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Generate SQL for creating Guacamole user with custom password',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Create regular user
|
||||
python generate_guacamole_user.py --username john --password SecurePass123
|
||||
|
||||
# Create administrator user
|
||||
python generate_guacamole_user.py --username admin --password AdminPass456 --admin
|
||||
|
||||
# Save to file
|
||||
python generate_guacamole_user.py --username admin --password AdminPass456 --admin > 002-custom-admin.sql
|
||||
|
||||
# Apply directly to running database
|
||||
python generate_guacamole_user.py --username admin --password AdminPass456 --admin | \\
|
||||
docker compose exec -T postgres psql -U guacamole_user -d guacamole_db
|
||||
|
||||
SECURITY NOTES:
|
||||
- Never commit generated SQL files with passwords to git!
|
||||
- Use strong passwords (minimum 16 characters, mixed case, numbers, symbols)
|
||||
- Change default passwords immediately after deployment
|
||||
- Store passwords securely (password manager, secrets vault)
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--username',
|
||||
required=True,
|
||||
help='Username for the new Guacamole user'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--password',
|
||||
required=True,
|
||||
help='Password for the new user (plain text)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--admin',
|
||||
action='store_true',
|
||||
help='Grant administrator privileges (ADMINISTER system permission)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--verify',
|
||||
action='store_true',
|
||||
help='Verify password by generating hash twice'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate password strength
|
||||
if len(args.password) < 8:
|
||||
print("[WARNING] Password is too short (< 8 characters)", file=sys.stderr)
|
||||
print(" Recommended: minimum 16 characters with mixed case, numbers, symbols", file=sys.stderr)
|
||||
response = input("Continue anyway? (y/N): ")
|
||||
if response.lower() != 'y':
|
||||
sys.exit(1)
|
||||
|
||||
# Verify if requested
|
||||
if args.verify:
|
||||
print("[VERIFY] Verifying hash generation...", file=sys.stderr)
|
||||
hash1, salt1 = generate_guacamole_password_hash(args.password)
|
||||
hash2, salt2 = generate_guacamole_password_hash(args.password)
|
||||
|
||||
# Salts should be different (random)
|
||||
if salt1 == salt2:
|
||||
print("[ERROR] Salt generation not random!", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# But if we use same salt, hash should be same
|
||||
# Use correct algorithm: SHA256(password_string + salt_hex_string)
|
||||
salt_hex_string = salt1.hex().upper()
|
||||
hash_test = hashlib.sha256((args.password + salt_hex_string).encode('utf-8')).digest()
|
||||
if hash_test == hash1:
|
||||
print("[OK] Hash generation verified", file=sys.stderr)
|
||||
else:
|
||||
print("[ERROR] Hash generation mismatch!", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Generate SQL
|
||||
sql = generate_sql(args.username, args.password, args.admin)
|
||||
|
||||
# Output
|
||||
print(sql)
|
||||
|
||||
# Print info to stderr (so it doesn't interfere with piping SQL)
|
||||
role = "Administrator" if args.admin else "Regular User"
|
||||
print(f"\n[OK] SQL generated successfully!", file=sys.stderr)
|
||||
print(f" Username: {args.username}", file=sys.stderr)
|
||||
print(f" Role: {role}", file=sys.stderr)
|
||||
print(f" Password length: {len(args.password)} characters", file=sys.stderr)
|
||||
print(f"\n[INFO] To apply this SQL:", file=sys.stderr)
|
||||
print(f" docker compose exec -T postgres psql -U guacamole_user -d guacamole_db < output.sql", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user