dn-scribe commited on
Commit
c255b46
·
1 Parent(s): 83435ca

Fix: bootstrap sequence, sqlite database detection, and migrations

Browse files
.DS_Store DELETED
Binary file (8.2 kB)
 
.gitignore CHANGED
@@ -14,6 +14,7 @@ __pycache__/
14
  attstore/
15
 
16
  .DS_Store/
 
17
  node_modules/
18
 
19
  # Documentation (pulled during build or via download script)
 
14
  attstore/
15
 
16
  .DS_Store/
17
+ *.DS_Store
18
  node_modules/
19
 
20
  # Documentation (pulled during build or via download script)
app.py CHANGED
@@ -22,6 +22,7 @@ jwt = JWTManager()
22
 
23
  # Create the app
24
  app = Flask(__name__)
 
25
  # Ensure templates auto-reload for development
26
  app.config['TEMPLATES_AUTO_RELOAD'] = True
27
  app.jinja_env.auto_reload = True
@@ -51,17 +52,13 @@ jwt.init_app(app)
51
  migrate = Migrate(app, db)
52
 
53
  # Import config and setup
54
- from config import setup_config, create_default_admin_user
55
  setup_config(app)
56
 
57
- with app.app_context():
58
- # Import models to ensure tables are created
59
- import models
60
- db.create_all()
61
-
62
- # Note: Default admin user creation moved to main.py after migrations
63
-
64
- # Import and register blueprints
65
  from api.evidence import evidence_bp
66
  from api.login import login_bp
67
  from api.syslog import syslog_bp
@@ -84,6 +81,8 @@ with app.app_context():
84
  app.register_blueprint(saml_config_bp)
85
  app.register_blueprint(saml_bp)
86
 
 
 
87
  # Public health endpoint (no auth)
88
  @app.get('/healthz')
89
  def healthz():
 
22
 
23
  # Create the app
24
  app = Flask(__name__)
25
+ os.makedirs(app.instance_path, exist_ok=True)
26
  # Ensure templates auto-reload for development
27
  app.config['TEMPLATES_AUTO_RELOAD'] = True
28
  app.jinja_env.auto_reload = True
 
52
  migrate = Migrate(app, db)
53
 
54
  # Import config and setup
55
+ from config import setup_config
56
  setup_config(app)
57
 
58
+ # Import models so SQLAlchemy metadata is populated for migrations
59
+ import models # noqa: E402,F401
60
+
61
+ def register_blueprints(app):
 
 
 
 
62
  from api.evidence import evidence_bp
63
  from api.login import login_bp
64
  from api.syslog import syslog_bp
 
81
  app.register_blueprint(saml_config_bp)
82
  app.register_blueprint(saml_bp)
83
 
84
+ register_blueprints(app)
85
+
86
  # Public health endpoint (no auth)
87
  @app.get('/healthz')
88
  def healthz():
main.py CHANGED
@@ -1,6 +1,11 @@
1
- from app import app
2
- from config import PORT
3
  import logging
 
 
 
 
 
 
 
4
  from werkzeug.serving import WSGIRequestHandler
5
  # --- Alembic migration runner ---
6
  from alembic import command
@@ -18,6 +23,44 @@ def run_migrations():
18
  command.upgrade(alembic_cfg, "head")
19
 
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  # Filter out specific paths from Werkzeug logs
22
  class FilteredWSGIRequestHandler(WSGIRequestHandler):
23
  def log(self, type, message, *args):
@@ -26,31 +69,13 @@ class FilteredWSGIRequestHandler(WSGIRequestHandler):
26
  return
27
  return super().log(type, message, *args)
28
 
29
- import os
30
- from app import db
31
-
32
- def db_file_exists():
33
- # Only works for SQLite
34
- db_uri = app.config.get("SQLALCHEMY_DATABASE_URI", "")
35
- if db_uri.startswith("sqlite:///"):
36
- db_path = db_uri.replace("sqlite:///", "")
37
- return os.path.exists(db_path)
38
- return True # For non-SQLite, always run migrations
39
-
40
  if __name__ == '__main__':
41
  from config import create_default_admin_user
42
-
43
- if not db_file_exists():
44
- with app.app_context():
45
- db.create_all()
46
- print("Database created, skipping migrations.")
47
- # Create default admin user for new database
48
- create_default_admin_user(app)
49
- else:
50
- run_migrations() # Ensure DB is migrated before serving requests
51
- # Create default admin user after migrations
52
- with app.app_context():
53
- create_default_admin_user(app)
54
-
55
  # Use filtered request handler
56
  app.run(host='0.0.0.0', port=PORT, debug=True, request_handler=FilteredWSGIRequestHandler)
 
 
 
1
  import logging
2
+ import os
3
+
4
+ from sqlalchemy import inspect
5
+ from sqlalchemy.engine.url import make_url
6
+
7
+ from app import app, db
8
+ from config import PORT
9
  from werkzeug.serving import WSGIRequestHandler
10
  # --- Alembic migration runner ---
11
  from alembic import command
 
23
  command.upgrade(alembic_cfg, "head")
24
 
25
 
26
+ def ensure_sqlite_database():
27
+ """Bootstrap SQLite DB files before Alembic runs."""
28
+ db_uri = app.config.get("SQLALCHEMY_DATABASE_URI", "")
29
+ if not db_uri.startswith("sqlite"):
30
+ return
31
+
32
+ url = make_url(db_uri)
33
+ database = url.database
34
+ if not database:
35
+ return
36
+
37
+ if os.path.isabs(database):
38
+ db_path = database
39
+ else:
40
+ db_path = os.path.join(app.instance_path, database)
41
+
42
+ os.makedirs(os.path.dirname(db_path), exist_ok=True)
43
+
44
+ bootstrap_reason = None
45
+ if not os.path.exists(db_path):
46
+ bootstrap_reason = "file missing"
47
+ else:
48
+ with app.app_context():
49
+ inspector = inspect(db.engine)
50
+ tables = inspector.get_table_names()
51
+ if 'users' not in tables:
52
+ bootstrap_reason = "schema missing"
53
+
54
+ if not bootstrap_reason:
55
+ logging.info(f"SQLite database already initialized at {db_path}")
56
+ return
57
+
58
+ logging.info(f"Bootstrapping SQLite database at {db_path} ({bootstrap_reason})")
59
+ with app.app_context():
60
+ db.create_all()
61
+ logging.info("SQLite database initialization complete")
62
+
63
+
64
  # Filter out specific paths from Werkzeug logs
65
  class FilteredWSGIRequestHandler(WSGIRequestHandler):
66
  def log(self, type, message, *args):
 
69
  return
70
  return super().log(type, message, *args)
71
 
 
 
 
 
 
 
 
 
 
 
 
72
  if __name__ == '__main__':
73
  from config import create_default_admin_user
74
+
75
+ ensure_sqlite_database()
76
+ run_migrations() # Always ensure DB schema is up to date before serving
77
+ with app.app_context():
78
+ create_default_admin_user(app)
79
+
 
 
 
 
 
 
 
80
  # Use filtered request handler
81
  app.run(host='0.0.0.0', port=PORT, debug=True, request_handler=FilteredWSGIRequestHandler)
migrations/versions/d2485e8b5d85_add_saml_support.py CHANGED
@@ -7,6 +7,7 @@ Create Date: 2025-12-08 11:21:20.267870
7
  """
8
  from alembic import op
9
  import sqlalchemy as sa
 
10
 
11
 
12
  # revision identifiers, used by Alembic.
@@ -17,20 +18,148 @@ depends_on = None
17
 
18
 
19
  def upgrade():
20
- # ### commands auto generated by Alembic - please adjust! ###
21
- with op.batch_alter_table('users', schema=None) as batch_op:
22
- batch_op.add_column(sa.Column('tenant_id', sa.Integer(), nullable=True))
23
- batch_op.create_index('ix_users_tenant_id', ['tenant_id'], unique=False)
24
- batch_op.create_foreign_key(None, 'tenants', ['tenant_id'], ['id'])
25
-
26
- # ### end Alembic commands ###
27
 
28
 
29
  def downgrade():
30
- # ### commands auto generated by Alembic - please adjust! ###
31
  with op.batch_alter_table('users', schema=None) as batch_op:
32
- batch_op.drop_constraint(None, type_='foreignkey')
33
  batch_op.drop_index('ix_users_tenant_id')
34
  batch_op.drop_column('tenant_id')
35
 
36
- # ### end Alembic commands ###
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  """
8
  from alembic import op
9
  import sqlalchemy as sa
10
+ from sqlalchemy import inspect
11
 
12
 
13
  # revision identifiers, used by Alembic.
 
18
 
19
 
20
  def upgrade():
21
+ """Ensure all SAML/tenant tables and relationships exist."""
22
+ bind = op.get_bind()
23
+ ensure_tenants(bind)
24
+ ensure_saml_configurations(bind)
25
+ ensure_external_identities(bind)
26
+ ensure_users_tenant_relationship(bind)
 
27
 
28
 
29
  def downgrade():
 
30
  with op.batch_alter_table('users', schema=None) as batch_op:
31
+ batch_op.drop_constraint('fk_users_tenant_id_tenants', type_='foreignkey')
32
  batch_op.drop_index('ix_users_tenant_id')
33
  batch_op.drop_column('tenant_id')
34
 
35
+ op.drop_table('external_identities')
36
+ op.drop_table('saml_configurations')
37
+ op.drop_table('tenants')
38
+
39
+
40
+ def ensure_tenants(bind):
41
+ inspector = inspect(bind)
42
+ tables = inspector.get_table_names()
43
+ if 'tenants' not in tables:
44
+ op.create_table(
45
+ 'tenants',
46
+ sa.Column('id', sa.Integer(), primary_key=True),
47
+ sa.Column('name', sa.String(length=128), nullable=False),
48
+ sa.Column('slug', sa.String(length=64), nullable=False, unique=True),
49
+ sa.Column('description', sa.Text(), nullable=True),
50
+ sa.Column('active', sa.Boolean(), nullable=False, server_default=sa.sql.expression.true()),
51
+ sa.Column('allow_password_auth', sa.Boolean(), nullable=False, server_default=sa.sql.expression.true()),
52
+ sa.Column('created_at', sa.DateTime(), nullable=True),
53
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
54
+ )
55
+ op.create_index('ix_tenants_slug', 'tenants', ['slug'], unique=False)
56
+ op.create_index('ix_tenants_active', 'tenants', ['active'], unique=False)
57
+ else:
58
+ existing_indexes = {idx['name'] for idx in inspector.get_indexes('tenants')}
59
+ if 'ix_tenants_slug' not in existing_indexes:
60
+ op.create_index('ix_tenants_slug', 'tenants', ['slug'], unique=False)
61
+ if 'ix_tenants_active' not in existing_indexes:
62
+ op.create_index('ix_tenants_active', 'tenants', ['active'], unique=False)
63
+
64
+
65
+ def ensure_saml_configurations(bind):
66
+ inspector = inspect(bind)
67
+ tables = inspector.get_table_names()
68
+ if 'saml_configurations' not in tables:
69
+ op.create_table(
70
+ 'saml_configurations',
71
+ sa.Column('id', sa.Integer(), primary_key=True),
72
+ sa.Column('tenant_id', sa.Integer(), sa.ForeignKey('tenants.id', name='fk_saml_configurations_tenant_id'), nullable=False),
73
+ sa.Column('name', sa.String(length=128), nullable=False),
74
+ sa.Column('slug', sa.String(length=64), nullable=False),
75
+ sa.Column('enabled', sa.Boolean(), nullable=False, server_default=sa.sql.expression.true()),
76
+ sa.Column('metadata_xml', sa.Text(), nullable=False),
77
+ sa.Column('idp_entity_id', sa.String(length=512), nullable=False),
78
+ sa.Column('idp_sso_url', sa.String(length=512), nullable=False),
79
+ sa.Column('idp_slo_url', sa.String(length=512), nullable=True),
80
+ sa.Column('idp_certificate', sa.Text(), nullable=False),
81
+ sa.Column('sp_entity_id', sa.String(length=512), nullable=True),
82
+ sa.Column('acs_url', sa.String(length=512), nullable=True),
83
+ sa.Column('slo_url', sa.String(length=512), nullable=True),
84
+ sa.Column('attribute_mapping', sa.JSON(), nullable=True),
85
+ sa.Column('auto_provision_users', sa.Boolean(), nullable=False, server_default=sa.sql.expression.true()),
86
+ sa.Column('allow_idp_initiated', sa.Boolean(), nullable=False, server_default=sa.sql.expression.true()),
87
+ sa.Column('created_at', sa.DateTime(), nullable=True),
88
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
89
+ sa.Column('created_by', sa.Integer(), sa.ForeignKey('users.id', name='fk_saml_configurations_created_by'), nullable=True),
90
+ sa.UniqueConstraint('tenant_id', 'slug', name='uq_saml_tenant_slug'),
91
+ )
92
+ op.create_index('ix_saml_configurations_tenant_id', 'saml_configurations', ['tenant_id'], unique=False)
93
+ op.create_index('ix_saml_configurations_enabled', 'saml_configurations', ['enabled'], unique=False)
94
+ else:
95
+ existing_indexes = {idx['name'] for idx in inspector.get_indexes('saml_configurations')}
96
+ if 'ix_saml_configurations_tenant_id' not in existing_indexes:
97
+ op.create_index('ix_saml_configurations_tenant_id', 'saml_configurations', ['tenant_id'], unique=False)
98
+ if 'ix_saml_configurations_enabled' not in existing_indexes:
99
+ op.create_index('ix_saml_configurations_enabled', 'saml_configurations', ['enabled'], unique=False)
100
+
101
+ existing_uniques = {uc['name'] for uc in inspector.get_unique_constraints('saml_configurations')}
102
+ if 'uq_saml_tenant_slug' not in existing_uniques:
103
+ op.create_unique_constraint('uq_saml_tenant_slug', 'saml_configurations', ['tenant_id', 'slug'])
104
+
105
+
106
+ def ensure_external_identities(bind):
107
+ inspector = inspect(bind)
108
+ tables = inspector.get_table_names()
109
+ if 'external_identities' not in tables:
110
+ op.create_table(
111
+ 'external_identities',
112
+ sa.Column('id', sa.Integer(), primary_key=True),
113
+ sa.Column('provider', sa.String(length=64), nullable=False),
114
+ sa.Column('provider_id', sa.Integer(), nullable=True),
115
+ sa.Column('external_id', sa.String(length=255), nullable=False),
116
+ sa.Column('tenant_id', sa.Integer(), sa.ForeignKey('tenants.id', name='fk_external_identities_tenant_id'), nullable=True),
117
+ sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id', name='fk_external_identities_user_id'), nullable=False),
118
+ sa.Column('attributes', sa.JSON(), nullable=True),
119
+ sa.Column('last_login_at', sa.DateTime(), nullable=True),
120
+ sa.Column('created_at', sa.DateTime(), nullable=True),
121
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
122
+ sa.UniqueConstraint('provider', 'provider_id', 'external_id', name='uq_external_identity'),
123
+ )
124
+ op.create_index('ix_external_identities_user_id', 'external_identities', ['user_id'], unique=False)
125
+ op.create_index('ix_external_identities_tenant_id', 'external_identities', ['tenant_id'], unique=False)
126
+ else:
127
+ existing_indexes = {idx['name'] for idx in inspector.get_indexes('external_identities')}
128
+ if 'ix_external_identities_user_id' not in existing_indexes:
129
+ op.create_index('ix_external_identities_user_id', 'external_identities', ['user_id'], unique=False)
130
+ if 'ix_external_identities_tenant_id' not in existing_indexes:
131
+ op.create_index('ix_external_identities_tenant_id', 'external_identities', ['tenant_id'], unique=False)
132
+
133
+ existing_uniques = {uc['name'] for uc in inspector.get_unique_constraints('external_identities')}
134
+ if 'uq_external_identity' not in existing_uniques:
135
+ op.create_unique_constraint('uq_external_identity', 'external_identities', ['provider', 'provider_id', 'external_id'])
136
+
137
+
138
+ def ensure_users_tenant_relationship(bind):
139
+ inspector = inspect(bind)
140
+ columns = {col['name'] for col in inspector.get_columns('users')}
141
+ existing_indexes = {idx['name'] for idx in inspector.get_indexes('users')}
142
+ existing_fks = inspector.get_foreign_keys('users')
143
+
144
+ fk_exists = any(
145
+ fk.get('name') == 'fk_users_tenant_id_tenants' or
146
+ (fk.get('referred_table') == 'tenants' and fk.get('constrained_columns') == ['tenant_id'])
147
+ for fk in existing_fks
148
+ )
149
+ needs_column = 'tenant_id' not in columns
150
+ needs_fk = not fk_exists
151
+
152
+ if needs_column or needs_fk:
153
+ with op.batch_alter_table('users', schema=None) as batch_op:
154
+ if needs_column:
155
+ batch_op.add_column(sa.Column('tenant_id', sa.Integer(), nullable=True))
156
+ if needs_fk:
157
+ batch_op.create_foreign_key(
158
+ 'fk_users_tenant_id_tenants',
159
+ 'tenants',
160
+ ['tenant_id'],
161
+ ['id']
162
+ )
163
+
164
+ if 'ix_users_tenant_id' not in existing_indexes:
165
+ op.create_index('ix_users_tenant_id', 'users', ['tenant_id'], unique=False)