Initial project structure with CLI foundation
- Set up main CLI entry point with argparse architecture - Implement complete tenant management system (CRUD operations) - Add PostgreSQL database connection layer with configuration - Create user management interface foundation - Implement rich terminal UI with formatted tables - Add interactive prompts with questionary library - Include comprehensive project documentation - Set up modular command structure with Parser/Command pattern
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.pyw
|
||||
*.pyz
|
||||
*.pywz
|
||||
*.pyzw
|
||||
*.pyzwz
|
||||
*.pyzwzw
|
||||
*.pyzwzwz
|
||||
*.pyzwzwzw
|
||||
*.pyzwzwzwz
|
||||
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.development
|
||||
.env.test
|
||||
.env.production
|
||||
|
||||
.venv/
|
||||
137
CLAUDE.md
Normal file
137
CLAUDE.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is the SecNex Automation CLI, a command-line interface for automating interactions with the SecNex cloud environment. The CLI provides tenant and user management capabilities with a PostgreSQL backend and interactive terminal interface.
|
||||
|
||||
## Development Policy
|
||||
|
||||
### AI-Assisted Development
|
||||
- **No AI Attribution**: Do not add "Authored-By", "Co-Authored-By", "Generated with Claude Code", or similar AI attribution in commits, code comments, or documentation
|
||||
- **Clean Code History**: Maintain a clean git history without AI-generated commit messages or attributions
|
||||
- **Professional Standards**: All code should be written to professional standards without explicit references to AI assistance
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Running the CLI
|
||||
```bash
|
||||
# Run from project root
|
||||
python src/secnex-cli/index.py
|
||||
|
||||
# Tenant management examples
|
||||
python src/secnex-cli/index.py tenant create
|
||||
python src/secnex-cli/index.py tenant list
|
||||
python src/secnex-cli/index.py tenant delete --name "tenant-name"
|
||||
|
||||
# User management examples
|
||||
python src/secnex-cli/index.py user create
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
The project uses external dependencies but lacks formal dependency management. Key dependencies:
|
||||
- `psycopg2` - PostgreSQL database adapter
|
||||
- `rich` - Terminal formatting and tables
|
||||
- `questionary` - Interactive command-line prompts
|
||||
- `argon2-cffi` - Password hashing with Argon2 algorithm
|
||||
|
||||
Install dependencies manually:
|
||||
```bash
|
||||
pip install psycopg2-binary rich questionary argon2-cffi
|
||||
```
|
||||
|
||||
## Project Architecture
|
||||
|
||||
### Entry Point (`src/secnex-cli/index.py`)
|
||||
- **Main Class**: `SecNexCLI` - Initializes database connection and sets up command parsing
|
||||
- **Database**: Hardcoded PostgreSQL connection (localhost:5432, database="api", user="postgres", password="postgres")
|
||||
- **Command Pattern**: Uses argparse subparsers with separate Parser and Command classes
|
||||
- **Match/Case**: Python 3.10+ match-case syntax for command dispatch
|
||||
|
||||
### Command Structure
|
||||
The CLI follows a Parser/Command pattern:
|
||||
|
||||
- **Parser Classes** (`cmd/tenant.py`, `cmd/user.py`): Define argparse structure and command-line arguments
|
||||
- **Command Classes**: Implement business logic and database operations
|
||||
- **Database Layer** (`database/`): Centralized connection management and configuration
|
||||
|
||||
### Database Schema
|
||||
- **Primary Table**: `core.tenants` with fields: `id`, `name`, `enabled`, `created_at`, `deleted_at`
|
||||
- **Soft Deletion**: Uses `deleted_at` timestamp for soft deletes
|
||||
- **Connection Management**: Direct psycopg2 connections with manual cursor management
|
||||
|
||||
### Tenant Commands (Fully Implemented)
|
||||
- `tenant create` - Interactive tenant creation with rich table output
|
||||
- `tenant list` - Lists active tenants in formatted table
|
||||
- `tenant get --name <tenant>` - Retrieves specific tenant details
|
||||
- `tenant delete --name <tenant> [--force]` - Soft delete (default) or hard delete with --force
|
||||
- `tenant enable/disable --name <tenant>` - Tenant state management
|
||||
- `tenant restore --name <tenant>` - Restore soft-deleted tenants
|
||||
|
||||
### User Commands (Fully Implemented)
|
||||
- `user create` - Interactive user creation with password hashing, tenant selection, and rich table output
|
||||
|
||||
## Key Implementation Details
|
||||
|
||||
### UI/UX Features
|
||||
- **Rich Tables**: Uses `rich.Table` for formatted terminal output with emojis (✅/❌ for enabled/disabled)
|
||||
- **Interactive Prompts**: Uses `questionary` for text input, password fields, and selection dropdowns
|
||||
- **Cancellation Handling**: Proper handling when users cancel prompts (Ctrl+C or ESC)
|
||||
|
||||
### Database Patterns
|
||||
- **Parameterized Queries**: All database operations use parameterized queries to prevent SQL injection
|
||||
- **Manual Connection Management**: Explicit cursor creation, commit, and close operations
|
||||
- **Error Handling**: Basic database connection error handling with graceful exits
|
||||
|
||||
### Security Features
|
||||
- **Argon2 Password Hashing**: Uses Argon2 algorithm for secure password storage with configurable parameters
|
||||
- **Secure Hash Configuration**: Time cost 3, memory cost 64MB, parallelism 4, 32-byte hash, 16-byte salt
|
||||
- **Password Verification**: Includes verification and rehash checking capabilities in `utils/hash.py`
|
||||
- **Transaction Management**: Proper rollback on database errors to maintain data integrity
|
||||
|
||||
### Code Organization
|
||||
- **Type Hints**: Consistent use of Python type hints throughout codebase
|
||||
- **Separation of Concerns**: Clear separation between parsing, command execution, and database operations
|
||||
- **Interactive vs Non-interactive**: Commands support both argument-based and interactive input
|
||||
|
||||
## Critical Missing Components
|
||||
|
||||
### Dependency Management
|
||||
- No `requirements.txt` or `pyproject.toml` exists
|
||||
- Dependencies must be installed manually
|
||||
- No version pinning or dependency resolution
|
||||
|
||||
### Configuration Management
|
||||
- Database credentials are hardcoded in `index.py:9-15`
|
||||
- No environment variable support or config file loading
|
||||
- No development/production configuration separation
|
||||
|
||||
### Incomplete Features
|
||||
- No error handling for database constraint violations (duplicate tenants, etc.)
|
||||
- No logging or audit trail functionality
|
||||
- No user authentication/login functionality (only user creation)
|
||||
|
||||
### Development Infrastructure
|
||||
- No test framework or test files
|
||||
- No linting configuration (flake8, black, etc.)
|
||||
- No CI/CD pipeline configuration
|
||||
- No shell completion support
|
||||
|
||||
## Database Connection Requirements
|
||||
|
||||
The CLI requires a running PostgreSQL instance with:
|
||||
- Database name: `api`
|
||||
- Tables:
|
||||
- `core.tenants` with schema: `id`, `name`, `enabled`, `created_at`, `deleted_at`
|
||||
- `auth.users` with schema: `id`, `tenant_id`, `name`, `email`, `password`, `created_at`, `enabled`
|
||||
- User: `postgres` with password `postgres` on localhost:5432 (hardcoded)
|
||||
|
||||
## Future Development Priorities
|
||||
|
||||
1. **Add formal dependency management** (requirements.txt or pyproject.toml)
|
||||
2. **Implement configuration system** for database connections and CLI settings
|
||||
3. **Complete user management** functionality
|
||||
4. **Add comprehensive error handling** and validation
|
||||
5. **Implement testing framework** with unit and integration tests
|
||||
6. **Add environment variable support** for configuration
|
||||
19
README.md
Normal file
19
README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# SecNex Automation CLI
|
||||
|
||||
This is a CLI to automate for SecNex cloud environment.
|
||||
|
||||
## Commands
|
||||
|
||||
### `tenant`
|
||||
|
||||
#### `tenant create` / `tenant new`
|
||||
|
||||
Create a new tenant.
|
||||
|
||||
#### `tenant list` / `tenant ls`
|
||||
|
||||
List all tenants.
|
||||
|
||||
#### `tenant delete`
|
||||
|
||||
Delete a tenant.
|
||||
2
src/secnex-cli/cmd/__init__.py
Normal file
2
src/secnex-cli/cmd/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .tenant import TenantParser, TenantCommand
|
||||
from .user import UserParser, UserCommand
|
||||
231
src/secnex-cli/cmd/tenant.py
Normal file
231
src/secnex-cli/cmd/tenant.py
Normal file
@@ -0,0 +1,231 @@
|
||||
import argparse
|
||||
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
import questionary
|
||||
|
||||
from database.conn import DatabaseConnection
|
||||
|
||||
class TenantParser:
|
||||
def __init__(self, parser: argparse.ArgumentParser) -> None:
|
||||
self.parser = parser
|
||||
|
||||
def add_parser(self) -> None:
|
||||
tenant_subparsers = self.parser.add_subparsers(dest="action", help="tenant actions")
|
||||
tenant_subparsers.add_parser("list", help="list all tenants")
|
||||
get_parser = tenant_subparsers.add_parser("get", help="get a tenant")
|
||||
get_parser.add_argument("--name", type=str, required=True, help="name of the tenant")
|
||||
create_parser = tenant_subparsers.add_parser("create", help="create a new tenant")
|
||||
create_parser.add_argument("--name", type=str, required=False, default=None, help="name of the tenant")
|
||||
delete_parser = tenant_subparsers.add_parser("delete", help="delete a tenant")
|
||||
delete_parser.add_argument("--name", type=str, required=False, default=None, help="name of the tenant")
|
||||
delete_parser.add_argument("--force", action="store_true", required=False, default=None, help="force delete a tenant")
|
||||
enable_parser = tenant_subparsers.add_parser("enable", help="enable a tenant")
|
||||
enable_parser.add_argument("--name", type=str, required=False, default=None, help="name of the tenant")
|
||||
disable_parser = tenant_subparsers.add_parser("disable", help="disable a tenant")
|
||||
disable_parser.add_argument("--name", type=str, required=False, default=None, help="name of the tenant")
|
||||
|
||||
class TenantCommand:
|
||||
def __init__(self, action: str, database_connection: DatabaseConnection) -> None:
|
||||
self.action = action
|
||||
self.__database_connection = database_connection
|
||||
|
||||
def execute(self, args: argparse.Namespace) -> None:
|
||||
match self.action:
|
||||
case "list":
|
||||
self.tenant_list()
|
||||
case "get":
|
||||
self.tenant_get(args.name)
|
||||
case "create":
|
||||
self.tenant_create(args.name)
|
||||
case "delete":
|
||||
self.tenant_delete(args.name, args.force)
|
||||
case "restore":
|
||||
self.tenant_restore(args.name)
|
||||
case "enable":
|
||||
self.tenant_enable(args.name)
|
||||
case "disable":
|
||||
self.tenant_disable(args.name)
|
||||
case _:
|
||||
print(f"Unknown action: {self.action}")
|
||||
|
||||
def tenant_create(self, name: str | None = None) -> None:
|
||||
if name is None:
|
||||
name = questionary.text("Enter the name of the tenant").ask()
|
||||
if name is None:
|
||||
print("Tenant creation cancelled")
|
||||
return
|
||||
|
||||
if self.__database_connection.connection is None:
|
||||
print("Database connection is not available")
|
||||
return
|
||||
|
||||
cursor = self.__database_connection.connection.cursor()
|
||||
|
||||
cursor.execute("INSERT INTO core.tenants (name, created_at, enabled) VALUES (%s, NOW(), TRUE) RETURNING *", (name,))
|
||||
tenant = cursor.fetchone()
|
||||
if tenant is None:
|
||||
print("Failed to create tenant")
|
||||
return
|
||||
|
||||
self.__database_connection.connection.commit()
|
||||
cursor.close()
|
||||
|
||||
print()
|
||||
|
||||
table = Table()
|
||||
table.add_column("ID", justify="left")
|
||||
table.add_column("Name", justify="left")
|
||||
table.add_column("Enabled", justify="center")
|
||||
table.add_column("Created At", justify="left")
|
||||
table.add_row(str(tenant[0]), tenant[1], "✅" if tenant[2] else "❌", tenant[3].strftime("%Y-%m-%d %H:%M:%S"))
|
||||
Console().print(table)
|
||||
|
||||
def tenant_delete(self, name: str | None = None, force: bool | None = None) -> None:
|
||||
if name is None:
|
||||
name = questionary.text("Enter the name of the tenant").ask()
|
||||
if name is None:
|
||||
print("Tenant deletion cancelled")
|
||||
return
|
||||
|
||||
if force is None:
|
||||
force = questionary.confirm("Are you sure you want to delete this tenant?").ask()
|
||||
if force is None:
|
||||
print("Tenant deletion cancelled")
|
||||
return
|
||||
|
||||
if self.__database_connection.connection is None:
|
||||
print("Database connection is not available")
|
||||
return
|
||||
|
||||
cursor = self.__database_connection.connection.cursor()
|
||||
if force:
|
||||
cursor.execute("DELETE FROM core.tenants WHERE name = %s", (name,))
|
||||
else:
|
||||
cursor.execute("UPDATE core.tenants SET deleted_at = NOW() WHERE name = %s AND deleted_at IS NULL", (name,))
|
||||
cursor.execute("UPDATE core.tenants SET enabled = FALSE WHERE name = %s AND deleted_at IS NOT NULL", (name,))
|
||||
self.__database_connection.connection.commit()
|
||||
cursor.close()
|
||||
print(f"Tenant {name} deleted successfully")
|
||||
|
||||
def tenant_restore(self, name: str) -> None:
|
||||
print(f"Restoring tenant {name}...")
|
||||
|
||||
if self.__database_connection.connection is None:
|
||||
print("Database connection is not available")
|
||||
return
|
||||
|
||||
cursor = self.__database_connection.connection.cursor()
|
||||
cursor.execute("UPDATE core.tenants SET deleted_at = NULL WHERE name = %s", (name,))
|
||||
cursor.execute("UPDATE core.tenants SET enabled = TRUE WHERE name = %s AND deleted_at IS NOT NULL", (name,))
|
||||
self.__database_connection.connection.commit()
|
||||
cursor.close()
|
||||
print(f"Tenant {name} restored successfully")
|
||||
|
||||
def tenant_enable(self, name: str | None = None) -> None:
|
||||
if name is None:
|
||||
name = questionary.text("Enter the name of the tenant").ask()
|
||||
if name is None:
|
||||
print("Tenant enable cancelled")
|
||||
return
|
||||
|
||||
print(f"Enabling tenant {name}...")
|
||||
|
||||
if self.__database_connection.connection is None:
|
||||
print("Database connection is not available")
|
||||
return
|
||||
|
||||
cursor = self.__database_connection.connection.cursor()
|
||||
cursor.execute("SELECT id, name, enabled, created_at FROM core.tenants WHERE name = %s", (name,))
|
||||
tenant = cursor.fetchone()
|
||||
if tenant is None:
|
||||
print("Tenant not found")
|
||||
return
|
||||
|
||||
cursor.execute("UPDATE core.tenants SET enabled = TRUE WHERE name = %s", (name,))
|
||||
|
||||
self.__database_connection.connection.commit()
|
||||
cursor.close()
|
||||
|
||||
table = Table()
|
||||
table.add_column("ID", justify="left")
|
||||
table.add_column("Name", justify="left")
|
||||
table.add_column("Enabled", justify="center")
|
||||
table.add_column("Created At", justify="left")
|
||||
table.add_row(str(tenant[0]), tenant[1], "✅" if not tenant[2] else "❌", tenant[3].strftime("%Y-%m-%d %H:%M:%S"))
|
||||
Console().print(table)
|
||||
|
||||
def tenant_disable(self, name: str | None = None) -> None:
|
||||
if name is None:
|
||||
name = questionary.text("Enter the name of the tenant").ask()
|
||||
if name is None:
|
||||
print("Tenant disable cancelled")
|
||||
return
|
||||
|
||||
print(f"Disabling tenant {name}...")
|
||||
|
||||
if self.__database_connection.connection is None:
|
||||
print("Database connection is not available")
|
||||
return
|
||||
|
||||
cursor = self.__database_connection.connection.cursor()
|
||||
|
||||
cursor.execute("SELECT id, name, enabled, created_at FROM core.tenants WHERE name = %s", (name,))
|
||||
tenant = cursor.fetchone()
|
||||
if tenant is None:
|
||||
print("Tenant not found")
|
||||
return
|
||||
|
||||
cursor.execute("UPDATE core.tenants SET enabled = FALSE WHERE name = %s", (name,))
|
||||
self.__database_connection.connection.commit()
|
||||
cursor.close()
|
||||
|
||||
table = Table()
|
||||
table.add_column("ID", justify="left")
|
||||
table.add_column("Name", justify="left")
|
||||
table.add_column("Enabled", justify="center")
|
||||
table.add_column("Created At", justify="left")
|
||||
table.add_row(str(tenant[0]), tenant[1], "✅" if not tenant[2] else "❌", tenant[3].strftime("%Y-%m-%d %H:%M:%S"))
|
||||
Console().print(table)
|
||||
|
||||
def tenant_list(self) -> None:
|
||||
print("Listing tenants...")
|
||||
|
||||
if self.__database_connection.connection is None:
|
||||
print("Database connection is not available")
|
||||
return
|
||||
|
||||
cursor = self.__database_connection.connection.cursor()
|
||||
cursor.execute("SELECT id, name, enabled, created_at FROM core.tenants WHERE deleted_at IS NULL")
|
||||
tenants = cursor.fetchall()
|
||||
|
||||
table = Table()
|
||||
table.add_column("ID", justify="left")
|
||||
table.add_column("Name", justify="left")
|
||||
table.add_column("Enabled", justify="center")
|
||||
table.add_column("Created At", justify="left")
|
||||
for tenant in tenants:
|
||||
table.add_row(str(tenant[0]), tenant[1], "✅" if tenant[2] else "❌", tenant[3].strftime("%Y-%m-%d %H:%M:%S"))
|
||||
Console().print(table)
|
||||
cursor.close()
|
||||
|
||||
def tenant_get(self, name: str) -> None:
|
||||
print(f"Getting tenant {name}...")
|
||||
|
||||
if self.__database_connection.connection is None:
|
||||
print("Database connection is not available")
|
||||
return
|
||||
|
||||
cursor = self.__database_connection.connection.cursor()
|
||||
cursor.execute("SELECT * FROM core.tenants WHERE name = %s", (name,))
|
||||
tenant = cursor.fetchone()
|
||||
|
||||
table = Table()
|
||||
table.add_column("ID", justify="left")
|
||||
table.add_column("Name", justify="left")
|
||||
table.add_column("Enabled", justify="center")
|
||||
table.add_column("Created At", justify="left")
|
||||
table.add_row(str(tenant[0]), tenant[1], "✅" if tenant[2] else "❌", tenant[3].strftime("%Y-%m-%d %H:%M:%S"))
|
||||
Console().print(table)
|
||||
|
||||
cursor.close()
|
||||
111
src/secnex-cli/cmd/user.py
Normal file
111
src/secnex-cli/cmd/user.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import argparse
|
||||
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
import questionary
|
||||
|
||||
from database.conn import DatabaseConnection
|
||||
from utils.hash import Argon2Hasher
|
||||
|
||||
class UserParser:
|
||||
def __init__(self, parser: argparse.ArgumentParser) -> None:
|
||||
self.parser = parser
|
||||
|
||||
def add_parser(self) -> None:
|
||||
user_subparsers = self.parser.add_subparsers(dest="action", help="user actions")
|
||||
user_subparsers.add_parser("create", help="create a new user")
|
||||
|
||||
class UserCommand:
|
||||
def __init__(self, action: str, database_connection: DatabaseConnection) -> None:
|
||||
self.action = action
|
||||
self.__database_connection = database_connection
|
||||
self.__hasher = Argon2Hasher()
|
||||
|
||||
def execute(self, args: argparse.Namespace) -> None:
|
||||
match self.action:
|
||||
case "create":
|
||||
self.user_create()
|
||||
case _:
|
||||
print(f"Unknown action: {self.action}")
|
||||
|
||||
def user_create(self) -> None:
|
||||
if self.__database_connection.connection is None:
|
||||
print("Database connection is not available")
|
||||
return
|
||||
|
||||
cursor = self.__database_connection.connection.cursor()
|
||||
cursor.execute("SELECT id, name FROM core.tenants WHERE enabled = TRUE AND deleted_at IS NULL")
|
||||
tenants = cursor.fetchall()
|
||||
if len(tenants) == 0:
|
||||
print("No tenants found")
|
||||
return
|
||||
|
||||
tenant_name = questionary.select("Select a tenant", choices=[tenant[1] for tenant in tenants]).ask()
|
||||
if tenant_name is None:
|
||||
print("User creation cancelled")
|
||||
return
|
||||
|
||||
tenant_id = None
|
||||
for tenant in tenants:
|
||||
if tenant[1] == tenant_name:
|
||||
tenant_id = tenant[0]
|
||||
break
|
||||
|
||||
if tenant_id is None:
|
||||
print("Tenant not found")
|
||||
return
|
||||
|
||||
name = questionary.text("Enter the name of the user").ask()
|
||||
if name is None:
|
||||
print("User creation cancelled")
|
||||
return
|
||||
|
||||
email = questionary.text("Enter the email of the user").ask()
|
||||
if email is None:
|
||||
print("User creation cancelled")
|
||||
return
|
||||
|
||||
password = questionary.password("Enter the password of the user").ask()
|
||||
if password is None:
|
||||
print("User creation cancelled")
|
||||
return
|
||||
|
||||
# Hash the password using Argon2
|
||||
hashed_password = self.__hasher.hash_password(password)
|
||||
|
||||
# Insert the user into the database
|
||||
try:
|
||||
cursor.execute(
|
||||
"INSERT INTO auth.users (tenant_id, name, email, password, created_at, enabled) VALUES (%s, %s, %s, %s, NOW(), TRUE) RETURNING id, name, email, created_at",
|
||||
(tenant_id, name, email, hashed_password)
|
||||
)
|
||||
user = cursor.fetchone()
|
||||
self.__database_connection.connection.commit()
|
||||
|
||||
if user is None:
|
||||
print("Failed to create user")
|
||||
return
|
||||
|
||||
# Display success message with user details
|
||||
print()
|
||||
table = Table()
|
||||
table.add_column("ID", justify="left")
|
||||
table.add_column("Name", justify="left")
|
||||
table.add_column("Email", justify="left")
|
||||
table.add_column("Tenant", justify="left")
|
||||
table.add_column("Created At", justify="left")
|
||||
table.add_row(
|
||||
str(user[0]),
|
||||
user[1],
|
||||
user[2],
|
||||
tenant_name, # This contains the tenant name for display
|
||||
user[3].strftime("%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
Console().print(table)
|
||||
print("✅ User created successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating user: {e}")
|
||||
self.__database_connection.connection.rollback()
|
||||
finally:
|
||||
cursor.close()
|
||||
16
src/secnex-cli/database/config.py
Normal file
16
src/secnex-cli/database/config.py
Normal file
@@ -0,0 +1,16 @@
|
||||
class DatabaseConfig:
|
||||
def __init__(self, host: str, port: int, database: str, user: str, password: str) -> None:
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.database = database
|
||||
self.user = user
|
||||
self.password = password
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"host": self.host,
|
||||
"port": self.port,
|
||||
"database": self.database,
|
||||
"user": self.user,
|
||||
"password": self.password,
|
||||
}
|
||||
16
src/secnex-cli/database/conn.py
Normal file
16
src/secnex-cli/database/conn.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import psycopg2
|
||||
|
||||
from .config import DatabaseConfig
|
||||
|
||||
class DatabaseConnection:
|
||||
def __init__(self, host: str, port: int, database: str, user: str, password: str) -> None:
|
||||
self.config = DatabaseConfig(host, port, database, user, password)
|
||||
self.connection = None
|
||||
|
||||
def connect(self) -> psycopg2.extensions.connection | None:
|
||||
try:
|
||||
self.connection = psycopg2.connect(**self.config.to_dict())
|
||||
return self.connection
|
||||
except psycopg2.Error as e:
|
||||
print(f"Error connecting to database: {e}")
|
||||
return None
|
||||
39
src/secnex-cli/index.py
Normal file
39
src/secnex-cli/index.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
from database.conn import DatabaseConnection
|
||||
from cmd import TenantParser, TenantCommand, UserParser, UserCommand
|
||||
|
||||
class SecNexCLI:
|
||||
def __init__(self) -> None:
|
||||
self.__database_connection = DatabaseConnection(
|
||||
host="localhost",
|
||||
port=5432,
|
||||
database="api",
|
||||
user="postgres",
|
||||
password="postgres"
|
||||
)
|
||||
|
||||
if self.__database_connection.connect() is None:
|
||||
print("Failed to connect to database")
|
||||
sys.exit(1)
|
||||
|
||||
self.parser = argparse.ArgumentParser(description="SecNex CLI")
|
||||
self.parser.add_argument("--version", action="version", version="SecNex CLI 1.0.0", help="show version of cli and exit")
|
||||
self.subparsers = self.parser.add_subparsers(dest="command", help="available commands")
|
||||
TenantParser(self.subparsers.add_parser("tenant", help="manage tenants")).add_parser()
|
||||
UserParser(self.subparsers.add_parser("user", help="manage users")).add_parser()
|
||||
|
||||
def run(self) -> None:
|
||||
args = self.parser.parse_args()
|
||||
match args.command:
|
||||
case "tenant":
|
||||
TenantCommand(args.action, self.__database_connection).execute(args)
|
||||
case "user":
|
||||
UserCommand(args.action, self.__database_connection).execute(args)
|
||||
case _:
|
||||
print(f"Unknown command: {args.command}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli = SecNexCLI()
|
||||
cli.run()
|
||||
54
src/secnex-cli/utils/hash.py
Normal file
54
src/secnex-cli/utils/hash.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import argon2
|
||||
from argon2 import PasswordHasher
|
||||
from argon2.exceptions import VerifyMismatchError
|
||||
|
||||
class Argon2Hasher:
|
||||
def __init__(self) -> None:
|
||||
self.ph = PasswordHasher(
|
||||
time_cost=3, # Number of iterations
|
||||
memory_cost=65536, # Memory usage in KiB (64MB)
|
||||
parallelism=4, # Number of parallel threads
|
||||
hash_len=32, # Hash length in bytes
|
||||
salt_len=16 # Salt length in bytes
|
||||
)
|
||||
|
||||
def hash_password(self, password: str) -> str:
|
||||
"""
|
||||
Hash a password using Argon2.
|
||||
|
||||
Args:
|
||||
password: The plain text password to hash
|
||||
|
||||
Returns:
|
||||
The hashed password as a string
|
||||
"""
|
||||
return self.ph.hash(password)
|
||||
|
||||
def verify_password(self, hashed_password: str, plain_password: str) -> bool:
|
||||
"""
|
||||
Verify a plain password against a hashed password.
|
||||
|
||||
Args:
|
||||
hashed_password: The hashed password from the database
|
||||
plain_password: The plain text password to verify
|
||||
|
||||
Returns:
|
||||
True if the password is correct, False otherwise
|
||||
"""
|
||||
try:
|
||||
self.ph.verify(hashed_password, plain_password)
|
||||
return True
|
||||
except VerifyMismatchError:
|
||||
return False
|
||||
|
||||
def needs_rehash(self, hashed_password: str) -> bool:
|
||||
"""
|
||||
Check if a password needs to be rehashed with new parameters.
|
||||
|
||||
Args:
|
||||
hashed_password: The hashed password to check
|
||||
|
||||
Returns:
|
||||
True if the password needs rehashing, False otherwise
|
||||
"""
|
||||
return self.ph.check_needs_rehash(hashed_password)
|
||||
Reference in New Issue
Block a user