Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c28ccf1
Fix: Typo in comment
CarolineDenis Jun 1, 2026
1213dc2
Fix: Improve DB setup script
CarolineDenis Jun 1, 2026
7a19f7f
Fix: Validate master credentials explicitly in the required-env checkt
CarolineDenis Jun 1, 2026
efd66ca
Fix: Require migration-capable privileges
CarolineDenis Jun 1, 2026
47cc4c8
Fix: Validate DJANGO_DB_ALIAS before indexing DATABASES
CarolineDenis Jun 1, 2026
f78c9e1
Fix: Read the DB role credentials from the environment
CarolineDenis Jun 1, 2026
7d9e8c9
Fix: Trigger fallback mechanism
CarolineDenis Jun 1, 2026
926fd43
Fix: Fail entrypoint fast when ./sp7_db_setup_check.sh fails
CarolineDenis Jun 2, 2026
a9fe052
Fix: Use override for startup migration flow
CarolineDenis Jun 2, 2026
1e5737c
Fix: Fix set -u
CarolineDenis Jun 2, 2026
841bf29
Fix: Remove schema-changing privileges
CarolineDenis Jun 2, 2026
4ad9bec
Fix: Check for sp6 tables only when it's not a new db
CarolineDenis Jun 2, 2026
e9f3f40
Fix: Remove useless try/except
CarolineDenis Jun 2, 2026
999cf16
Fix: Closed Fi before the grant block starts
CarolineDenis Jun 3, 2026
eb7c2e4
Fix: Fix log message
CarolineDenis Jun 3, 2026
08ee353
Fix: Materialize scopes before computing final_discipline
CarolineDenis Jun 3, 2026
0586860
Fix: Fix import
CarolineDenis Jun 3, 2026
0361cc3
fix: add helper fct
CarolineDenis Jun 3, 2026
7277f66
fix: Fix syntax in setup_check
CarolineDenis Jun 3, 2026
da2532b
fix: Use SQL_MIGRATOR_*
CarolineDenis Jun 3, 2026
b46fe09
fix: Make SQL escaping
CarolineDenis Jun 3, 2026
0b2e4b8
fix: Use get_or_create() for idempotency to match production behavior
CarolineDenis Jun 3, 2026
fa20860
fix: do not fail on missing variable for test panel
CarolineDenis Jun 5, 2026
756c6d3
fix: Fix wrong user name
CarolineDenis Jun 5, 2026
d9c0ae7
fix: Fix wrong user name
CarolineDenis Jun 5, 2026
aaf0498
fix: Improve error logs
CarolineDenis Jun 5, 2026
8c2ef7d
fix: Fix host variable initialization order that prevents environment…
CarolineDenis Jun 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pip_requirements:
$(PIP) install --upgrade -r requirements.txt

django_migrations:
$(PYTHON) manage.py base_specify_migration --database=migrations
$(PYTHON) manage.py base_specify_migration --use-override --database=migrations
$(PYTHON) manage.py migrate --database=migrations

specifyweb/settings/build_version.py: .FORCE
Expand Down
6 changes: 2 additions & 4 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ if [ "$1" = 've/bin/gunicorn' ] || [ "$1" = 've/bin/python' ]; then
rsync -a --delete specifyweb/frontend/static/ /volumes/static-files/frontend-static
cd /opt/specify7
echo "Applying Django migrations."
set +e
./sp7_db_setup_check.sh # Setup db users and run mirgations
./sp7_db_setup_check.sh # Setup db users and run migrations
# ve/bin/python manage.py base_specify_migration
# ve/bin/python manage.py migrate
ve/bin/python manage.py run_key_migration_functions # Uncomment if you want the key migration functions to run on startup.
set -e
# ve/bin/python manage.py run_key_migration_functions # Uncomment if you want the key migration functions to run on startup.
fi
exec "$@"
459 changes: 195 additions & 264 deletions sp7_db_setup_check.sh

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions specifyweb/backend/businessrules/uniqueness_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,15 +395,15 @@ def create_uniqueness_rule(model_name: str, discipline, is_database_constraint:
UniquenessRuleField = registry.get_model(
'businessrules', 'UniquenessRuleField') if registry else models.UniquenessRuleField

fields = list(fields)
scopes = list(scopes)

final_discipline = None if rule_is_global(scopes) else discipline

candidate_rules = UniquenessRule.objects.filter(modelName=model_name,
isDatabaseConstraint=is_database_constraint,
discipline=final_discipline)

fields = list(fields)
scopes = list(scopes)

for rule in candidate_rules:
# If the rule already exists, skip creating the rule
if _rule_fields_match(
Expand Down
65 changes: 34 additions & 31 deletions specifyweb/backend/permissions/initialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,26 @@ def is_sp6_user_permissions_migrated(user, apps=apps) -> bool:
return UserRole.objects.filter(specifyuser=user).exists() or \
UserPolicy.objects.filter(specifyuser=user).exists()

def initialize(
wipe: bool = False,
apps=apps,
*,
migrate_sp6_users: bool = True,
) -> None:
def has_sp6_permissions_tables() -> bool:
with connection.cursor() as cursor:
cursor.execute("""
SELECT COUNT(*)
FROM information_schema.tables
WHERE table_name IN ('specifyuser_spprincipal', 'spuserrole')
AND table_schema = DATABASE();
""")
return cursor.fetchone()[0] == 2

def initialize(wipe: bool=False, apps=apps) -> None:
with transaction.atomic():
if wipe:
wipe_permissions(apps)
create_admins(apps)
create_roles(apps)
if migrate_sp6_users:
if 'test' in ''.join(sys.argv):
assign_users_to_roles_during_testing(apps)
else:
assign_users_to_roles(apps)
if 'test' in ''.join(sys.argv):
assign_users_to_roles_during_testing(apps)
else:
assign_users_to_roles(apps)

def create_admins(apps=apps) -> None:
UserPolicy = apps.get_model('permissions', 'UserPolicy')
Expand Down Expand Up @@ -91,15 +95,10 @@ def assign_users_to_roles(apps=apps) -> None:
}

results = []
if not has_sp6_permissions_tables():
return # Newly created sp7 databases don't have these sp6 specific tables.

with connection.cursor() as cursor:
cursor.execute("""
SELECT COUNT(*)
FROM information_schema.tables
WHERE table_name IN ('specifyuser_spprincipal', 'spuserrole')
AND table_schema = DATABASE();
""")
if cursor.fetchone()[0] < 2:
return # Newly created sp7 databases don't have these sp6 specific tables.
cursor.execute("""
SELECT
u.SpecifyUserID as user_id,
Expand Down Expand Up @@ -165,7 +164,6 @@ def assign_users_to_roles_during_testing(apps=apps) -> None:
Specifyuser = apps.get_model('specify', 'Specifyuser')
Agent = apps.get_model('specify', 'Agent')

cursor = connection.cursor()
for user in Specifyuser.objects.all():
for collection in Collection.objects.all():
if user.usertype == 'Manager':
Expand All @@ -175,16 +173,21 @@ def assign_users_to_roles_during_testing(apps=apps) -> None:
if user.usertype in ('LimitedAccess', 'Guest'):
user.roles.create(role=Role.objects.get(collection=collection, name="Read Only - Legacy"))

for colid, _ in users_collections_for_sp6(cursor, user.id):
# Does the user has an agent for the collection?
if Agent.objects.filter(specifyuser=user, division__disciplines__collections__id=colid).exists():
# Give them access to the collection.
UserPolicy.objects.create(
collection_id=colid,
specifyuser_id=user.id,
resource=CollectionAccessPT.access.resource(),
action=CollectionAccessPT.access.action(),
)
if not has_sp6_permissions_tables():
return

with connection.cursor() as cursor:
for user in Specifyuser.objects.all():
for colid, _ in users_collections_for_sp6(cursor, user.id):
# Does the user has an agent for the collection?
if Agent.objects.filter(specifyuser=user, division__disciplines__collections__id=colid).exists():
# Give them access to the collection.
UserPolicy.objects.get_or_create(
collection_id=colid,
specifyuser_id=user.id,
resource=CollectionAccessPT.access.resource(),
action=CollectionAccessPT.access.action(),
)

def create_roles(apps = apps) -> None:
LibraryRole = apps.get_model('permissions', 'LibraryRole')
Expand Down Expand Up @@ -548,4 +551,4 @@ def create_roles(apps = apps) -> None:
)
if is_new:
for lp in collection_admin.policies.all():
ca.policies.get_or_create(resource=lp.resource, action=lp.action)
ca.policies.get_or_create(resource=lp.resource, action=lp.action)
6 changes: 6 additions & 0 deletions specifyweb/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@
}

DB_ALIAS = os.getenv("DJANGO_DB_ALIAS", "default") # Might want to set to "app" in the future
if DB_ALIAS not in DATABASES:
valid_aliases = ", ".join(sorted(DATABASES))
raise ValueError(
f"Invalid DJANGO_DB_ALIAS '{DB_ALIAS}'. "
f"Expected one of: {valid_aliases}."
)
if DB_ALIAS != "default":
from copy import deepcopy
DATABASES['default'] = deepcopy(DATABASES[DB_ALIAS])
Expand Down
24 changes: 9 additions & 15 deletions specifyweb/settings/specify_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,15 @@
# https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-OPTIONS
DATABASE_OPTIONS = {}

# The master user login. This is the MySQL user used to connect to the
# database. This can be the same as the Specify 6 master user.
MASTER_NAME = 'MasterUser'
MASTER_PASSWORD = 'MasterPassword'
MIGRATOR_NAME = 'MasterUser'
MIGRATOR_PASSWORD = 'MasterPassword'
APP_USER_NAME = 'MasterUser'
APP_USER_PASSWORD = 'MasterPassword'

# MASTER_NAME = os.environ.get('MASTER_NAME', 'root')
# MASTER_PASSWORD = os.environ.get('MASTER_NAME', 'password')
# MIGRATOR_NAME = os.environ.get('MIGRATOR_NAME', MASTER_NAME)
# MIGRATOR_PASSWORD = os.environ.get('MIGRATOR_PASSWORD', MASTER_PASSWORD)
# APP_USER_NAME = os.environ.get('APP_USER_NAME', MIGRATOR_NAME)
# APP_USER_PASSWORD = os.environ.get('APP_USER_PASSWORD', MIGRATOR_PASSWORD)
# The master, migrator, and application user logins. The migrator and app
# credentials fall back through the more privileged roles for compatibility.
ROOT_PASSWORD = os.getenv('MYSQL_ROOT_PASSWORD', 'password')
MASTER_NAME = os.getenv('MASTER_NAME', 'root')
MASTER_PASSWORD = os.getenv('MASTER_PASSWORD', ROOT_PASSWORD)
MIGRATOR_NAME = os.getenv('MIGRATOR_NAME', MASTER_NAME)
MIGRATOR_PASSWORD = os.getenv('MIGRATOR_PASSWORD', MASTER_PASSWORD)
APP_USER_NAME = os.getenv('APP_USER_NAME', MIGRATOR_NAME)
APP_USER_PASSWORD = os.getenv('APP_USER_PASSWORD', MIGRATOR_PASSWORD)

# The Specify web attachment server URL.
WEB_ATTACHMENT_URL = None
Expand Down
22 changes: 7 additions & 15 deletions specifyweb/specify/management/commands/base_specify_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,20 @@ def add_arguments(self, parser):
def handle(self, *args, **options):
use_override = bool(options.get('use_override', False))
alias = options["database"]
conn = connections[alias]
logger.info(f"Running base_specify_migration using database alias '{alias}'")

# Validate the alias exists and is usable; fallback to 'master' if not
original_alias = alias
try:
transaction.atomic(using=alias)
except:
connections[alias].ensure_connection()
except Exception as e:
alias = 'master'
logger.warning(f"Database alias '{original_alias}' unavailable ({e}), falling back to 'master'")

conn = connections[alias]
with transaction.atomic(using=alias):
with conn.cursor() as cursor:
# Check django table
try:
cursor.execute("""
SELECT 1
FROM django_migrations
LIMIT 1;
""")
exists = True
except:
exists = False

if not exists:
if "django_migrations" not in conn.introspection.table_names(cursor):
# Check if the django_migrations table exists and create it if it doesn't
cursor.execute("""
CREATE TABLE IF NOT EXISTS `django_migrations` (
Expand Down
Loading