Files
ServerKit/backend/app/models/managed_database_user.py
T
Juan DenisandClaude Fable 5 76a688fb49 feat(databases): managed DB users and one-click Adminer SSO
managed_database_users rows (migration 050) track users/grants so they
survive restarts; SSO launches an on-demand Adminer container
pre-authenticated with a single-use, 5-minute shadow credential scoped
to one database, reaped on schedule. Also mounts the config tuner in
the explorer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:24:15 -04:00

78 lines
3.0 KiB
Python

import json
from datetime import datetime
from app import db
class ManagedDatabaseUser(db.Model):
"""A database user/grant ServerKit created on a managed database.
Live engines forget nothing, but ServerKit used to: users created through
the panel left no durable trace, so restarts lost the mapping between a
managed database and the credentials provisioned for it. This row is that
trace — it powers the users panel and scopes one-click admin SSO.
``is_shadow`` marks short-lived, single-use credentials (e.g. the Adminer
SSO login) that a reaper drops once ``expires_at`` passes. Passwords are
NEVER stored here — they cross the API exactly once at creation time.
"""
__tablename__ = 'managed_database_users'
id = db.Column(db.Integer, primary_key=True)
managed_database_id = db.Column(
db.Integer, db.ForeignKey('managed_databases.id', ondelete='CASCADE'),
nullable=False, index=True,
)
username = db.Column(db.String(120), nullable=False)
# JSON list of grant keywords, e.g. ["ALL"] or ["SELECT", "INSERT"].
grants = db.Column(db.Text, nullable=False, default='["ALL"]')
is_shadow = db.Column(db.Boolean, nullable=False, default=False)
expires_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Deleting the managed database removes its user rows (ORM cascade); the
# engine-side users are dropped explicitly by the service, never implicitly.
managed_database = db.relationship(
'ManagedDatabase',
backref=db.backref('users', cascade='all, delete-orphan'),
)
__table_args__ = (
db.UniqueConstraint('managed_database_id', 'username',
name='uq_managed_db_user'),
)
def get_grants(self):
try:
grants = json.loads(self.grants or '[]')
return grants if isinstance(grants, list) else []
except (ValueError, TypeError):
return []
def set_grants(self, grants):
self.grants = json.dumps(list(grants or []))
@property
def is_expired(self):
return bool(self.expires_at and self.expires_at < datetime.utcnow())
def to_dict(self, live=None):
data = {
'id': self.id,
'managed_database_id': self.managed_database_id,
'username': self.username,
'grants': self.get_grants(),
'is_shadow': self.is_shadow,
'expires_at': self.expires_at.isoformat() if self.expires_at else None,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
}
if live is not None:
data['present'] = live.get('present')
return data
def __repr__(self):
return f'<ManagedDatabaseUser {self.username} db={self.managed_database_id}>'