Skip to content
Draft
4 changes: 2 additions & 2 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ APP_USER_PASSWORD=specify_user
# https://discourse.specifysoftware.org/t/allow-support-login-documentation/2838
ALLOW_SUPPORT_LOGIN=false
# The amount of time in seconds each token is valid for
SUPPORT_LOGIN_TTL = 180
SUPPORT_LOGIN_TTL=180

# Make sure to set the `SECRET_KEY` to a unique value
SECRET_KEY=change_this_to_some_unique_random_string
Expand Down Expand Up @@ -60,7 +60,7 @@ CELERY_RESULT_BACKEND=redis://redis/1
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = America/Chicago
TIME_ZONE=America/Chicago

# This variable controls the Specify 7 logging level. Possible values
# are:
Expand Down
56 changes: 33 additions & 23 deletions sp7_db_setup_check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,21 @@ fi
# Detect client host as seen by MariaDB
CLIENT_HOST="$(mariadb -N -B -h "$DB_HOST" -P "$DB_PORT" \
-u "$MASTER_USER_NAME" --password="$MASTER_USER_PASSWORD" \
-sse "SELECT SUBSTRING_INDEX(USER(),'@',-1);")" || CLIENT_HOST=""
-sse "SELECT SUBSTRING_INDEX(CURRENT_USER(),'@',-1);")" || CLIENT_HOST=""
CLIENT_HOST="${CLIENT_HOST%% *}"

if [[ -n "$CLIENT_HOST" ]]; then
echo "Client host as seen by MariaDB: '$CLIENT_HOST'"
if [[ "$SAME_MASTER_AND_MIGRATOR" == true ]]; then
MIGRATOR_USER_HOST="${CLIENT_HOST:-}"
else
MIGRATOR_USER_HOST="${MIGRATOR_USER_HOST:-${CLIENT_HOST:-}}"
fi

if [[ "$SAME_MASTER_AND_APP" == true ]]; then
APP_USER_HOST="${CLIENT_HOST:-}"
elif [[ "$SAME_MIGRATOR_AND_APP" == true ]]; then
APP_USER_HOST="${MIGRATOR_USER_HOST:-}"
else
echo "Warning: Could not detect client host via USER(); will only create users for explicit *_HOST values."
APP_USER_HOST="${APP_USER_HOST:-${CLIENT_HOST:-}}"
fi

# Create database if it doesn't exist
Expand Down Expand Up @@ -203,13 +211,13 @@ if [[ "$SAME_MASTER_AND_MIGRATOR" == true ]]; then
echo "Relying on master privileges for runtime connections."
else
USER_EXISTS=$(mysql -h "$DB_HOST" -P "$DB_PORT" -u "$MASTER_USER_NAME" --password="$MASTER_USER_PASSWORD" -sse \
"SELECT COUNT(*) FROM mysql.user WHERE user = '$SQL_MIGRATOR_NAME' AND host = '$CLIENT_HOST';")
"SELECT COUNT(*) FROM mysql.user WHERE user = '$SQL_MIGRATOR_NAME' AND host = '$MIGRATOR_USER_HOST';")

if [[ "$USER_EXISTS" -eq 0 && "$MIGRATOR_NAME" != "root" ]]; then
echo "Creating migrator user '$MIGRATOR_NAME'..."
echo "Executing: mysql -h \"$DB_HOST\" -P \"$DB_PORT\" -u \"$MASTER_USER_NAME\" --password=\"<hidden>\" -e \"CREATE USER '${SQL_MIGRATOR_NAME}'@'${CLIENT_HOST}' IDENTIFIED BY '<hidden>';\""
echo "Executing: mysql -h \"$DB_HOST\" -P \"$DB_PORT\" -u \"$MASTER_USER_NAME\" --password=\"<hidden>\" -e \"CREATE USER '${SQL_MIGRATOR_NAME}'@'${MIGRATOR_USER_HOST}' IDENTIFIED BY '<hidden>';\""
if mysql -h "$DB_HOST" -P "$DB_PORT" -u "$MASTER_USER_NAME" --password="$MASTER_USER_PASSWORD" \
-e "CREATE USER '$SQL_MIGRATOR_NAME'@'$CLIENT_HOST' IDENTIFIED BY '$SQL_MIGRATOR_PASSWORD';"; then
-e "CREATE USER '$SQL_MIGRATOR_NAME'@'$MIGRATOR_USER_HOST' IDENTIFIED BY '$SQL_MIGRATOR_PASSWORD';"; then
NEW_MIGRATOR_USER_CREATED=1
else
echo "Error: Failed to create user."
Expand All @@ -225,20 +233,20 @@ else
echo "Migrator user already exists. Refreshing privileges on '${DB_NAME}'..."
fi

echo "Executing: mysql -h \"$DB_HOST\" -P \"$DB_PORT\" -u \"$MASTER_USER_NAME\" --password=\"<hidden>\" -e \"GRANT ALL PRIVILEGES ON \`${SQL_DB_IDENTIFIER}\`.* TO '${SQL_MIGRATOR_NAME}'@'${CLIENT_HOST}'; FLUSH PRIVILEGES;\""
echo "Executing: mysql -h \"$DB_HOST\" -P \"$DB_PORT\" -u \"$MASTER_USER_NAME\" --password=\"<hidden>\" -e \"GRANT ALL PRIVILEGES ON \`${SQL_DB_IDENTIFIER}\`.* TO '${SQL_MIGRATOR_NAME}'@'${MIGRATOR_USER_HOST}'; FLUSH PRIVILEGES;\""

if ! mysql -h "$DB_HOST" -P "$DB_PORT" -u "$MASTER_USER_NAME" --password="$MASTER_USER_PASSWORD" \
-e "GRANT ALL PRIVILEGES ON \`${SQL_DB_IDENTIFIER}\`.* TO '${SQL_MIGRATOR_NAME}'@'${CLIENT_HOST}'; FLUSH PRIVILEGES;"; then
-e "GRANT ALL PRIVILEGES ON \`${SQL_DB_IDENTIFIER}\`.* TO '${SQL_MIGRATOR_NAME}'@'${MIGRATOR_USER_HOST}'; FLUSH PRIVILEGES;"; then
echo "Error: Failed to grant privileges to migrator user."
exit 1
fi

GRANTS_OUTPUT="$(mysql -N -B --raw -h "$DB_HOST" -P "$DB_PORT" \
-u "$MASTER_USER_NAME" --password="$MASTER_USER_PASSWORD" \
-e "SHOW GRANTS FOR '${SQL_MIGRATOR_NAME}'@'${CLIENT_HOST}';" 2>/dev/null || true)"
-e "SHOW GRANTS FOR '${SQL_MIGRATOR_NAME}'@'${MIGRATOR_USER_HOST}';" 2>/dev/null || true)"

if [[ -z "$GRANTS_OUTPUT" ]]; then
echo "Error: Could not retrieve grants for '${SQL_MIGRATOR_NAME}'@'${CLIENT_HOST}'."
echo "Error: Could not retrieve grants for '${SQL_MIGRATOR_NAME}'@'${MIGRATOR_USER_HOST}'."
exit 1
fi

Expand All @@ -253,28 +261,30 @@ for g in "${MIGRATOR_GRANTS_LINES[@]}"; do
done

if [[ "$migrator_has_required_permissions" == true ]]; then
echo "Verified: '${SQL_MIGRATOR_NAME}'@'${CLIENT_HOST}' has migration privileges on '${DB_NAME}'."
echo "Verified: '${SQL_MIGRATOR_NAME}'@'${MIGRATOR_USER_HOST}' has migration privileges on '${DB_NAME}'."
else
echo "Error: '${SQL_MIGRATOR_NAME}'@'${CLIENT_HOST}' lacks migration privileges on '${DB_NAME}'."
echo "Error: '${SQL_MIGRATOR_NAME}'@'${MIGRATOR_USER_HOST}' lacks migration privileges on '${DB_NAME}'."
echo "Required for migrations (any one GRANT must include all of): ${MIGRATION_REQUIRED_PRIVS[*]}"
echo "Grants found:"
echo "$GRANTS_OUTPUT"
exit 1
fi
fi

# BUG: this should probably be skipped if the app user is the same as the
# master or migrator
# Create app user if it doesn't exist
USER_EXISTS=$(mysql -h "$DB_HOST" -P "$DB_PORT" -u "$MASTER_USER_NAME" --password="$MASTER_USER_PASSWORD" -sse \
"SELECT COUNT(*) FROM mysql.user WHERE user = '$SQL_APP_USER_NAME' AND host = '$CLIENT_HOST';")
"SELECT COUNT(*) FROM mysql.user WHERE user = '$SQL_APP_USER_NAME' AND host = '$APP_USER_HOST';")

if [[ "$USER_EXISTS" -eq 0 && "$APP_USER_NAME" != "root" ]]; then
echo "Creating app user '$SQL_APP_USER_NAME'..."
echo "Executing: mysql -h \"$DB_HOST\" -P \"$DB_PORT\" -u \"$MASTER_USER_NAME\" --password=\"<hidden>\" -e \"CREATE USER '${SQL_APP_USER_NAME}'@'${CLIENT_HOST}' IDENTIFIED BY '<hidden>';\""
echo "Executing: mysql -h \"$DB_HOST\" -P \"$DB_PORT\" -u \"$MASTER_USER_NAME\" --password=\"<hidden>\" -e \"CREATE USER '${SQL_APP_USER_NAME}'@'${APP_USER_HOST}' IDENTIFIED BY '<hidden>';\""
if mysql -h "$DB_HOST" -P "$DB_PORT" -u "$MASTER_USER_NAME" --password="$MASTER_USER_PASSWORD" \
-e "CREATE USER '$SQL_APP_USER_NAME'@'$CLIENT_HOST' IDENTIFIED BY '$SQL_APP_USER_PASSWORD';"; then
-e "CREATE USER '$SQL_APP_USER_NAME'@'$APP_USER_HOST' IDENTIFIED BY '$SQL_APP_USER_PASSWORD';"; then
NEW_APP_USER_CREATED=1
else
echo "Error: Failed to create app user '${APP_USER_NAME}'@'${CLIENT_HOST}'."
echo "Error: Failed to create app user '${APP_USER_NAME}'@'${APP_USER_HOST}'."
echo "Falling back to migrator credentials for app user."
APP_USER_NAME="$MIGRATOR_NAME"
APP_USER_PASSWORD="$MIGRATOR_PASSWORD"
Expand Down Expand Up @@ -304,17 +314,17 @@ else
echo "App user already exists. Refreshing privileges on '${DB_NAME}'..."
fi

echo "Executing: mysql -h \"$DB_HOST\" -P \"$DB_PORT\" -u \"$MASTER_USER_NAME\" --password=\"<hidden>\" -e \"GRANT SELECT, INSERT, UPDATE, DELETE, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE ON \`${SQL_DB_IDENTIFIER}\`.* TO ${SQL_APP_USER_NAME}@'${CLIENT_HOST}'; FLUSH PRIVILEGES;\""
if ! mysql -h "$DB_HOST" -P "$DB_PORT" -u "$MASTER_USER_NAME" --password="$MASTER_USER_PASSWORD" -e "GRANT SELECT, INSERT, UPDATE, DELETE, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE ON \`${SQL_DB_IDENTIFIER}\`.* TO '${SQL_APP_USER_NAME}'@'${CLIENT_HOST}'; FLUSH PRIVILEGES;"; then
echo "Executing: mysql -h \"$DB_HOST\" -P \"$DB_PORT\" -u \"$MASTER_USER_NAME\" --password=\"<hidden>\" -e \"GRANT SELECT, INSERT, UPDATE, DELETE, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE ON \`${SQL_DB_IDENTIFIER}\`.* TO ${SQL_APP_USER_NAME}@'${APP_USER_HOST}'; FLUSH PRIVILEGES;\""
if ! mysql -h "$DB_HOST" -P "$DB_PORT" -u "$MASTER_USER_NAME" --password="$MASTER_USER_PASSWORD" -e "GRANT SELECT, INSERT, UPDATE, DELETE, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE ON \`${SQL_DB_IDENTIFIER}\`.* TO '${SQL_APP_USER_NAME}'@'${APP_USER_HOST}'; FLUSH PRIVILEGES;"; then
echo "Error: Failed to grant privileges to app user."
exit 1
fi

APP_GRANTS_RAW="$(mysql -N -B -h "$DB_HOST" -P "$DB_PORT" -u "$MASTER_USER_NAME" --password="$MASTER_USER_PASSWORD" \
-e "SHOW GRANTS FOR '${SQL_APP_USER_NAME}'@'${CLIENT_HOST}';" 2>/dev/null || true)"
-e "SHOW GRANTS FOR '${SQL_APP_USER_NAME}'@'${APP_USER_HOST}';" 2>/dev/null || true)"

if [[ -z "$APP_GRANTS_RAW" ]]; then
echo "Error: Could not retrieve grants for '${SQL_APP_USER_NAME}'@'${CLIENT_HOST}'."
echo "Error: Could not retrieve grants for '${SQL_APP_USER_NAME}'@'${APP_USER_HOST}'."
exit 1
fi

Expand All @@ -330,9 +340,9 @@ for g in "${APP_GRANTS_LINES[@]}"; do
done

if [[ "$app_has_required_permissions" == true ]]; then
echo "Verified: '${APP_USER_NAME}'@'${CLIENT_HOST}' has required privileges on '${DB_NAME}'."
echo "Verified: '${APP_USER_NAME}'@'${APP_USER_HOST}' has required privileges on '${DB_NAME}'."
else
echo "Error: '${APP_USER_NAME}'@'${CLIENT_HOST}' lacks required privileges on '${DB_NAME}'."
echo "Error: '${APP_USER_NAME}'@'${APP_USER_HOST}' lacks required privileges on '${DB_NAME}'."
echo "Required (any one GRANT must include all of): ${APP_REQUIRED_PRIVS[*]}"
echo "Grants found:"
echo "$APP_GRANTS_RAW"
Expand Down
1 change: 1 addition & 0 deletions specifyweb/backend/delete_blockers/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
from . import views
urlpatterns = [
re_path(r'^delete_blockers/(?P<model>\w+)/(?P<id>\d+)/$', views.delete_blockers),
re_path(r'^old_delete_blockers/(?P<model>\w+)/(?P<id>\d+)/$', views.old_delete_blockers),
]

66 changes: 64 additions & 2 deletions specifyweb/backend/delete_blockers/views.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
from collections import defaultdict

from django import http
from django.db import router, transaction
from django.db.models.deletion import Collector
from django.db.models.deletion import Collector, CASCADE, PROTECT
from django.db.models import ForeignKey
from django.views.decorators.http import require_POST

from specifyweb.middleware.general import require_http_methods
from specifyweb.specify.api.crud import (
get_discipline_delete_guard_blockers,
get_object_or_404,
get_model,
prepare_discipline_for_delete,
)
from specifyweb.specify.models import protect_with_blockers
from specifyweb.specify.api.serializers import toJson
from specifyweb.specify.views import login_maybe_required

@login_maybe_required
@require_http_methods(['GET', 'HEAD'])
def delete_blockers(request, model, id):
def old_delete_blockers(request, model, id):
"""Returns a JSON list of fields on <model> that point to related
resources which prevent the resource <id> of that model from being
deleted.
"""
# limit = request.GET["limit"]
# depth_limit = request.GET["depthLimit"]

obj = get_object_or_404(model, id=int(id))
using = router.db_for_write(obj.__class__, instance=obj)

Expand All @@ -38,6 +46,60 @@ def delete_blockers(request, model, id):

return http.HttpResponse(toJson(result), content_type='application/json')

@login_maybe_required
@require_http_methods(['GET'])
def delete_blockers(request, model, id):
limit = int(request.GET["limit"]) if "limit" in request.GET else 20
offset = int(request.GET["offset"]) if "offset" in request.GET else 0
obj = get_object_or_404(model, id=int(id))
immediate, deferred = fetch_immediate_blockers(obj, limit=limit, offset=offset)
result = {
"results": immediate,
"next": deferred
}
return http.HttpResponse(toJson(result), content_type='application/json')

def fetch_immediate_blockers(obj, limit=20, offset=0):
all_fields = obj._meta.get_fields(include_hidden=True)
all_relationships = filter(
# Check whether there are any concrete fields that SHOULD be included
# here, like some ToOne fields that acts as blockers
lambda field: field.is_relation and not field.concrete,
all_fields
)
results = []
next = []
for relationship in all_relationships:
related_ids = _prepare_blockers(obj, relationship, limit=limit, offset=offset)
if len(related_ids) == 0:
continue
complete = limit == 0 or len(related_ids) < limit
payload = {
"table": relationship.related_model._meta.model_name,
"field": relationship.field.name,
"ids": list(related_ids),
"offset": offset,
"limit": limit,
"complete": complete
}
if relationship.on_delete is protect_with_blockers or relationship.on_delete is PROTECT:
results.append(payload)
elif relationship.on_delete is CASCADE:
next.append(payload)
return results, next


def _prepare_blockers(obj, relationship, limit=20, offset=0):
query_set = (
relationship.related_model.objects
.filter(
**{relationship.field.name: obj.pk}
).order_by("pk")
.values_list("pk", flat=True))
if limit != 0:
query_set = query_set[offset: offset + limit]
return query_set

def _collect_delete_blockers(obj, using) -> list[dict]:
collector = Collector(using=using)
collector.delete_blockers = []
Expand Down
Loading
Loading