Write new secret manager using existing RSA logic
This commit is contained in:
@ -0,0 +1,33 @@
|
||||
"""Implement db structures for internal password manager
|
||||
|
||||
Revision ID: 1657c5d25d2c
|
||||
Revises: b4e135ff347a
|
||||
Create Date: 2025-06-21 07:22:17.792528
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '1657c5d25d2c'
|
||||
down_revision: Union[str, None] = 'b4e135ff347a'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('client', sa.Column('is_system', sa.Boolean(), nullable=False, default=False, server_default="0"))
|
||||
op.add_column('client_secret', sa.Column('is_system', sa.Boolean(), nullable=False, default=False, server_default="0"))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('client_secret', 'is_system')
|
||||
op.drop_column('client', 'is_system')
|
||||
@ -0,0 +1,44 @@
|
||||
"""Remove secret key from password database
|
||||
|
||||
Revision ID: 71f7272a6ee1
|
||||
Revises: 1657c5d25d2c
|
||||
Create Date: 2025-06-22 18:42:53.207334
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '71f7272a6ee1'
|
||||
down_revision: Union[str, None] = '1657c5d25d2c'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('managed_secret')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('managed_secret',
|
||||
sa.Column('id', sa.CHAR(length=32), nullable=False),
|
||||
sa.Column('name', sa.VARCHAR(), nullable=False),
|
||||
sa.Column('description', sa.VARCHAR(), nullable=True),
|
||||
sa.Column('secret', sa.VARCHAR(), nullable=False),
|
||||
sa.Column('client_id', sa.CHAR(length=32), nullable=True),
|
||||
sa.Column('deleted', sa.BOOLEAN(), nullable=False),
|
||||
sa.Column('created_at', sa.DATETIME(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.Column('updated_at', sa.DATETIME(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
||||
sa.Column('deleted_at', sa.DATETIME(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['client_id'], ['client.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
@ -107,8 +107,7 @@ class ClientOperations:
|
||||
return ClientView.from_client(db_client)
|
||||
|
||||
async def create_client(
|
||||
self,
|
||||
create_model: ClientCreate,
|
||||
self, create_model: ClientCreate, system_client: bool = False
|
||||
) -> ClientView:
|
||||
"""Create a new client."""
|
||||
existing_id = await self.get_client_id(FlexID.name(create_model.name))
|
||||
@ -117,6 +116,15 @@ class ClientOperations:
|
||||
status_code=400, detail="Error: A client already exists with this name."
|
||||
)
|
||||
client = create_model.to_client()
|
||||
if system_client:
|
||||
statement = query_active_clients().where(Client.is_system.is_(True))
|
||||
results = await self.session.scalars(statement)
|
||||
other_system_clients = results.all()
|
||||
if other_system_clients:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Only one system client may exist"
|
||||
)
|
||||
client.is_system = True
|
||||
self.session.add(client)
|
||||
await self.session.flush()
|
||||
await self.session.commit()
|
||||
@ -246,6 +254,15 @@ class ClientOperations:
|
||||
|
||||
return ClientPolicyView.from_client(db_client)
|
||||
|
||||
async def get_system_client(self) -> ClientView:
|
||||
"""Get the system client, if it exists."""
|
||||
statement = query_active_clients().where(Client.is_system.is_(True))
|
||||
result = await self.session.scalars(statement)
|
||||
client = result.first()
|
||||
if not client:
|
||||
raise HTTPException(status_code=404, detail="No system client registered")
|
||||
return ClientView.from_client(client)
|
||||
|
||||
|
||||
def resolve_order(statement: Select[Any], order_by: str, reversed: bool) -> Select[Any]:
|
||||
"""Resolve ordering."""
|
||||
@ -261,12 +278,13 @@ def resolve_order(statement: Select[Any], order_by: str, reversed: bool) -> Sele
|
||||
statement = statement.order_by(column.desc())
|
||||
else:
|
||||
statement = statement.order_by(column.asc())
|
||||
#FIXME: Remove
|
||||
# FIXME: Remove
|
||||
LOG.info("Ordered by %s (%r)", order_by, reversed)
|
||||
return statement
|
||||
LOG.warning("Unsupported order field: %s", order_by)
|
||||
return statement
|
||||
|
||||
|
||||
def filter_client_statement(
|
||||
statement: Select[Any], params: ClientListParams, ignore_limits: bool = False
|
||||
) -> Select[Any]:
|
||||
@ -299,6 +317,7 @@ async def get_clients(
|
||||
.select_from(Client)
|
||||
.where(Client.is_deleted.is_not(True))
|
||||
.where(Client.is_active.is_not(False))
|
||||
.where(Client.is_system.is_not(True))
|
||||
)
|
||||
count_statement = cast(
|
||||
Select[tuple[int]],
|
||||
@ -307,7 +326,8 @@ async def get_clients(
|
||||
|
||||
total_results = (await session.scalars(count_statement)).one()
|
||||
|
||||
statement = filter_client_statement(query_active_clients(), filter_query, False)
|
||||
statement = query_active_clients().where(Client.is_system.is_not(True))
|
||||
statement = filter_client_statement(statement, filter_query, False)
|
||||
|
||||
results = await session.scalars(statement)
|
||||
remainder = total_results - filter_query.offset - filter_query.limit
|
||||
|
||||
@ -46,6 +46,25 @@ def create_client_router(get_db_session: AsyncDBSessionDep) -> APIRouter:
|
||||
client_op = ClientOperations(session, request)
|
||||
return await client_op.create_client(client)
|
||||
|
||||
@router.get("/internal/system_client/", include_in_schema=False)
|
||||
async def get_system_client(
|
||||
request: Request,
|
||||
session: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
) -> ClientView:
|
||||
"""Get the system client."""
|
||||
client_op = ClientOperations(session, request)
|
||||
return await client_op.get_system_client()
|
||||
|
||||
@router.post("/internal/system_client/", include_in_schema=False)
|
||||
async def create_system_client(
|
||||
request: Request,
|
||||
client: ClientCreate,
|
||||
session: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
) -> ClientView:
|
||||
"""Create system client."""
|
||||
client_op = ClientOperations(session, request)
|
||||
return await client_op.create_client(client, system_client=True)
|
||||
|
||||
@router.get("/clients/{client_identifier}")
|
||||
async def fetch_client_by_name(
|
||||
request: Request,
|
||||
|
||||
@ -242,7 +242,7 @@ async def resolve_client_secret_clients(
|
||||
# Ensure we don't create the object before we have at least one client.
|
||||
clients = ClientSecretDetailList(name=name)
|
||||
clients.ids.append(str(client_secret.id))
|
||||
if client_secret.client:
|
||||
if client_secret.client and not client_secret.client.is_system:
|
||||
clients.clients.append(
|
||||
ClientReference(
|
||||
id=str(client_secret.client.id), name=client_secret.client.name
|
||||
|
||||
@ -110,7 +110,6 @@ def get_async_engine(url: URL, echo: bool = False, **engine_kwargs: str) -> Asyn
|
||||
"""Get an async engine."""
|
||||
engine = create_async_engine(url, echo=echo, **engine_kwargs)
|
||||
if url.drivername.startswith("sqlite+"):
|
||||
|
||||
@event.listens_for(engine.sync_engine, "connect")
|
||||
def set_sqlite_pragma(
|
||||
dbapi_connection: sqlite3.Connection, _connection_record: object
|
||||
|
||||
@ -67,6 +67,7 @@ class Client(Base):
|
||||
|
||||
is_active: Mapped[bool] = mapped_column(sa.Boolean, default=True)
|
||||
is_deleted: Mapped[bool] = mapped_column(sa.Boolean, default=False)
|
||||
is_system: Mapped[bool] = mapped_column(sa.Boolean, default=False)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
|
||||
@ -141,6 +142,8 @@ class ClientSecret(Base):
|
||||
client: Mapped[Client] = relationship(back_populates="secrets")
|
||||
deleted: Mapped[bool] = mapped_column(default=False)
|
||||
|
||||
is_system: Mapped[bool] = mapped_column(sa.Boolean, default=False)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user