Skip to content
Closed
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
Empty file.
46 changes: 46 additions & 0 deletions ddpui/api/admin_portal/organizations_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from ninja import Router
from ninja.errors import HttpError

from ddpui.auth import has_permission
from ddpui.models.org import Org

admin_org_router = Router()


@admin_org_router.get("/v1/organizations")
@admin_org_router.get("/v1/organizations/")
@has_permission(["can_manage_organization"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check seed files for permission slugs
rg -rn "can_manage_organ" --type=json
rg -rn "can_manage_organ" --type=py

Repository: DalgoT4D/DDP_backend

Length of output: 212


🏁 Script executed:

cat -n ddpui/api/admin_portal/organizations_api.py | head -30

Repository: DalgoT4D/DDP_backend

Length of output: 1048


🏁 Script executed:

find . -type f -name "*seed*" -o -name "*fixture*" -o -name "*migration*" | grep -E "\.(py|json|sql)$" | head -20

Repository: DalgoT4D/DDP_backend

Length of output: 151


🏁 Script executed:

rg -i "permission" --type=py -l | head -15

Repository: DalgoT4D/DDP_backend

Length of output: 695


🏁 Script executed:

cat -n ddpui/models/role_based_access.py

Repository: DalgoT4D/DDP_backend

Length of output: 1464


🏁 Script executed:

grep -n "can_manage_organ" ddpui/core/dbt_automation/seeds/seed.py

Repository: DalgoT4D/DDP_backend

Length of output: 46


🏁 Script executed:

grep -n "can_manage_organ" ddpui/management/commands/seed_map_data.py

Repository: DalgoT4D/DDP_backend

Length of output: 46


🏁 Script executed:

rg "can_manage_organ" -i

Repository: DalgoT4D/DDP_backend

Length of output: 242


🏁 Script executed:

find . -path ./node_modules -prune -o -type f -name "*migration*" -print | grep -E "\.py$" | head -20

Repository: DalgoT4D/DDP_backend

Length of output: 46


🏁 Script executed:

find . -path ./node_modules -prune -o -type f -name "*fixture*" -print | head -20

Repository: DalgoT4D/DDP_backend

Length of output: 46


🏁 Script executed:

rg "Permission" --type=py -A 3 -B 1 | grep -E "(can_manage|slug|create)" | head -30

Repository: DalgoT4D/DDP_backend

Length of output: 3547


🏁 Script executed:

find . -type f \( -name "conftest.py" -o -name "*test_auth*" \) | head -10

Repository: DalgoT4D/DDP_backend

Length of output: 177


🏁 Script executed:

ls -la ddpui/migrations/ | head -30

Repository: DalgoT4D/DDP_backend

Length of output: 2542


🏁 Script executed:

cat -n ddpui/tests/core/test_auth.py | head -100

Repository: DalgoT4D/DDP_backend

Length of output: 3710


🏁 Script executed:

rg "can_manage_org" ddpui/migrations/ -A 2 -B 2

Repository: DalgoT4D/DDP_backend

Length of output: 2156


🏁 Script executed:

find . -name "001_roles.json" -o -name "002_permissions.json" -o -name "003_role_permissions.json"

Repository: DalgoT4D/DDP_backend

Length of output: 147


🏁 Script executed:

find . -type d -name "fixtures" | head -10

Repository: DalgoT4D/DDP_backend

Length of output: 46


🏁 Script executed:

cat seed/002_permissions.json

Repository: DalgoT4D/DDP_backend

Length of output: 13642


🏁 Script executed:

cat seed/003_role_permissions.json

Repository: DalgoT4D/DDP_backend

Length of output: 34486


🏁 Script executed:

grep -r "has_permission" ddpui/auth.py -A 10 | head -40

Repository: DalgoT4D/DDP_backend

Length of output: 546


🏁 Script executed:

cat seed/001_roles.json

Repository: DalgoT4D/DDP_backend

Length of output: 983


🏁 Script executed:

cat -n ddpui/auth.py | grep -A 25 "def has_permission"

Repository: DalgoT4D/DDP_backend

Length of output: 1142


Add missing permission can_manage_organization to seed data or use existing permission slug

The permission slug can_manage_organization does not exist in seed/002_permissions.json. The @has_permission decorator will deny all requests to this endpoint with a 403 error since the required permission can never be found in user permissions. Either add this permission to the seed data or use an existing permission slug that matches the intended authorization level.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ddpui/api/admin_portal/organizations_api.py` at line 12, The endpoint is
guarded by `@has_permission`("can_manage_organization") but that permission slug
is missing from the permissions seed, causing 403s; fix by either adding a
matching permission entry with slug "can_manage_organization" to the permissions
seed (002_permissions.json) with appropriate name/description and re-seed, or
change the decorator to use an existing permission slug that expresses the same
intent (e.g., an existing "manage_organizations" or similar slug) in
organizations_api.py so the permission check matches seeded permissions.

def get_admin_organizations(request, name: str | None = None):
"""List organizations with optional filtering."""
queryset = Org.objects.all()

if name:
queryset = queryset.filter(name__icontains=name)

orgs = list(queryset.values("id", "name", "slug"))

return {
"success": True,
"count": len(orgs),
"data": orgs,
}


@admin_org_router.get("/v1/organizations/{org_id}")
@admin_org_router.get("/v1/organizations/{org_id}/")
@has_permission(["can_manage_organization"])
def get_single_org(request, org_id: int):
"""Get single organization by ID."""
org = (
Org.objects.filter(id=org_id)
.values("id", "name", "slug")
.first()
)

if not org:
raise HttpError(404, "Organization not found")

return {
"success": True,
"data": org,
}
11 changes: 9 additions & 2 deletions ddpui/migrations/0085_syncstats.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ class Migration(migrations.Migration):
(
"sync_type",
models.CharField(
choices=[("manual", "manual"), ("orchestrate", "orchestrate")]
max_length=20,
choices=[
("manual", "manual"),
("orchestrate", "orchestrate"),
],
),
),
("sync_time", models.DateTimeField()),
Expand All @@ -37,7 +41,10 @@ class Migration(migrations.Migration):
("sync_data_volume_b", models.IntegerField(default=0)),
(
"org",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="ddpui.org"),
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="ddpui.org",
),
),
],
),
Expand Down
27 changes: 21 additions & 6 deletions ddpui/models/airbyte.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""track sync times, number of records, volume of data by client and connection"""

from django.db import models

from ddpui.models.org import Org


Expand All @@ -12,7 +13,12 @@ class SyncStats(models.Model):
job_id = models.IntegerField(null=True)
attempt = models.IntegerField(default=0)
status = models.TextField()
sync_type = models.CharField(choices=[("manual", "manual"), ("orchestrate", "orchestrate")])

sync_type = models.CharField(
max_length=20,
choices=[("manual", "manual"), ("orchestrate", "orchestrate")],
)

sync_time = models.DateTimeField()
sync_duration_s = models.BigIntegerField(default=0)
sync_records = models.BigIntegerField(default=0)
Expand Down Expand Up @@ -41,7 +47,9 @@ class AirbyteJob(models.Model):
job_type = models.CharField(
max_length=20
) # check_connection_source┃check_connection_destination┃discover_schema┃get_spec┃sync┃reset_connection┃refresh┃clear
config_id = models.CharField(max_length=100) # connection_id, source_id, destination_id
config_id = models.CharField(
max_length=100
) # connection_id, source_id, destination_id
status = models.CharField(
max_length=20
) # pending┃running┃incomplete┃failed┃succeeded┃cancelled
Expand All @@ -63,15 +71,22 @@ class AirbyteJob(models.Model):
null=True
) # contains information about the attempts made for this job. only populated if the job has attempts.

started_at = models.DateTimeField(null=True) # because the api spec says this will be optional
started_at = models.DateTimeField(
null=True
) # because the api spec says this will be optional
ended_at = models.DateTimeField(
null=True
) # when the job ended; can be null if we pull or sync an ongonig job
) # when the job ended; can be null if we pull or sync an ongoing job
created_at = models.DateTimeField() # when the job was created in airbyte
updated_at = models.DateTimeField(auto_now=True) # when the django record was last updated
updated_at = models.DateTimeField(
auto_now=True
) # when the django record was last updated

def __str__(self) -> str:
return f"AirbyteJob[Job ID: {self.job_id}|Job Type: {self.job_type}|Status: {self.status}]"
return (
f"AirbyteJob[Job ID: {self.job_id}|"
f"Job Type: {self.job_type}|Status: {self.status}]"
)

@property
def duration(self):
Expand Down
17 changes: 9 additions & 8 deletions ddpui/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,33 @@
from pydantic import ValidationError as PydanticValidationError

from ddpui import auth
from ddpui.api.admin_portal.organizations_api import admin_org_router
from ddpui.api.airbyte_api import airbyte_router
from ddpui.api.charts_api import charts_router
from ddpui.api.dashboard_api import dashboard_router
from ddpui.api.dashboard_native_api import dashboard_native_router
from ddpui.api.data_api import data_router
from ddpui.api.dbt_api import dbt_router
from ddpui.api.filter_api import filter_router
from ddpui.api.notifications_api import notification_router
from ddpui.api.org_preferences_api import orgpreference_router
from ddpui.api.orgtask_api import orgtask_router
from ddpui.api.pipeline_api import pipeline_router
from ddpui.api.public_api import public_router
from ddpui.api.report_api import report_router
from ddpui.api.superset_api import superset_router
from ddpui.api.task_api import task_router
from ddpui.api.transform_api import transform_router
from ddpui.api.user_org_api import user_org_router
from ddpui.api.user_preferences_api import userpreference_router
from ddpui.api.org_preferences_api import orgpreference_router
from ddpui.api.warehouse_api import warehouse_router
from ddpui.api.webhook_api import webhook_router
from ddpui.api.charts_api import charts_router
from ddpui.api.dashboard_native_api import dashboard_native_router
from ddpui.api.filter_api import filter_router
from ddpui.api.public_api import public_router
from ddpui.api.report_api import report_router


src_api = NinjaAPI(
urls_namespace="api",
title="Dalgo backend apis",
description="Open source ELT orchestrator",
docs_url="/api/docs",
# auth=auth.CustomAuthMiddleware(),
auth=auth.CustomJwtAuthMiddleware(),
)

Expand Down Expand Up @@ -108,6 +107,7 @@ def ninja_default_error_handler(
src_api.add_router("/api/dashboards/", dashboard_native_router)
src_api.add_router("/api/filters/", filter_router)
src_api.add_router("/api/reports/", report_router)
src_api.add_router("/api/admin/", admin_org_router)

# Public API without authentication
public_api = NinjaAPI(
Expand All @@ -116,4 +116,5 @@ def ninja_default_error_handler(
description="Public endpoints for shared dashboards - no authentication required",
docs_url="/api/v1/public/docs",
)

public_api.add_router("/api/v1/public/", public_router)