Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions snuba/admin/audit_log/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class AuditLogAction(Enum):
FORCE_MIGRATION_OVERWRITE = "force.migration.overwrite"
RAN_SUDO_SYSTEM_QUERY = "ran.sudo.system.query"
RAN_CLUSTERLESS_SYSTEM_QUERY = "ran.clusterless.system.query"
RAN_ADHOC_MANUAL_JOB = "ran.adhoc.manual.job"


MIGRATION_ACTIONS = [
Expand Down
11 changes: 11 additions & 0 deletions snuba/admin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1028,5 +1028,16 @@ def run_job_by_type(job_type: str) -> Response:
# The runner records status/logs under job_id before raising, so hand
# it back to let operators inspect the failed run's logs.
return make_response(jsonify({"error": str(e), "job_id": job_id}), 500)
finally:
audit_log.record(
g.user.email,
AuditLogAction.RAN_ADHOC_MANUAL_JOB,
{
"job_id": job_id,
"job_type": job_type,
"params": json.dumps(params, sort_keys=True),
},
notify=True,
)

return make_response(jsonify({"job_id": job_id, "status": job_status}), 200)
55 changes: 55 additions & 0 deletions tests/admin/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
)

from snuba import settings
from snuba.admin.audit_log.action import AuditLogAction
from snuba.admin.auth import USER_HEADER_KEY
from snuba.admin.auth_roles import DEFAULT_ROLES
from snuba.admin.clickhouse.clusters import TABLES_DATABASE
from snuba.admin.user import AdminUser
from snuba.datasets.factory import get_enabled_dataset_names
from snuba.web.rpc import RPCEndpoint

Expand Down Expand Up @@ -644,6 +647,58 @@ def test_run_job_by_type_is_repeatable(admin_api: FlaskClient) -> None:
assert len(job_ids) == 2


@pytest.mark.redis_db
def test_run_job_by_type_reports_parameters_and_user(admin_api: FlaskClient) -> None:
params = {"message": "hello", "nested": {"enabled": True}}
user = AdminUser(email="operator@sentry.io", id="123", roles=DEFAULT_ROLES)
with (
mock.patch("snuba.admin.views.audit_log") as audit_log,
mock.patch("snuba.admin.views.authorize_request", return_value=user),
):
response = admin_api.post(
"/job-types/ToyJob/run",
data=json.dumps({"params": params}),
headers={"Content-Type": "application/json"},
)

assert response.status_code == 200
job_id = json.loads(response.data)["job_id"]
audit_log.record.assert_called_once_with(
"operator@sentry.io",
AuditLogAction.RAN_ADHOC_MANUAL_JOB,
{
"job_id": job_id,
"job_type": "ToyJob",
"params": json.dumps(params, sort_keys=True),
},
notify=True,
)


@pytest.mark.redis_db
def test_failed_run_job_by_type_is_reported(admin_api: FlaskClient) -> None:
params = {"fail": True}
with mock.patch("snuba.admin.views.audit_log") as audit_log:
response = admin_api.post(
"/job-types/ToyJob/run",
data=json.dumps({"params": params}),
headers={"Content-Type": "application/json"},
)

assert response.status_code == 500
job_id = json.loads(response.data)["job_id"]
audit_log.record.assert_called_once_with(
"unknown",
AuditLogAction.RAN_ADHOC_MANUAL_JOB,
{
"job_id": job_id,
"job_type": "ToyJob",
"params": json.dumps(params, sort_keys=True),
},
notify=True,
)


@pytest.mark.redis_db
def test_run_job_by_type_rejects_non_adhoc_job(admin_api: FlaskClient) -> None:
response = admin_api.post("/job-types/DeleteEventsByTagKeyValue/run")
Expand Down
Loading