Add terminal session management with heartbeat monitoring, idle timeout detection, session reuse logic, and command history panel UI with search and filtering capabilities
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""Add terminal_command_logs table for command history
|
||||
|
||||
Revision ID: 0014
|
||||
Revises: 0013_add_terminal_sessions_table
|
||||
Create Date: 2024-12-18
|
||||
|
||||
This migration creates the terminal_command_logs table to store
|
||||
validated terminal commands for history and audit purposes.
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0014'
|
||||
down_revision = '0013'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def table_exists(table_name: str) -> bool:
|
||||
"""Check if a table exists in the database."""
|
||||
bind = op.get_bind()
|
||||
inspector = inspect(bind)
|
||||
return table_name in inspector.get_table_names()
|
||||
|
||||
|
||||
def index_exists(index_name: str, table_name: str) -> bool:
|
||||
"""Check if an index exists on a table."""
|
||||
bind = op.get_bind()
|
||||
inspector = inspect(bind)
|
||||
indexes = inspector.get_indexes(table_name)
|
||||
return any(idx['name'] == index_name for idx in indexes)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Check if table already exists (idempotent migration)
|
||||
if table_exists('terminal_command_logs'):
|
||||
print("Table 'terminal_command_logs' already exists, skipping creation.")
|
||||
else:
|
||||
op.create_table(
|
||||
'terminal_command_logs',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column('host_id', sa.String(), nullable=False),
|
||||
sa.Column('user_id', sa.String(), nullable=True),
|
||||
sa.Column('terminal_session_id', sa.String(64), nullable=True),
|
||||
sa.Column('command', sa.Text(), nullable=False),
|
||||
sa.Column('command_hash', sa.String(64), nullable=False),
|
||||
sa.Column('source', sa.String(20), server_default='terminal', nullable=False),
|
||||
sa.Column('is_blocked', sa.Boolean(), server_default='0', nullable=False),
|
||||
sa.Column('blocked_reason', sa.String(255), nullable=True),
|
||||
sa.Column('username', sa.String(100), nullable=True),
|
||||
sa.Column('host_name', sa.String(100), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.ForeignKeyConstraint(['host_id'], ['hosts.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='SET NULL'),
|
||||
)
|
||||
|
||||
# Create indexes for efficient querying (only if they don't exist)
|
||||
if not index_exists('ix_terminal_command_logs_created_at', 'terminal_command_logs'):
|
||||
op.create_index('ix_terminal_command_logs_created_at', 'terminal_command_logs', ['created_at'])
|
||||
if not index_exists('ix_terminal_command_logs_host_id', 'terminal_command_logs'):
|
||||
op.create_index('ix_terminal_command_logs_host_id', 'terminal_command_logs', ['host_id'])
|
||||
if not index_exists('ix_terminal_command_logs_user_id', 'terminal_command_logs'):
|
||||
op.create_index('ix_terminal_command_logs_user_id', 'terminal_command_logs', ['user_id'])
|
||||
if not index_exists('ix_terminal_command_logs_terminal_session_id', 'terminal_command_logs'):
|
||||
op.create_index('ix_terminal_command_logs_terminal_session_id', 'terminal_command_logs', ['terminal_session_id'])
|
||||
if not index_exists('ix_terminal_command_logs_command_hash', 'terminal_command_logs'):
|
||||
op.create_index('ix_terminal_command_logs_command_hash', 'terminal_command_logs', ['command_hash'])
|
||||
|
||||
# Composite indexes for common query patterns
|
||||
if not index_exists('ix_terminal_cmd_host_created', 'terminal_command_logs'):
|
||||
op.create_index('ix_terminal_cmd_host_created', 'terminal_command_logs', ['host_id', 'created_at'])
|
||||
if not index_exists('ix_terminal_cmd_user_created', 'terminal_command_logs'):
|
||||
op.create_index('ix_terminal_cmd_user_created', 'terminal_command_logs', ['user_id', 'created_at'])
|
||||
if not index_exists('ix_terminal_cmd_host_hash', 'terminal_command_logs'):
|
||||
op.create_index('ix_terminal_cmd_host_hash', 'terminal_command_logs', ['host_id', 'command_hash'])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Drop indexes
|
||||
op.drop_index('ix_terminal_cmd_host_hash', table_name='terminal_command_logs')
|
||||
op.drop_index('ix_terminal_cmd_user_created', table_name='terminal_command_logs')
|
||||
op.drop_index('ix_terminal_cmd_host_created', table_name='terminal_command_logs')
|
||||
op.drop_index('ix_terminal_command_logs_command_hash', table_name='terminal_command_logs')
|
||||
op.drop_index('ix_terminal_command_logs_terminal_session_id', table_name='terminal_command_logs')
|
||||
op.drop_index('ix_terminal_command_logs_user_id', table_name='terminal_command_logs')
|
||||
op.drop_index('ix_terminal_command_logs_host_id', table_name='terminal_command_logs')
|
||||
op.drop_index('ix_terminal_command_logs_created_at', table_name='terminal_command_logs')
|
||||
|
||||
# Drop table
|
||||
op.drop_table('terminal_command_logs')
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Add last_seen_at and reason_closed to terminal_sessions
|
||||
|
||||
Revision ID: 0015
|
||||
Revises: 0014
|
||||
Create Date: 2024-12-18
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '0015'
|
||||
down_revision: Union[str, None] = '0014'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# NOTE: SQLite cannot do `ALTER TABLE ... ADD COLUMN ... DEFAULT CURRENT_TIMESTAMP`.
|
||||
# We use batch mode to recreate the table safely.
|
||||
|
||||
with op.batch_alter_table('terminal_sessions', recreate='always') as batch_op:
|
||||
batch_op.add_column(sa.Column('last_seen_at', sa.DateTime(timezone=True), nullable=True))
|
||||
batch_op.add_column(sa.Column('reason_closed', sa.String(30), nullable=True))
|
||||
|
||||
# Backfill last_seen_at from created_at for existing rows
|
||||
op.execute("UPDATE terminal_sessions SET last_seen_at = created_at WHERE last_seen_at IS NULL")
|
||||
|
||||
# Enforce NOT NULL + default for future inserts (recreate table again for SQLite)
|
||||
with op.batch_alter_table('terminal_sessions', recreate='always') as batch_op:
|
||||
batch_op.alter_column('last_seen_at', nullable=False, server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
|
||||
# Create indexes for efficient queries
|
||||
op.create_index('ix_terminal_sessions_user_status', 'terminal_sessions', ['user_id', 'status'])
|
||||
op.create_index('ix_terminal_sessions_last_seen', 'terminal_sessions', ['last_seen_at'])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Drop indexes
|
||||
op.drop_index('ix_terminal_sessions_last_seen', table_name='terminal_sessions')
|
||||
op.drop_index('ix_terminal_sessions_user_status', table_name='terminal_sessions')
|
||||
|
||||
# Drop columns (batch mode for SQLite)
|
||||
with op.batch_alter_table('terminal_sessions', recreate='always') as batch_op:
|
||||
batch_op.drop_column('reason_closed')
|
||||
batch_op.drop_column('last_seen_at')
|
||||
Reference in New Issue
Block a user