Feat: Added CA-merged with Securid-Principals, Depart, Client-CLI

This commit is contained in:
2026-02-27 21:59:01 +05:45
parent 92fd57447d
commit b2212ab4d6
29 changed files with 3718 additions and 53 deletions
@@ -0,0 +1,173 @@
"""Add SSH CA models: SSHKey, SSHCertificate, CA, CertificateAuditLog.
Revision ID: 007
Revises: 006
Create Date: 2026-02-27 11:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '007'
down_revision = '006'
branch_labels = None
depends_on = None
def upgrade():
# ### CA table ###
op.create_table('cas',
sa.Column('organization_id', sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('key_type', sa.Enum('ed25519', 'rsa', 'ecdsa', name='ca_key_type_enum'), nullable=False),
sa.Column('private_key', sa.Text(), nullable=False),
sa.Column('public_key', sa.Text(), nullable=False),
sa.Column('fingerprint', sa.String(length=255), nullable=False),
sa.Column('crl_enabled', sa.Boolean(), nullable=False),
sa.Column('crl_endpoint', sa.String(length=512), nullable=True),
sa.Column('default_cert_validity_hours', sa.Integer(), nullable=False),
sa.Column('max_cert_validity_hours', sa.Integer(), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('rotated_at', sa.DateTime(), nullable=True),
sa.Column('rotation_reason', sa.String(length=255), nullable=True),
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('id'),
sa.UniqueConstraint('fingerprint'),
sa.UniqueConstraint('organization_id', 'name', name='uix_org_ca_name')
)
op.create_index(op.f('ix_cas_organization_id'), 'cas', ['organization_id'], unique=False)
op.create_index('idx_ca_org_active', 'cas', ['organization_id', 'is_active'], unique=False)
# ### SSHKey table ###
op.create_table('ssh_keys',
sa.Column('user_id', sa.String(length=36), nullable=False),
sa.Column('payload', sa.Text(), nullable=False),
sa.Column('fingerprint', sa.String(length=255), nullable=False),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('verified', sa.Boolean(), nullable=False),
sa.Column('verified_at', sa.DateTime(), nullable=True),
sa.Column('verify_text', sa.String(length=255), nullable=True),
sa.Column('verify_text_created_at', sa.DateTime(), nullable=True),
sa.Column('key_type', sa.String(length=50), nullable=True),
sa.Column('key_bits', sa.Integer(), nullable=True),
sa.Column('key_comment', sa.String(length=255), nullable=True),
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('id'),
sa.UniqueConstraint('payload'),
sa.UniqueConstraint('fingerprint')
)
op.create_index(op.f('ix_ssh_keys_user_id'), 'ssh_keys', ['user_id'], unique=False)
op.create_index(op.f('ix_ssh_keys_fingerprint'), 'ssh_keys', ['fingerprint'], unique=False)
op.create_index(op.f('ix_ssh_keys_verified'), 'ssh_keys', ['verified'], unique=False)
op.create_index('idx_ssh_key_user_verified', 'ssh_keys', ['user_id', 'verified'], unique=False)
# ### SSHCertificate table ###
op.create_table('ssh_certificates',
sa.Column('ca_id', sa.String(length=36), nullable=False),
sa.Column('user_id', sa.String(length=36), nullable=False),
sa.Column('ssh_key_id', sa.String(length=36), nullable=False),
sa.Column('certificate', sa.Text(), nullable=False),
sa.Column('serial', sa.String(length=255), nullable=False),
sa.Column('key_id', sa.String(length=255), nullable=False),
sa.Column('cert_type', sa.Enum('user', 'host', name='ssh_cert_type_enum'), nullable=False),
sa.Column('principals', sa.JSON(), nullable=False),
sa.Column('valid_after', sa.DateTime(), nullable=False),
sa.Column('valid_before', sa.DateTime(), nullable=False),
sa.Column('revoked', sa.Boolean(), nullable=False),
sa.Column('revoked_at', sa.DateTime(), nullable=True),
sa.Column('revoke_reason', sa.String(length=255), nullable=True),
sa.Column('status', sa.Enum('requested', 'issued', 'revoked', 'expired', 'superseded', name='ssh_cert_status_enum'), nullable=False),
sa.Column('request_ip', sa.String(length=45), nullable=True),
sa.Column('request_user_agent', sa.String(length=512), nullable=True),
sa.Column('critical_options', sa.JSON(), nullable=True),
sa.Column('extensions', sa.JSON(), nullable=True),
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['ca_id'], ['cas.id'], ),
sa.ForeignKeyConstraint(['ssh_key_id'], ['ssh_keys.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('id'),
sa.UniqueConstraint('serial')
)
op.create_index(op.f('ix_ssh_certificates_ca_id'), 'ssh_certificates', ['ca_id'], unique=False)
op.create_index(op.f('ix_ssh_certificates_user_id'), 'ssh_certificates', ['user_id'], unique=False)
op.create_index(op.f('ix_ssh_certificates_ssh_key_id'), 'ssh_certificates', ['ssh_key_id'], unique=False)
op.create_index(op.f('ix_ssh_certificates_serial'), 'ssh_certificates', ['serial'], unique=False)
op.create_index(op.f('ix_ssh_certificates_revoked'), 'ssh_certificates', ['revoked'], unique=False)
op.create_index(op.f('ix_ssh_certificates_status'), 'ssh_certificates', ['status'], unique=False)
op.create_index('idx_cert_user_status', 'ssh_certificates', ['user_id', 'status'], unique=False)
op.create_index('idx_cert_validity', 'ssh_certificates', ['valid_after', 'valid_before'], unique=False)
op.create_index('idx_cert_revoked', 'ssh_certificates', ['revoked', 'revoked_at'], unique=False)
# ### CertificateAuditLog table ###
op.create_table('certificate_audit_logs',
sa.Column('certificate_id', sa.String(length=36), nullable=False),
sa.Column('user_id', sa.String(length=36), nullable=True),
sa.Column('action', sa.String(length=50), nullable=False),
sa.Column('ip_address', sa.String(length=45), nullable=True),
sa.Column('user_agent', sa.String(length=512), nullable=True),
sa.Column('request_id', sa.String(length=36), nullable=True),
sa.Column('message', sa.Text(), nullable=True),
sa.Column('extra_data', sa.JSON(), nullable=True),
sa.Column('success', sa.Boolean(), nullable=False),
sa.Column('error_message', sa.Text(), nullable=True),
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['certificate_id'], ['ssh_certificates.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('id')
)
op.create_index(op.f('ix_certificate_audit_logs_certificate_id'), 'certificate_audit_logs', ['certificate_id'], unique=False)
op.create_index(op.f('ix_certificate_audit_logs_user_id'), 'certificate_audit_logs', ['user_id'], unique=False)
op.create_index(op.f('ix_certificate_audit_logs_action'), 'certificate_audit_logs', ['action'], unique=False)
op.create_index('idx_cert_audit_cert_action', 'certificate_audit_logs', ['certificate_id', 'action'], unique=False)
op.create_index('idx_cert_audit_user', 'certificate_audit_logs', ['user_id', 'created_at'], unique=False)
def downgrade():
op.drop_index('idx_cert_audit_user', table_name='certificate_audit_logs')
op.drop_index('idx_cert_audit_cert_action', table_name='certificate_audit_logs')
op.drop_index(op.f('ix_certificate_audit_logs_action'), table_name='certificate_audit_logs')
op.drop_index(op.f('ix_certificate_audit_logs_user_id'), table_name='certificate_audit_logs')
op.drop_index(op.f('ix_certificate_audit_logs_certificate_id'), table_name='certificate_audit_logs')
op.drop_table('certificate_audit_logs')
op.drop_index('idx_cert_revoked', table_name='ssh_certificates')
op.drop_index('idx_cert_validity', table_name='ssh_certificates')
op.drop_index('idx_cert_user_status', table_name='ssh_certificates')
op.drop_index(op.f('ix_ssh_certificates_status'), table_name='ssh_certificates')
op.drop_index(op.f('ix_ssh_certificates_revoked'), table_name='ssh_certificates')
op.drop_index(op.f('ix_ssh_certificates_serial'), table_name='ssh_certificates')
op.drop_index(op.f('ix_ssh_certificates_ssh_key_id'), table_name='ssh_certificates')
op.drop_index(op.f('ix_ssh_certificates_user_id'), table_name='ssh_certificates')
op.drop_index(op.f('ix_ssh_certificates_ca_id'), table_name='ssh_certificates')
op.drop_table('ssh_certificates')
op.drop_index('idx_ssh_key_user_verified', table_name='ssh_keys')
op.drop_index(op.f('ix_ssh_keys_verified'), table_name='ssh_keys')
op.drop_index(op.f('ix_ssh_keys_fingerprint'), table_name='ssh_keys')
op.drop_index(op.f('ix_ssh_keys_user_id'), table_name='ssh_keys')
op.drop_table('ssh_keys')
op.drop_index('idx_ca_org_active', table_name='cas')
op.drop_index(op.f('ix_cas_organization_id'), table_name='cas')
op.drop_table('cas')
@@ -0,0 +1,53 @@
"""Add TOTP and WEBAUTHN to authmethodtype enum.
Revision ID: 008
Revises: 007
Create Date: 2026-02-27 15:00:00.000000
The original migration (001_base) created authmethodtype with only:
PASSWORD, GOOGLE, GITHUB, MICROSOFT, SAML, OIDC
This migration adds the missing TOTP and WEBAUTHN values so
has_totp_enabled() and has_webauthn_enabled() queries work correctly.
"""
from alembic import op
import sqlalchemy as sa
revision = '008'
down_revision = '007'
branch_labels = None
depends_on = None
def upgrade():
# Add TOTP to the enum (idempotent approach using DO block)
op.execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_enum
WHERE enumlabel = 'TOTP'
AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'authmethodtype')
) THEN
ALTER TYPE authmethodtype ADD VALUE 'TOTP';
END IF;
END$$;
""")
op.execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_enum
WHERE enumlabel = 'WEBAUTHN'
AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'authmethodtype')
) THEN
ALTER TYPE authmethodtype ADD VALUE 'WEBAUTHN';
END IF;
END$$;
""")
def downgrade():
# PostgreSQL does not support removing enum values; downgrade is a no-op.
pass
@@ -0,0 +1,61 @@
"""Sync auditaction enum with all AuditAction Python enum values.
Revision ID: 009
Revises: 008
Create Date: 2026-02-27 15:20:00.000000
The auditaction DB enum was only created with the initial 17 values from 001_base.py.
All TOTP, WebAuthn, OAuth, SSH, CA, Principal, and Department audit actions were added
to the Python enum but never synced to the DB type.
"""
from alembic import op
revision = '009'
down_revision = '008'
branch_labels = None
depends_on = None
MISSING_VALUES = [
'TOTP_ENROLL_INITIATED', 'TOTP_ENROLL_COMPLETED', 'TOTP_VERIFY_SUCCESS',
'TOTP_VERIFY_FAILED', 'TOTP_DISABLED', 'TOTP_BACKUP_CODE_USED',
'TOTP_BACKUP_CODES_REGENERATED', 'WEBAUTHN_REGISTER_INITIATED',
'WEBAUTHN_REGISTER_COMPLETED', 'WEBAUTHN_REGISTER_FAILED',
'WEBAUTHN_LOGIN_INITIATED', 'WEBAUTHN_LOGIN_SUCCESS', 'WEBAUTHN_LOGIN_FAILED',
'WEBAUTHN_CREDENTIAL_DELETED', 'WEBAUTHN_CREDENTIAL_RENAMED',
'ORG_SECURITY_POLICY_UPDATE', 'USER_SECURITY_POLICY_OVERRIDE_UPDATE',
'MFA_POLICY_USER_SUSPENDED', 'MFA_POLICY_USER_COMPLIANT',
'EXTERNAL_AUTH_LINK_INITIATED', 'EXTERNAL_AUTH_LINK_COMPLETED',
'EXTERNAL_AUTH_LINK_FAILED', 'EXTERNAL_AUTH_UNLINK', 'EXTERNAL_AUTH_LOGIN',
'EXTERNAL_AUTH_LOGIN_FAILED', 'EXTERNAL_AUTH_TOKEN_REFRESH',
'EXTERNAL_AUTH_CONFIG_CREATE', 'EXTERNAL_AUTH_CONFIG_UPDATE',
'EXTERNAL_AUTH_CONFIG_DELETE', 'SSH_KEY_ADDED', 'SSH_KEY_VERIFIED',
'SSH_KEY_DELETED', 'SSH_KEY_VALIDATION_FAILED', 'SSH_CERT_REQUESTED',
'SSH_CERT_ISSUED', 'SSH_CERT_FAILED', 'SSH_CERT_REVOKED', 'SSH_CERT_EXPIRED',
'CA_CREATED', 'CA_UPDATED', 'CA_DELETED', 'CA_KEY_ROTATED',
'PRINCIPAL_CREATED', 'PRINCIPAL_UPDATED', 'PRINCIPAL_DELETED',
'PRINCIPAL_MEMBER_ADDED', 'PRINCIPAL_MEMBER_REMOVED',
'DEPARTMENT_CREATED', 'DEPARTMENT_UPDATED', 'DEPARTMENT_DELETED',
'DEPARTMENT_MEMBER_ADDED', 'DEPARTMENT_MEMBER_REMOVED',
]
def upgrade():
for val in MISSING_VALUES:
op.execute(f"""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_enum
WHERE enumlabel = '{val}'
AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'auditaction')
) THEN
ALTER TYPE auditaction ADD VALUE '{val}';
END IF;
END$$;
""")
def downgrade():
# PostgreSQL does not support removing enum values; downgrade is a no-op.
pass
@@ -0,0 +1,33 @@
"""Make CA.organization_id nullable (system CA) and add cert_id to sign response
Revision ID: 012_ca_nullable_org_and_cert_serial
Revises: 011_org_invite_tokens
Create Date: 2025-01-01 00:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
revision = '012_ca_nullable_org'
down_revision = '011_org_invite_tokens'
branch_labels = None
depends_on = None
def upgrade():
# Allow CA records without an org (e.g. the global system-config CA)
with op.batch_alter_table('cas', schema=None) as batch_op:
batch_op.alter_column(
'organization_id',
existing_type=sa.String(36),
nullable=True,
)
def downgrade():
with op.batch_alter_table('cas', schema=None) as batch_op:
batch_op.alter_column(
'organization_id',
existing_type=sa.String(36),
nullable=False,
)