Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Matomo scan user using infra_ops lambda #789

Merged
merged 7 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
6 changes: 5 additions & 1 deletion backend/env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ staging:
ELASTICSEARCH_ENDPOINT: ${ssm:/crossfeed/staging/ELASTICSEARCH_ENDPOINT}
REACT_APP_TERMS_VERSION: ${ssm:/crossfeed/staging/REACT_APP_TERMS_VERSION}
MATOMO_URL: http://matomo.cfs.lz.us-cert.gov
MATOMO_DB_HOST: ${ssm:/crossfeed/staging/MATOMO_DATABASE_HOST}
MATOMO_DB_PASSWORD: ${ssm:/crossfeed/staging/MATOMO_DATABASE_PASSWORD}
EXPORT_BUCKET_NAME: cisa-crossfeed-staging-exports
PE_API_URL: ${ssm:/crossfeed/staging/PE_API_URL}
REPORTS_BUCKET_NAME: cisa-crossfeed-staging-reports
Expand Down Expand Up @@ -243,7 +245,7 @@ prod:
LOGIN_GOV_JWT_KEY: ${ssm:/crossfeed/prod/LOGIN_GOV_JWT_KEY}
LOGIN_GOV_ISSUER: ${ssm:/crossfeed/prod/LOGIN_GOV_ISSUER}
DOMAIN: ${ssm:/crossfeed/prod/DOMAIN}
CERT_DOMAIN: staging.crossfeed.cyber.dhs.gov
CERT_DOMAIN: crossfeed.cyber.dhs.gov
FARGATE_SG_ID: ${ssm:/crossfeed/prod/WORKER_SG_ID}
FARGATE_SUBNET_ID: ${ssm:/crossfeed/prod/WORKER_SUBNET_ID}
FARGATE_MAX_CONCURRENCY: 300
Expand All @@ -264,6 +266,8 @@ prod:
ELASTICACHE_ENDPOINT: ${ssm:/crossfeed/prod/ELASTICACHE_ENDPOINT}
REACT_APP_TERMS_VERSION: ${ssm:/crossfeed/prod/REACT_APP_TERMS_VERSION}
MATOMO_URL: http://matomo.cfs.lz.us-cert.gov
MATOMO_DB_HOST: ${ssm:/crossfeed/prod/MATOMO_DATABASE_HOST}
MATOMO_DB_PASSWORD: ${ssm:/crossfeed/prod/MATOMO_DATABASE_PASSWORD}
EXPORT_BUCKET_NAME: cisa-crossfeed-prod-exports
PE_API_URL: ${ssm:/crossfeed/prod/PE_API_URL}
REPORTS_BUCKET_NAME: cisa-crossfeed-prod-reports
Expand Down
2 changes: 2 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ minio==7.2.12
multidict==6.1.0
mypy==1.13.0
mypy-extensions==1.0.0
netaddr==1.3.0
orjson==3.10.12
packaging==24.2
pika==1.3.2
Expand All @@ -58,6 +59,7 @@ pydantic_core==2.27.1
Pygments==2.18.0
PyJWT==2.10.1
pylint==3.3.2
PyMySQL==1.1.1
pytest==8.3.4
pytest-django==4.9.0
pytest-env==1.1.5
Expand Down
4 changes: 4 additions & 0 deletions backend/src/tasks/functions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ updateScanTaskStatus:
clusterArn:
- ${file(env.yml):${self:provider.stage}-ecs-cluster, ''}

infraOps:
timeout: 900
handler: src/xfd_django/xfd_api/tasks/infra_ops.handler

serviceStatsCache:
handler: src/xfd_django/xfd_api/tasks/elasticache_tasks.populate_services_cache
events:
Expand Down
133 changes: 133 additions & 0 deletions backend/src/xfd_django/xfd_api/helpers/infra_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""Infra Ops helpers."""
# File: xfd_api/utils/db_utils.py
# Standard Python Libraries
import os

# Third-Party Libraries
from django.conf import settings
from django.db import connections
import pymysql


def create_scan_user():
"""Create and configure the XFD scanning user if it does not already exist."""
# Only create if not in the DMZ
is_dmz = os.getenv("IS_DMZ", "0") == "1"

if is_dmz:
print("IS_DMZ is set to 1. Skipping creation of the scanning user.")
return

user = os.getenv("POSTGRES_SCAN_USER")
password = os.getenv("POSTGRES_SCAN_PASSWORD")
if not user or not password:
print("POSTGRES_SCAN_USER or POSTGRES_SCAN_PASSWORD is not set.")
return

db_name = settings.DATABASES["default"]["NAME"]

with connections["default"].cursor() as cursor:
try:
# Check if the user already exists
cursor.execute("SELECT 1 FROM pg_roles WHERE rolname = %s;", [user])
user_exists = cursor.fetchone() is not None

if not user_exists:
# Create the user
cursor.execute(
"CREATE ROLE {} LOGIN PASSWORD %s;".format(user), [password]
)
print("User '{}' created successfully.".format(user))
else:
print("User '{}' already exists. Skipping creation.".format(user))

# Grant privileges (idempotent as well)
cursor.execute("GRANT CONNECT ON DATABASE {} TO {};".format(db_name, user))
cursor.execute("GRANT USAGE ON SCHEMA public TO {};".format(user))
cursor.execute(
"GRANT SELECT ON ALL TABLES IN SCHEMA public TO {};".format(user)
)
cursor.execute(
"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO {};".format(
user
)
)

print("User '{}' configured successfully.".format(user))
except Exception as e:
print("Error creating or configuring scan user: {}".format(e))


def create_matomo_scan_user():
"""Create and configure the Matomo scanning user if it does not already exist."""
# Only create if not in the DMZ
is_dmz = os.getenv("IS_DMZ", "0") == "1"
if is_dmz:
print("IS_DMZ is set to 1. Skipping creation of the scanning user.")
return

# Database connection settings
db_host = os.getenv("MATOMO_DB_HOST")
db_name = "matomo"
db_user = "matomo"
db_password = os.getenv("MATOMO_DB_PASSWORD")

scan_user = os.getenv("POSTGRES_SCAN_USER")
scan_password = os.getenv("POSTGRES_SCAN_PASSWORD")

if not all([db_host, db_user, db_password, scan_user, scan_password]):
print("Database connection credentials or scan user details are missing.")
return

try:
conn = pymysql.connect(
host=db_host,
user=db_user,
password=db_password,
database=db_name,
cursorclass=pymysql.cursors.DictCursor,
)

with conn.cursor() as cursor:
# Check if the scan user already exists
cursor.execute("SELECT User FROM mysql.user WHERE User = %s;", [scan_user])
user_exists = cursor.fetchone() is not None

if not user_exists:
# Create the scan user
cursor.execute(
"CREATE USER %s@'%%' IDENTIFIED BY %s;", [scan_user, scan_password]
)
print(
"User '{}' created successfully in Matomo database.".format(
scan_user
)
)
else:
print("User '{}' already exists. Skipping creation.".format(scan_user))

# Grant privileges (aligned with PostgreSQL version)
cursor.execute(
"GRANT CONNECT ON DATABASE {} TO %s@'%%';".format(db_name), [scan_user]
)
cursor.execute("GRANT USAGE ON SCHEMA public TO %s@'%%';", [scan_user])
cursor.execute(
"GRANT SELECT ON {}.* TO %s@'%%';".format(db_name), [scan_user]
)
cursor.execute(
"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO %s@'%%';",
[scan_user],
) # ✅ ADDED
cursor.execute("FLUSH PRIVILEGES;") # MariaDB-specific, but necessary

print(
"User '{}' configured successfully in Matomo database.".format(
scan_user
)
)

conn.commit()
conn.close()

except Exception as e:
print("Error creating or configuring scan user: {}".format(e))
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from django.core.management.base import BaseCommand
from xfd_api.tasks.searchSync import handler as sync_es_domains
from xfd_api.tasks.syncdb_helpers import (
create_scan_user,
drop_all_tables,
manage_elasticsearch_indices,
populate_sample_data,
Expand Down Expand Up @@ -49,10 +48,6 @@ def handle(self, *args, **options):
# Step 2: Elasticsearch Index Management
manage_elasticsearch_indices(dangerouslyforce)

# Step 3: Create the scanning user if doesn't exist
self.stdout.write("Creating and configuring the scanning user...")
create_scan_user()

# Step 4: Populate Sample Data
if populate:
self.stdout.write("Populating the database with sample data...")
Expand Down
37 changes: 37 additions & 0 deletions backend/src/xfd_django/xfd_api/tasks/infra_ops.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Run infra ops."""
# Standard Python Libraries
import os

# Third-Party Libraries
import django

# Set the Django settings module
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xfd_django.settings")
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"

# Initialize Django
django.setup()

# Third-Party Libraries
from xfd_api.helpers.infra_helpers import create_matomo_scan_user, create_scan_user


def handler(event, context):
"""Trigger infra ops."""
try:
# Create the XFD db scanning user if doesn't exist
create_scan_user()

# Create the Matomo db scanning user if doesn't exist
create_matomo_scan_user()

return {
"statusCode": 200,
"body": "Database synchronization completed successfully.",
}
except Exception as e:
print("Error during syncdb: {}".format(str(e)))
return {
"statusCode": 500,
"body": "Database synchronization failed: {}".format(str(e)),
}
49 changes: 0 additions & 49 deletions backend/src/xfd_django/xfd_api/tasks/syncdb_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,52 +506,3 @@ def sync_es_organizations():
except Exception as e:
print("Error syncing organizations: {}".format(e))
raise e


def create_scan_user():
"""Create and configure the scanning user if it does not already exist."""
# Only create if not in the DMZ
is_dmz = os.getenv("IS_DMZ", "0") == "1"

if is_dmz:
print("IS_DMZ is set to 1. Skipping creation of the scanning user.")
return

user = os.getenv("POSTGRES_SCAN_USER")
password = os.getenv("POSTGRES_SCAN_PASSWORD")
if not user or not password:
print("POSTGRES_SCAN_USER or POSTGRES_SCAN_PASSWORD is not set.")
return

db_name = settings.DATABASES["default"]["NAME"]

with connections["default"].cursor() as cursor:
try:
# Check if the user already exists
cursor.execute("SELECT 1 FROM pg_roles WHERE rolname = %s;", [user])
user_exists = cursor.fetchone() is not None

if not user_exists:
# Create the user
cursor.execute(
"CREATE ROLE {} LOGIN PASSWORD %s;".format(user), [password]
)
print("User '{}' created successfully.".format(user))
else:
print("User '{}' already exists. Skipping creation.".format(user))

# Grant privileges (idempotent as well)
cursor.execute("GRANT CONNECT ON DATABASE {} TO {};".format(db_name, user))
cursor.execute("GRANT USAGE ON SCHEMA public TO {};".format(user))
cursor.execute(
"GRANT SELECT ON ALL TABLES IN SCHEMA public TO {};".format(user)
)
cursor.execute(
"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO {};".format(
user
)
)

print("User '{}' configured successfully.".format(user))
except Exception as e:
print("Error creating or configuring scan user: {}".format(e))
Loading