auth 3.4.0__tar.gz → 3.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {auth-3.4.0 → auth-3.5.0}/PKG-INFO +1 -1
- {auth-3.4.0 → auth-3.5.0}/auth/__init__.py +3 -0
- {auth-3.4.0 → auth-3.5.0}/auth/database.py +2 -0
- auth-3.5.0/auth/schema_check.py +61 -0
- {auth-3.4.0 → auth-3.5.0}/auth.egg-info/PKG-INFO +1 -1
- {auth-3.4.0 → auth-3.5.0}/auth.egg-info/SOURCES.txt +2 -0
- {auth-3.4.0 → auth-3.5.0}/pyproject.toml +1 -1
- auth-3.5.0/tests/test_schema_check.py +134 -0
- {auth-3.4.0 → auth-3.5.0}/LICENSE +0 -0
- {auth-3.4.0 → auth-3.5.0}/README.md +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/api_keys.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/audit.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/circuit_breaker.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/client/__init__.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/client/_transport.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/client/base.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/client/keys.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/client/rbac.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/client/workflow.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/cmd/__init__.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/cmd/server.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/config.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/core/REST/__init__.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/core/REST/client.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/core/__init__.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/core/models/__init__.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/decorators.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/docs_page.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/encryption.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/keycheck.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/logging_config.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/main.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/models/sql.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/response_format.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/rls.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/routes/__init__.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/routes/_common.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/routes/apikeys.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/routes/membership.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/routes/permissions.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/routes/public.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/routes/roles.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/routes/tenant.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/routes/workflow.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/sanitizer.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/schema_reconcile.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/server.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/services/base.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/services/keys.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/services/queries.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/services/rbac.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/services/recovery.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/services/rotation.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/services/service.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/services/tenant.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/static_docs/__init__.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/static_docs/api_reference.md +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/static_docs/claude_guide.md +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/static_docs/landing.md +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/static_docs/page.html +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/validation.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth/workflow_checker.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth.egg-info/dependency_links.txt +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth.egg-info/entry_points.txt +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth.egg-info/requires.txt +0 -0
- {auth-3.4.0 → auth-3.5.0}/auth.egg-info/top_level.txt +0 -0
- {auth-3.4.0 → auth-3.5.0}/setup.cfg +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_api_keys.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_api_keys_encryption.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_audit_integrity.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_client.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_client_exceptions_do_not_render_payload.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_client_rest.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_cmd_server.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_config.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_correctness_hardening.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_database_pool_settings.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_database_sslmode.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_degraded_response_is_a_contract.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_docs_page.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_encryption.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_encryption_integration.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_flask.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_gunicorn_config_invariants.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_key_rotation.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_log_redaction.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_no_bundled_libpq.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_phase_a_hardening.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_provision_script_no_dsn_leak.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_reencryption.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_routes_errors.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_self_service_audit.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_server.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_service_lifecycle.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_settings_do_not_echo_secrets.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_strict_default.py +0 -0
- {auth-3.4.0 → auth-3.5.0}/tests/test_strict_users.py +0 -0
|
@@ -10,6 +10,7 @@ from auth.client import ( # Import the new client
|
|
|
10
10
|
EnhancedAuthClient,
|
|
11
11
|
)
|
|
12
12
|
from auth.database import SessionLocal
|
|
13
|
+
from auth.schema_check import SchemaMismatchError, check_schema
|
|
13
14
|
from auth.services.service import AuthorizationService
|
|
14
15
|
|
|
15
16
|
try:
|
|
@@ -157,5 +158,7 @@ __all__ = [
|
|
|
157
158
|
"Client",
|
|
158
159
|
"EnhancedAuthClient",
|
|
159
160
|
"SessionLocal",
|
|
161
|
+
"SchemaMismatchError",
|
|
162
|
+
"check_schema",
|
|
160
163
|
"__version__",
|
|
161
164
|
]
|
|
@@ -15,6 +15,7 @@ from sqlalchemy.orm import Session, sessionmaker
|
|
|
15
15
|
from sqlalchemy.pool import Pool
|
|
16
16
|
|
|
17
17
|
from auth.config import DatabaseType, Settings, get_settings, warn_on_weak_secrets
|
|
18
|
+
from auth.schema_check import check_schema
|
|
18
19
|
from auth.schema_reconcile import (
|
|
19
20
|
GRANDFATHER_MARKER, # noqa: F401 - re-exported; tests and embedded callers import it from here
|
|
20
21
|
_apply_tenant_rls,
|
|
@@ -379,6 +380,7 @@ def create_tables(raise_on_error: bool = False):
|
|
|
379
380
|
_ensure_audit_partition_runway(engine)
|
|
380
381
|
_reconcile_text_columns(engine)
|
|
381
382
|
_grandfather_strict_users(engine)
|
|
383
|
+
check_schema(engine)
|
|
382
384
|
logger.info("Tables created successfully.")
|
|
383
385
|
except Exception:
|
|
384
386
|
logger.exception("create_tables failed")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import List, Optional, Sequence, Tuple, Union
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import Table, inspect
|
|
4
|
+
from sqlalchemy.engine import Connection, Engine
|
|
5
|
+
|
|
6
|
+
Bind = Union[Engine, Connection]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SchemaMismatchError(RuntimeError):
|
|
10
|
+
def __init__(
|
|
11
|
+
self, missing_tables: Sequence[str], missing_columns: Sequence[str]
|
|
12
|
+
) -> None:
|
|
13
|
+
self.missing_tables = list(missing_tables)
|
|
14
|
+
self.missing_columns = list(missing_columns)
|
|
15
|
+
parts = []
|
|
16
|
+
if self.missing_tables:
|
|
17
|
+
parts.append("missing tables: " + ", ".join(self.missing_tables))
|
|
18
|
+
if self.missing_columns:
|
|
19
|
+
parts.append("missing columns: " + ", ".join(self.missing_columns))
|
|
20
|
+
super().__init__(
|
|
21
|
+
"the live database is behind the installed auth models ("
|
|
22
|
+
+ "; ".join(parts)
|
|
23
|
+
+ "). The auth changelog lists the DDL each release needs."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _qualified(table: Table) -> str:
|
|
28
|
+
return f"{table.schema}.{table.name}" if table.schema else table.name
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def find_schema_drift(bind: Bind) -> Tuple[List[str], List[str]]:
|
|
32
|
+
import auth.audit # noqa: F401
|
|
33
|
+
from auth.models.sql import Base
|
|
34
|
+
|
|
35
|
+
inspector = inspect(bind)
|
|
36
|
+
missing_tables: List[str] = []
|
|
37
|
+
missing_columns: List[str] = []
|
|
38
|
+
for table in sorted(Base.metadata.tables.values(), key=_qualified):
|
|
39
|
+
if not inspector.has_table(table.name, schema=table.schema):
|
|
40
|
+
missing_tables.append(_qualified(table))
|
|
41
|
+
continue
|
|
42
|
+
live = {
|
|
43
|
+
column["name"]
|
|
44
|
+
for column in inspector.get_columns(table.name, schema=table.schema)
|
|
45
|
+
}
|
|
46
|
+
missing_columns.extend(
|
|
47
|
+
f"{_qualified(table)}.{column.name}"
|
|
48
|
+
for column in table.columns
|
|
49
|
+
if column.name not in live
|
|
50
|
+
)
|
|
51
|
+
return missing_tables, missing_columns
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def check_schema(bind: Optional[Bind] = None) -> None:
|
|
55
|
+
if bind is None:
|
|
56
|
+
from auth.database import engine
|
|
57
|
+
|
|
58
|
+
bind = engine
|
|
59
|
+
missing_tables, missing_columns = find_schema_drift(bind)
|
|
60
|
+
if missing_tables or missing_columns:
|
|
61
|
+
raise SchemaMismatchError(missing_tables, missing_columns)
|
|
@@ -16,6 +16,7 @@ auth/main.py
|
|
|
16
16
|
auth/response_format.py
|
|
17
17
|
auth/rls.py
|
|
18
18
|
auth/sanitizer.py
|
|
19
|
+
auth/schema_check.py
|
|
19
20
|
auth/schema_reconcile.py
|
|
20
21
|
auth/server.py
|
|
21
22
|
auth/validation.py
|
|
@@ -85,6 +86,7 @@ tests/test_phase_a_hardening.py
|
|
|
85
86
|
tests/test_provision_script_no_dsn_leak.py
|
|
86
87
|
tests/test_reencryption.py
|
|
87
88
|
tests/test_routes_errors.py
|
|
89
|
+
tests/test_schema_check.py
|
|
88
90
|
tests/test_self_service_audit.py
|
|
89
91
|
tests/test_server.py
|
|
90
92
|
tests/test_service_lifecycle.py
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from sqlalchemy import create_engine, event, text
|
|
5
|
+
|
|
6
|
+
import auth
|
|
7
|
+
import auth.audit # noqa: F401
|
|
8
|
+
import auth.database
|
|
9
|
+
from auth.models.sql import Base
|
|
10
|
+
from auth.schema_check import SchemaMismatchError, check_schema, find_schema_drift
|
|
11
|
+
|
|
12
|
+
V310_TENANT_SETTINGS = (
|
|
13
|
+
"CREATE TABLE auth_tenant_settings ("
|
|
14
|
+
"id INTEGER PRIMARY KEY, creator VARCHAR(64) NOT NULL, "
|
|
15
|
+
"strict_users BOOLEAN NOT NULL, date_created DATETIME, modified DATETIME)"
|
|
16
|
+
)
|
|
17
|
+
V340_ADDED = [
|
|
18
|
+
"auth_tenant_settings.rotate_key_hash",
|
|
19
|
+
"auth_tenant_settings.rotate_key_issued_at",
|
|
20
|
+
"auth_tenant_settings.canary",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
pytestmark = pytest.mark.skipif(
|
|
24
|
+
any(t.schema for t in Base.metadata.tables.values()),
|
|
25
|
+
reason="SQLite drift tests build unqualified tables",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def current_engine(tmp_path):
|
|
31
|
+
engine = create_engine(f"sqlite:///{tmp_path / 'current.sqlite3'}")
|
|
32
|
+
Base.metadata.create_all(engine)
|
|
33
|
+
yield engine
|
|
34
|
+
engine.dispose()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def v310_engine(current_engine):
|
|
39
|
+
with current_engine.begin() as conn:
|
|
40
|
+
conn.execute(text("DROP TABLE auth_tenant_settings"))
|
|
41
|
+
conn.execute(text(V310_TENANT_SETTINGS))
|
|
42
|
+
return current_engine
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _statements(engine):
|
|
46
|
+
seen = []
|
|
47
|
+
event.listen(
|
|
48
|
+
engine,
|
|
49
|
+
"before_cursor_execute",
|
|
50
|
+
lambda conn, cursor, statement, *a: seen.append(statement),
|
|
51
|
+
)
|
|
52
|
+
return seen
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_current_schema_passes(current_engine):
|
|
56
|
+
assert find_schema_drift(current_engine) == ([], [])
|
|
57
|
+
check_schema(current_engine)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_v310_tenant_settings_names_the_three_missing_columns(v310_engine):
|
|
61
|
+
assert find_schema_drift(v310_engine) == ([], V340_ADDED)
|
|
62
|
+
with pytest.raises(SchemaMismatchError) as caught:
|
|
63
|
+
check_schema(v310_engine)
|
|
64
|
+
assert caught.value.missing_columns == V340_ADDED
|
|
65
|
+
assert caught.value.missing_tables == []
|
|
66
|
+
for name in V340_ADDED:
|
|
67
|
+
assert name in str(caught.value)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_missing_table_is_named(current_engine):
|
|
71
|
+
with current_engine.begin() as conn:
|
|
72
|
+
conn.execute(text("DROP TABLE auth_tenant_settings"))
|
|
73
|
+
with pytest.raises(SchemaMismatchError) as caught:
|
|
74
|
+
check_schema(current_engine)
|
|
75
|
+
assert caught.value.missing_tables == ["auth_tenant_settings"]
|
|
76
|
+
assert caught.value.missing_columns == []
|
|
77
|
+
assert "missing tables: auth_tenant_settings" in str(caught.value)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_extra_live_columns_are_allowed(current_engine):
|
|
81
|
+
with current_engine.begin() as conn:
|
|
82
|
+
conn.execute(text("ALTER TABLE auth_tenant_settings ADD COLUMN creator_fp TEXT"))
|
|
83
|
+
check_schema(current_engine)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_check_issues_no_ddl_or_writes(v310_engine):
|
|
87
|
+
seen = _statements(v310_engine)
|
|
88
|
+
with v310_engine.begin() as conn:
|
|
89
|
+
conn.execute(text("ALTER TABLE auth_tenant_settings ADD COLUMN probe TEXT"))
|
|
90
|
+
assert any(s.startswith("ALTER TABLE") for s in seen)
|
|
91
|
+
seen.clear()
|
|
92
|
+
with pytest.raises(SchemaMismatchError):
|
|
93
|
+
check_schema(v310_engine)
|
|
94
|
+
assert seen
|
|
95
|
+
writes = ("CREATE", "ALTER", "DROP", "INSERT", "UPDATE", "DELETE")
|
|
96
|
+
assert [s for s in seen if s.lstrip().upper().startswith(writes)] == []
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_message_does_not_carry_the_database_location(v310_engine):
|
|
100
|
+
with pytest.raises(SchemaMismatchError) as caught:
|
|
101
|
+
check_schema(v310_engine)
|
|
102
|
+
assert str(v310_engine.url.database) not in str(caught.value)
|
|
103
|
+
assert "sqlite" not in str(caught.value)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_create_tables_raises_on_drift_when_asked(v310_engine, monkeypatch):
|
|
107
|
+
monkeypatch.setattr(auth.database, "engine", v310_engine)
|
|
108
|
+
with pytest.raises(SchemaMismatchError) as caught:
|
|
109
|
+
auth.database.create_tables(raise_on_error=True)
|
|
110
|
+
assert caught.value.missing_columns == V340_ADDED
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_create_tables_logs_drift_by_default(v310_engine, monkeypatch, caplog):
|
|
114
|
+
monkeypatch.setattr(auth.database, "engine", v310_engine)
|
|
115
|
+
with caplog.at_level(logging.ERROR, logger=auth.database.logger.name):
|
|
116
|
+
auth.database.create_tables()
|
|
117
|
+
logged = "\n".join(r.getMessage() + (r.exc_text or "") for r in caplog.records)
|
|
118
|
+
assert "create_tables failed" in logged
|
|
119
|
+
for name in V340_ADDED:
|
|
120
|
+
assert name in logged
|
|
121
|
+
assert "Tables created successfully." not in caplog.text
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_create_tables_on_current_schema_reports_success(current_engine, monkeypatch, caplog):
|
|
125
|
+
monkeypatch.setattr(auth.database, "engine", current_engine)
|
|
126
|
+
with caplog.at_level(logging.INFO, logger=auth.database.logger.name):
|
|
127
|
+
auth.database.create_tables(raise_on_error=True)
|
|
128
|
+
assert "Tables created successfully." in caplog.text
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_public_exports():
|
|
132
|
+
assert auth.check_schema is check_schema
|
|
133
|
+
assert auth.SchemaMismatchError is SchemaMismatchError
|
|
134
|
+
assert {"check_schema", "SchemaMismatchError"} <= set(auth.__all__)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|