feat(ssh): change SSH key uniqueness to per-user scope

Previously, SSH key fingerprints were globally unique across all users,
preventing the same key from being registered by different users. This
change makes fingerprint uniqueness scoped to individual users.

- Remove global unique constraints on payload and fingerprint columns
- Add composite unique constraint on (user_id, fingerprint)
- Make add_ssh_key operation idempotent for same user
- Return tuple (SSHKey, is_new) from service to indicate creation status
- Update API to return 200 for existing keys, 201 for new keys

BREAKING CHANGE: API behavior changed - duplicate key addition now
returns 200 OK instead of 409 Conflict. Service method signature changed
from returning SSHKey to tuple[SSHKey, bool].
This commit is contained in:
2026-04-25 06:22:08 +09:30
parent cec04f3cb2
commit de6f39e7e3
5 changed files with 132 additions and 38 deletions
@@ -0,0 +1,44 @@
"""Per-user SSH key fingerprint uniqueness.
Revision ID: a1b2c3d4e5f6
Revises: 8f2d9e4a7c1b
Create Date: 2026-04-24 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a1b2c3d4e5f6'
down_revision = '8f2d9e4a7c1b'
branch_labels = None
depends_on = None
def upgrade():
# Drop the global unique constraint on payload
op.drop_constraint('ssh_keys_payload_key', 'ssh_keys', type_='unique')
# Drop the global unique index on fingerprint
op.drop_index('ix_ssh_keys_fingerprint', table_name='ssh_keys')
# Create a non-unique index on fingerprint for query performance
op.create_index(op.f('ix_ssh_keys_fingerprint'), 'ssh_keys', ['fingerprint'], unique=False)
# Add composite unique constraint for per-user fingerprint uniqueness
op.create_unique_constraint('uix_user_fingerprint', 'ssh_keys', ['user_id', 'fingerprint'])
def downgrade():
# Drop the composite unique constraint
op.drop_constraint('uix_user_fingerprint', 'ssh_keys', type_='unique')
# Drop the non-unique index
op.drop_index(op.f('ix_ssh_keys_fingerprint'), table_name='ssh_keys')
# Recreate the global unique index
op.create_index('ix_ssh_keys_fingerprint', 'ssh_keys', ['fingerprint'], unique=True)
# Recreate the global unique constraint on payload
op.create_unique_constraint('ssh_keys_payload_key', 'ssh_keys', ['payload'])