Skip to content

Own deployed dashboards with the engineering account, not any org admin - #94

Merged
imenattatra merged 1 commit into
metr-mainfrom
hotfix/restrict-dash-owner-to-metr-account
Sep 9, 2026
Merged

imenattatra merged 1 commit into
metr-mainfrom
hotfix/restrict-dash-owner-to-metr-account

Conversation

@imenattatra

@imenattatra imenattatra commented Sep 9, 2026 •

Copy link
Copy Markdown

Part of https://github.com/metr-systems/backlog/issues/4764

Note for reviewers: This is a hotfix and i am merging it right away but feel free to review and comment and if there are changes i can apply them as refactoring later, thank you

AI disclaimer : Assisted by claude

Summary

When we deploy a composed dashboard into a customer organization, every object we create there : the dashboard, and each query behind its widgets — needs an owner. Up to now the code just grabbed whichever admin happened to be first in that org's admin group.
With heimstaden, we ran into the case where this was a client account and we had to archive entities and change entities owner by code in order to unarchive them again.

With the update in this PR . The deployment looks up a dedicated per-org engineering account by a fixed email pattern, engineering+@metr.systems, and uses that account as the owner of everything it creates.

Deployment is all-or-nothing across every target org. If even one target org is missing its engineering+@metr.systems user, the entire deployment is rolled back. The run is still recorded, with the failing org and reason listed, so it's clear which org blocked it.

Code Strategy

deploy_to_target_org now gets its owner from a new get_deploy_user, which looks the org's engineering+<slug>@metr.systems user up by org_id and email and raises DeployUserError when it's absent, so deploy_composed_dashboard records it as that org's failure and rolls the whole run back .

>>> from redash.models import Organization
>>> from redash_global.models import ComposedDashboard, DeploymentRun, GlobalAdminUser
>>> from redash_global.deployment.deploy import deploy_composed_dashboard
>>> url_identifier = "composed-dashboard-with-fixed-param"
>>> composed = ComposedDashboard.query.filter_by(url_identifier=url_identifier).first()
>>> target_org = Organization.query.filter_by(slug="new-org").first()
>>> target_org_other = Organization.query.filter_by(slug="staging").first()
>>> deployed_by = GlobalAdminUser.get_by_username("admin@staging.metr.systems")
>>> run = deploy_composed_dashboard(composed, [target_org, target_org_other], deployed_by)
INFO:redash_global.deployment.deploy:Skipping org 1 for composed dashboard 19: no sub-dashboards assigned
[2026-09-09 15:44:26,728][PID:10][INFO][redash_global.deployment.deploy] Skipping org 1 for composed dashboard 19: no sub-dashboards assigned
>>> # show latest run details
>>> run = (
...     DeploymentRun.query.filter_by(composed_dashboard_id=composed.id)
...     .order_by(DeploymentRun.created_at.desc())
...     .first()
... )
>>>
>>> admin = GlobalAdminUser.query.get(run.global_admin_user_id)
>>> print(
...     f"run {run.id} at {run.created_at} by {admin.username}: {'OK' if run.succeeded else 'FAILED'}"
... )
run 6 at 2026-09-09 15:44:26.731185+00:00 by admin@staging.metr.systems: FAILED
>>> for result in run.results:
...     org = Organization.query.get(result.organization_id)
...     for error in result.errors:
...         print(f"  {org.slug}: {error}")
...     if not result.errors:
...         print(f"  {org.slug}: no errors")
...
  new-org: Organization 2 has no deploy user 'engineering+new-org@metr.systems'
>>>
>>>
>>> from redash import models
>>>
>>> org = models.Organization.get_by_slug("new-org")
>>> assert org, "no org with slug 'new-org'"
>>>
>>> email = f"engineering+{org.slug}@metr.systems"
>>> user = models.User(
...     org=org,
...     email=email,
...     name="Admin",
...     group_ids=[org.default_group.id, org.admin_group.id],
... )
>>> user.hash_password("xxx")
>>> models.db.session.add(user)
>>> models.db.session.commit()
>>>
>>>
>>> run = deploy_composed_dashboard(composed, [target_org, target_org_other], deployed_by)
INFO:root:Unable to update hash for query 342 because of invalid parameters: The following parameter values are incompatible with their definitions: address
[2026-09-09 15:50:11,987][PID:10][INFO][root] Unable to update hash for query 342 because of invalid parameters: The following parameter values are incompatible with their definitions: address
INFO:redash_global.deployment.deploy:Skipping org 1 for composed dashboard 19: no sub-dashboards assigned
[2026-09-09 15:50:12,040][PID:10][INFO][redash_global.deployment.deploy] Skipping org 1 for composed dashboard 19: no sub-dashboards assigned
[2026-09-09 15:50:12,055][PID:10][INFO][rq.job.redash.tasks.queries.execution] job.func_name= job.id= Inserting job for 776de89476f82df1b396df4be5c0b905 with metadata={'query_id': 341}
[2026-09-09 15:50:12,061][PID:10][INFO][rq.job.redash.tasks.queries.execution] job.func_name= job.id= [776de89476f82df1b396df4be5c0b905] Created new job: cfd1d849-137f-4473-946f-9c72db81bc24
>>>
>>>
>>> run = (
...     DeploymentRun.query.filter_by(composed_dashboard_id=composed.id)
...     .order_by(DeploymentRun.created_at.desc())
...     .first()
... )
>>>
>>> admin = GlobalAdminUser.query.get(run.global_admin_user_id)
>>> print(
...     f"run {run.id} at {run.created_at} by {admin.username}: {'OK' if run.succeeded else 'FAILED'}"
... )
run 7 at 2026-09-09 15:50:12.042686+00:00 by admin@staging.metr.systems: OK
>>> for result in run.results:
...     org = Organization.query.get(result.organization_id)
...     for error in result.errors:
...         print(f"  {org.slug}: {error}")
...     if not result.errors:
...         print(f"  {org.slug}: no errors")
...
  new-org: no errors
>>>

QA

  • Unit tests (pytest)
  • Manually(staging , see the code above)

@imenattatra
imenattatra requested review from a team and helenalebreton September 9, 2026 16:03
@imenattatra
imenattatra merged commit 765ad31 into metr-main Sep 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants