Skip to content

Feature/resource sharing v2 - #1436

Open
siddhant3030 wants to merge 46 commits into
mainfrom
feature/resource-sharing-v2
Open

Feature/resource sharing v2#1436
siddhant3030 wants to merge 46 commits into
mainfrom
feature/resource-sharing-v2

Conversation

@siddhant3030

@siddhant3030 siddhant3030 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Added resource sharing for dashboards, charts, reports, and KPIs with view/edit access for people, groups, and pending invitations.
    • Added access requests, approvals, declines, privacy controls, and ownership transfers.
    • Added organization People and Groups management.
    • Added configurable access defaults, public-sharing settings, and per-resource access indicators.
    • Added access-aware viewing, editing, listing, duplication, locking, filtering, and sharing.
  • Bug Fixes
    • Invitation acceptance now preserves group memberships and resource access.
    • Removed related shares and access requests when resources are deleted.

Ishankoradia and others added 6 commits July 22, 2026 23:49
…Dict)

sentry_sdk auto-loads its anthropic integration at settings import; the
new anthropic SDK's generated types use TypedDict(extra_items=...), which
typing-extensions 4.12.2 rejects, crashing every manage.py command.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds organization groups, resource grants, access requests, access defaults, centralized access evaluation, ownership authorization, and resource-level checks. New models, APIs, migrations, schemas, permissions, and invitation promotion logic support user, group, and resource access management.

Changes

Access controls

Layer / File(s) Summary
Access data contracts and persistence
ddpui/models/*, ddpui/schemas/access/*, ddpui/migrations/*, seed/*
Adds resource-share, access-request, group, privacy, and organization preference models. Adds API schemas, migrations, seeded permissions, role identifiers, and package exports.
Access resolution and ownership
ddpui/core/access/*, ddpui/auth.py, ddpui/core/*/*
Adds resource lookup, access precedence, accessible query filters, ownership checks, ownership transfer, and the has_access decorator.
Resource sharing and access requests
ddpui/api/access_api.py, ddpui/core/access/resource_share.py, ddpui/routes.py
Adds grant CRUD, dashboard cascade synchronization, privacy toggling, ownership transfer, access-request workflows, and the mounted access router.
People, groups, and invitation promotion
ddpui/api/user_org_api.py, ddpui/models/org_user.py, ddpui/core/orguserfunctions.py
Adds people listing, group CRUD, member management, pending invitation handling, resource-share cleanup, and invitation acceptance promotion.
Resource API enforcement
ddpui/api/dashboard_native_api.py, ddpui/api/charts_api.py, ddpui/api/kpi_api.py, ddpui/api/report_api.py, ddpui/services/*, ddpui/schemas/*
Applies resource filtering and access checks to dashboard, chart, KPI, and report operations. Responses include resolved access levels, and deletions remove related shares and requests.
Organization access defaults
ddpui/api/org_preferences_api.py, ddpui/schemas/org_preferences_schema.py, ddpui/models/org_preferences.py
Adds the authenticated endpoint for updating organization-wide access defaults and public-sharing settings.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: ishankoradia

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the pull request as the second version of the resource-sharing feature, which matches the main changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/resource-sharing-v2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
ddpui/api/access_api.py (1)

1-110: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

CI: black reformatted this file.

Same 0_checks (3.10, 6) failure as ddpui/core/access/resource_share.py — run black ddpui/api/access_api.py locally (this would also collapse the unnecessarily multi-line add_resource_grants signature at lines 60-62) and commit the result.

🤖 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/access_api.py` around lines 1 - 110, Run Black on the access API
module and commit its formatting changes, including collapsing the unnecessarily
split add_resource_grants signature while preserving all behavior.

Source: Pipeline failures

ddpui/core/access/resource_share.py (1)

1-280: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

CI: black reformatted this file.

The 0_checks (3.10, 6) job failed because the black pre-commit hook modified this file. Run black ddpui/core/access/resource_share.py locally and commit the result.

🤖 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/core/access/resource_share.py` around lines 1 - 280, Run Black on the
resource-share module, applying its formatting changes to the file containing
list_grants, add_grants, and _resolve_pending_email_to_invitation_id, then
commit the reformatted result.

Source: Pipeline failures

🧹 Nitpick comments (2)
ddpui/schemas/access/resource_share_schema.py (1)

48-56: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Use EmailStr for PendingGrantPayload.email.

Plain str accepts any garbage input; _resolve_pending_email_to_invitation_id only normalizes with .strip().lower() and doesn't validate format, so a malformed email can create a bogus Invitation/ResourceShare row. Pydantic's EmailStr catches this at the schema boundary.

♻️ Proposed fix
-from ninja import Schema
+from ninja import Schema
+from pydantic import EmailStr

 class PendingGrantPayload(Schema):
     """..."""
-    email: str
+    email: EmailStr
     access_level: AccessLevel
🤖 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/schemas/access/resource_share_schema.py` around lines 48 - 56, Update
PendingGrantPayload.email from str to Pydantic’s EmailStr type, preserving the
existing access_level field and payload behavior while enforcing email-format
validation at schema validation.
pyproject.toml (1)

261-261: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer a bounded specifier over an unbounded >=.

Moving from an exact pin to >=4.14.0 trades reproducibility for an open-ended range. typing_extensions's own docs recommend depending on it as a compatible-release range rather than an unbounded floor.

♻️ Proposed fix
-    "typing-extensions>=4.14.0",
+    "typing-extensions~=4.14",

Confirm which of your other dependencies actually require >=4.14.0 before deciding on the bound.

🤖 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 `@pyproject.toml` at line 261, Update the typing-extensions dependency
specifier in pyproject.toml to use a bounded compatible-release range instead of
the unbounded >=4.14.0 constraint. First verify which dependencies require
version 4.14.0, then choose an upper bound that preserves those requirements
while keeping dependency resolution reproducible.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@ddpui/api/access_api.py`:
- Around line 7-19: Remove the unused has_permission import and update the add,
update, and delete grant endpoints in the access router to enforce their
corresponding can_share_* permissions before invoking resource_share operations.
Preserve owner/admin access while rejecting org users who lack the relevant
sharing permission, using the existing is_creator_or_admin and router endpoint
symbols.

In `@ddpui/api/user_org_api.py`:
- Around line 615-634: Move invitation email dispatch out of the
transaction.atomic flow used by add_user_group_members, coordinating with the
shared invitation logic in create_user_group. Ensure database changes complete
successfully before sending invitations, using the existing
transaction-completion mechanism where appropriate, while preserving member and
invitation creation behavior.
- Around line 293-305: Guard the nullable new_role access in the active-user
branch of the OrgUser iteration, matching the existing pending-invitation
handling below: return a safe null/default value for role_slug and role_name
when curr_orguser.new_role is absent, while preserving the current values when a
role exists.
- Around line 524-550: Move invitation email delivery out of the
transaction.atomic scope in create_user_group, coordinating with the shared
_resolve_pending_emails and _add_members_to_group flow so emails are sent only
after the database transaction commits. Apply the same change to the
corresponding add_user_group_members path, preserving group creation and
membership updates within the transaction.
- Around line 460-472: Update the callers create_user_group and
add_user_group_members to retain the newly created active OrgUser IDs returned
by invite_user_v1, merge them with the existing orguser_ids, and pass the
combined IDs to _add_members_to_group. Ensure existing-user invitations, which
do not create Invitation rows, are still linked to the group and represented in
the returned email-to-invitation mapping as appropriate.

In `@ddpui/core/orguserfunctions.py`:
- Around line 317-359: The invitation acceptance flow, including
accept_invitation_v1 and its caller post_organization_user_accept_invite_v1,
must execute atomically. Add the established transaction.atomic protection
around the full acceptance operation so failures roll back group membership and
resource-share promotions along with the invitation deletion.

In `@ddpui/models/org_user.py`:
- Around line 255-273: Add a database-level UniqueConstraint for the (group,
orguser) pair in OrgUserGroupMember.Meta.constraints, alongside the existing
invitation constraint. Preserve the current invitation uniqueness constraint and
use a distinct constraint name for the orguser pairing.

In `@ddpui/models/resource_share.py`:
- Around line 63-70: Update the ResourceShare model’s Meta constraints to add
database-level uniqueness for the active principal/resource grant identity used
by add_grants, covering org, resource_type, resource_id, principal_type, and
principal_id. Preserve the existing lookup indexes and align the constraint with
the app’s idempotency semantics so concurrent creates cannot produce duplicate
active rows.

---

Outside diff comments:
In `@ddpui/api/access_api.py`:
- Around line 1-110: Run Black on the access API module and commit its
formatting changes, including collapsing the unnecessarily split
add_resource_grants signature while preserving all behavior.

In `@ddpui/core/access/resource_share.py`:
- Around line 1-280: Run Black on the resource-share module, applying its
formatting changes to the file containing list_grants, add_grants, and
_resolve_pending_email_to_invitation_id, then commit the reformatted result.

---

Nitpick comments:
In `@ddpui/schemas/access/resource_share_schema.py`:
- Around line 48-56: Update PendingGrantPayload.email from str to Pydantic’s
EmailStr type, preserving the existing access_level field and payload behavior
while enforcing email-format validation at schema validation.

In `@pyproject.toml`:
- Line 261: Update the typing-extensions dependency specifier in pyproject.toml
to use a bounded compatible-release range instead of the unbounded >=4.14.0
constraint. First verify which dependencies require version 4.14.0, then choose
an upper bound that preserves those requirements while keeping dependency
resolution reproducible.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6c1762a0-e4c7-46f9-ae5e-a65e0c5ba872

📥 Commits

Reviewing files that changed from the base of the PR and between 82c019d and 149c7f6.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (28)
  • ddpui/api/access_api.py
  • ddpui/api/dashboard_native_api.py
  • ddpui/api/org_preferences_api.py
  • ddpui/api/user_org_api.py
  • ddpui/core/access/__init__.py
  • ddpui/core/access/ownership.py
  • ddpui/core/access/resource_share.py
  • ddpui/core/access/shareable_types.py
  • ddpui/core/alerts/alert_service.py
  • ddpui/core/kpi/kpi_service.py
  • ddpui/core/metric/metric_service.py
  • ddpui/core/orguserfunctions.py
  • ddpui/core/reports/report_service.py
  • ddpui/migrations/0170_orgusergroup_orgpreferences_allow_public_sharing_and_more.py
  • ddpui/models/__init__.py
  • ddpui/models/org_preferences.py
  • ddpui/models/org_user.py
  • ddpui/models/resource_share.py
  • ddpui/routes.py
  • ddpui/schemas/access/__init__.py
  • ddpui/schemas/access/resource_share_schema.py
  • ddpui/schemas/org_preferences_schema.py
  • ddpui/services/chart_service.py
  • ddpui/services/dashboard_service.py
  • ddpui/tests/core/test_ownership.py
  • pyproject.toml
  • seed/002_permissions.json
  • seed/003_role_permissions.json

Comment thread ddpui/api/access_api.py
Comment thread ddpui/api/user_org_api.py
Comment on lines +293 to +305
for curr_orguser in OrgUser.objects.filter(org=org).select_related("user", "new_role"):
rows.append(
PeopleRow(
email=curr_orguser.user.email,
role_slug=curr_orguser.new_role.slug,
role_name=curr_orguser.new_role.name,
status="active",
created_by_email=None,
orguser_id=curr_orguser.id,
invitation_id=None,
created_at=curr_orguser.user.date_joined,
)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unguarded new_role access can crash the endpoint.

OrgUser.new_role is nullable (on_delete=models.SET_NULL, null=True), so curr_orguser.new_role.slug/.name will raise AttributeError for any active org user whose role was deleted. Note the pending-invitation branch just below (Lines 313-314) already guards this the same way — the active branch should be consistent.

🐛 Proposed fix
             PeopleRow(
                 email=curr_orguser.user.email,
-                role_slug=curr_orguser.new_role.slug,
-                role_name=curr_orguser.new_role.name,
+                role_slug=curr_orguser.new_role.slug if curr_orguser.new_role else "",
+                role_name=curr_orguser.new_role.name if curr_orguser.new_role else "",
                 status="active",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for curr_orguser in OrgUser.objects.filter(org=org).select_related("user", "new_role"):
rows.append(
PeopleRow(
email=curr_orguser.user.email,
role_slug=curr_orguser.new_role.slug,
role_name=curr_orguser.new_role.name,
status="active",
created_by_email=None,
orguser_id=curr_orguser.id,
invitation_id=None,
created_at=curr_orguser.user.date_joined,
)
)
for curr_orguser in OrgUser.objects.filter(org=org).select_related("user", "new_role"):
rows.append(
PeopleRow(
email=curr_orguser.user.email,
role_slug=curr_orguser.new_role.slug if curr_orguser.new_role else "",
role_name=curr_orguser.new_role.name if curr_orguser.new_role else "",
status="active",
created_by_email=None,
orguser_id=curr_orguser.id,
invitation_id=None,
created_at=curr_orguser.user.date_joined,
)
)
🤖 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/user_org_api.py` around lines 293 - 305, Guard the nullable
new_role access in the active-user branch of the OrgUser iteration, matching the
existing pending-invitation handling below: return a safe null/default value for
role_slug and role_name when curr_orguser.new_role is absent, while preserving
the current values when a role exists.

Comment thread ddpui/api/user_org_api.py
Comment on lines +460 to +472
for email in to_invite:
payload = NewInvitationSchema(
invited_email=email,
invited_role_uuid=invite_role_uuid,
)
_, error = orguserfunctions.invite_user_v1(orguser, payload)
if error:
raise HttpError(400, f"failed to invite {email}: {error}")
invitation = Invitation.objects.filter(invited_by__org=org, invited_email=email).first()
if invitation is not None:
existing_invites[email] = invitation.id

return existing_invites

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Newly-invited emails belonging to existing platform users silently never join the group.

When invite_user_v1 (ddpui/core/orguserfunctions.py:207-278) is called for an email that already has a Django User account, it creates an OrgUser directly and returns without creating an Invitation row. Back here, the subsequent Invitation.objects.filter(...).first() lookup then returns None, so existing_invites[email] is never populated for that email — the address is silently dropped from the returned email_to_invitation_id dict, and the freshly-created OrgUser is never linked via _add_members_to_group. The caller gets a 200 response with no indication that the member wasn't added.

🐛 Proposed fix sketch
     if to_invite:
         ...
+        newly_active_orguser_ids: list[int] = []
         for email in to_invite:
             payload = NewInvitationSchema(
                 invited_email=email,
                 invited_role_uuid=invite_role_uuid,
             )
             _, error = orguserfunctions.invite_user_v1(orguser, payload)
             if error:
                 raise HttpError(400, f"failed to invite {email}: {error}")
             invitation = Invitation.objects.filter(invited_by__org=org, invited_email=email).first()
             if invitation is not None:
                 existing_invites[email] = invitation.id
+            else:
+                ou = OrgUser.objects.filter(org=org, user__email__iexact=email).first()
+                if ou is not None:
+                    newly_active_orguser_ids.append(ou.id)
 
-    return existing_invites
+    return existing_invites, newly_active_orguser_ids

Callers (create_user_group, add_user_group_members) would need to merge newly_active_orguser_ids into the orguser_ids passed to _add_members_to_group.

🤖 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/user_org_api.py` around lines 460 - 472, Update the callers
create_user_group and add_user_group_members to retain the newly created active
OrgUser IDs returned by invite_user_v1, merge them with the existing
orguser_ids, and pass the combined IDs to _add_members_to_group. Ensure
existing-user invitations, which do not create Invitation rows, are still linked
to the group and represented in the returned email-to-invitation mapping as
appropriate.

Comment thread ddpui/api/user_org_api.py
Comment on lines +524 to +550
@user_org_router.post(
"/v1/organizations/user_groups",
response=GroupDetailSchema,
)
@has_permission(["can_create_user_group"])
@transaction.atomic
def create_user_group(request, payload: CreateGroupPayload):
orguser: OrgUser = request.orguser
if orguser.org is None:
raise HttpError(400, "no associated org")

name = payload.name.strip()
if not name:
raise HttpError(400, "group name is required")

if OrgUserGroup.objects.filter(org=orguser.org, name__iexact=name).exists():
raise HttpError(400, "a group with this name already exists")

email_to_invitation_id = _resolve_pending_emails(
orguser.org, orguser, payload.pending_emails, payload.invite_role_uuid
)

group = OrgUserGroup.objects.create(org=orguser.org, name=name, created_by=orguser)
_add_members_to_group(group, payload.orguser_ids, email_to_invitation_id)

return _serialize_group_detail(group)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Invitation email sent inside transaction.atomic — root cause shared with add_user_group_members.

See consolidated comment below for details and the second affected site.

🤖 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/user_org_api.py` around lines 524 - 550, Move invitation email
delivery out of the transaction.atomic scope in create_user_group, coordinating
with the shared _resolve_pending_emails and _add_members_to_group flow so emails
are sent only after the database transaction commits. Apply the same change to
the corresponding add_user_group_members path, preserving group creation and
membership updates within the transaction.

Comment thread ddpui/api/user_org_api.py
Comment on lines +615 to +634
@user_org_router.post(
"/v1/organizations/user_groups/{group_id}/members",
response=GroupDetailSchema,
)
@has_permission(["can_edit_user_group"])
@transaction.atomic
def add_user_group_members(request, group_id: int, payload: AddMembersPayload):
orguser: OrgUser = request.orguser
if orguser.org is None:
raise HttpError(400, "no associated org")
group = OrgUserGroup.objects.filter(id=group_id, org=orguser.org).first()
if group is None:
raise HttpError(404, "group not found")

email_to_invitation_id = _resolve_pending_emails(
orguser.org, orguser, payload.pending_emails, payload.invite_role_uuid
)
_add_members_to_group(group, payload.orguser_ids, email_to_invitation_id)
return _serialize_group_detail(group)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Invitation email sent inside transaction.atomic — root cause shared with create_user_group.

See consolidated comment below for details and the second affected site.

🤖 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/user_org_api.py` around lines 615 - 634, Move invitation email
dispatch out of the transaction.atomic flow used by add_user_group_members,
coordinating with the shared invitation logic in create_user_group. Ensure
database changes complete successfully before sending invitations, using the
existing transaction-completion mechanism where appropriate, while preserving
member and invitation creation behavior.

Comment on lines +317 to 359

# Preserve any group memberships that were pinned to this invitation:
# promote each member row to point at the accepting orguser instead. If
# the orguser is already a direct member of that group, drop the
# invitation-linked row to avoid duplicates. After this, invitation.delete()
# nulls out any remaining invitation_id via SET_NULL.
from ddpui.models.org_user import OrgUserGroupMember # local import to avoid cycles
from ddpui.models.resource_share import ResourceShare, ResourceSharePrincipalType

invitation_member_rows = OrgUserGroupMember.objects.filter(invitation=invitation)
existing_group_ids = set(
OrgUserGroupMember.objects.filter(orguser=orguser).values_list("group_id", flat=True)
)
for member_row in invitation_member_rows:
if member_row.group_id in existing_group_ids:
member_row.delete()
else:
member_row.orguser = orguser
member_row.save(update_fields=["orguser", "updated_at"])
existing_group_ids.add(member_row.group_id)

# Promote any pending resource shares in the same way: point them at the
# accepting orguser as a direct user grant. If the orguser already has a
# direct share on the same resource, drop the invitation-linked row.
invitation_share_rows = ResourceShare.objects.filter(invitation=invitation)
existing_direct_keys = set(
ResourceShare.objects.filter(
org=invitation.invited_by.org,
principal_type=ResourceSharePrincipalType.USER,
principal_id=orguser.id,
).values_list("resource_type", "resource_id")
)
for share_row in invitation_share_rows:
key = (share_row.resource_type, share_row.resource_id)
if key in existing_direct_keys:
share_row.delete()
else:
share_row.principal_type = ResourceSharePrincipalType.USER
share_row.principal_id = orguser.id
share_row.save(update_fields=["principal_type", "principal_id"])
existing_direct_keys.add(key)

invitation.delete()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -B5 'def accept_invitation_v1' ddpui/core/orguserfunctions.py
rg -n -B3 -A3 'accept_invitation_v1' ddpui/api/user_org_api.py

Repository: DalgoT4D/DDP_backend

Length of output: 640


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== orguserfunctions imports and accept_invitation_v1 wrapper =="
sed -n '1,80p;240,365p' ddpui/core/orguserfunctions.py

echo
echo "== user_org_api accept endpoint relevant block =="
sed -n '820,860p' ddpui/api/user_org_api.py

echo
echo "== search for atomic decorators/calls around accept_invitation_v1 =="
rg -n 'transaction\.atomic|`@transaction`|accept_invitation_v1|accept_invitation' ddpui -g '*.py'

Repository: DalgoT4D/DDP_backend

Length of output: 11480


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== imports and accept_invitation_v1 wrapper =="
sed -n '1,80p' ddpui/core/orguserfunctions.py
sed -n '240,365p' ddpui/core/orguserfunctions.py

echo
echo "== user_org_api accept endpoint relevant block =="
sed -n '820,860p' ddpui/api/user_org_api.py

echo
echo "== atomic/accept_invitation references (concise) =="
rg -n 'transaction\.atomic|`@transaction`|accept_invitation_v1|accept_invitation' ddpui -g '*.py'

Repository: DalgoT4D/DDP_backend

Length of output: 11446


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== user_org_api relevant surrounding lines around nearby atoms and accept route =="
sed -n '505,545p' ddpui/api/user_org_api.py
sed -n '608,630p' ddpui/api/user_org_api.py
sed -n '836,860p' ddpui/api/user_org_api.py

echo
echo "== line-numbered context for user_org_api imports/router setup =="
sed -n '1,45p' ddpui/api/user_org_api.py
rg -n -C2 'user_org_router\s*=|`@router`|database_router|connection' ddpui/api/user_org_api.py

Repository: DalgoT4D/DDP_backend

Length of output: 4752


Wrap the invitation-accept path in a transaction.

post_organization_user_accept_invite_v1 calls accept_invitation_v1 without @transaction.atomic, and accept_invitation_v1 performs several DB writes before invitation.delete(). If any step fails partway through, group memberships or resource shares can be promoted while others remain pinned to the still-existing invitation.

🤖 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/core/orguserfunctions.py` around lines 317 - 359, The invitation
acceptance flow, including accept_invitation_v1 and its caller
post_organization_user_accept_invite_v1, must execute atomically. Add the
established transaction.atomic protection around the full acceptance operation
so failures roll back group membership and resource-share promotions along with
the invitation deletion.

Comment thread ddpui/models/org_user.py
Comment on lines +255 to +273
class OrgUserGroupMember(models.Model):
"""One membership row: a group has this ``OrgUser`` (active) or this
``invitation`` (invited, not yet a user) as a member. When the invitation
is accepted, ``orguser`` is set and ``invitation`` becomes NULL via
``on_delete=SET_NULL``."""

group = models.ForeignKey(OrgUserGroup, on_delete=models.CASCADE, related_name="members")
orguser = models.ForeignKey(OrgUser, on_delete=models.CASCADE, null=True)
invitation = models.ForeignKey(Invitation, on_delete=models.SET_NULL, null=True)
created_at = models.DateTimeField(auto_created=True, default=timezone.now)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
db_table = "orguser_group_member"
constraints = [
models.UniqueConstraint(
fields=["group", "invitation"], name="uq_orguser_group_member_invitation"
)
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Asymmetric uniqueness: (group, invitation) is constrained, (group, orguser) is not.

OrgUserGroupMember guards duplicate invitation-based membership at the DB level but not duplicate orguser-based membership, even though _add_members_to_group uses the same check-then-create idempotency pattern for both paths. See consolidated comment (shared root cause with ResourceShare in ddpui/models/resource_share.py).

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 269-273: Mutable default value for class attribute

(RUF012)

🤖 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/models/org_user.py` around lines 255 - 273, Add a database-level
UniqueConstraint for the (group, orguser) pair in
OrgUserGroupMember.Meta.constraints, alongside the existing invitation
constraint. Preserve the current invitation uniqueness constraint and use a
distinct constraint name for the orguser pairing.

Comment thread ddpui/models/resource_share.py
siddhant3030 and others added 5 commits July 27, 2026 18:00
…rs + gating

Grants and org-default floors are now enforced on dashboard endpoints
(previously grants could be created/displayed but nothing consumed them).

- core/access/access_resolver.py: the single per-resource access decision
  (effective_level / effective_levels / accessible_q). Precedence:
  creator/admin -> explicit user grant -> group grants (max) -> org floor.
- auth.py: with_resource + require_level decorators (next to has_permission).
- dashboard endpoints: edit-path gates move to the area slug can_view_dashboards
  + require_level("edit"), so a granted/floored member can edit; list filters
  via accessible_q; detail/list responses carry my_access.

Dashboard-only this pass (charts/reports follow). No-op for orgs on the
default view floors; behavior changes only when a floor is lowered or a
grant is added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Purely mechanical rename for readability (no behavior change):
- module access_resolver.py -> resource_access.py
- effective_level    -> get_user_access
- effective_levels   -> get_user_access_map
- accessible_q       -> accessible_filter
- helpers: _floor_level -> _org_floor, _grant_levels -> _grants_map,
  _visible -> _hide_no_access

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cement

Resource sharing enforcement (dashboards)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
ddpui/api/user_org_api.py (4)

1340-1341: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard nullable orguser.org before reading logo_url.

OrgUser.org is nullable. Both new had_logo expressions execute before the try block. A user without an organization therefore receives an AttributeError instead of a client error.

Proposed fix
     orguser: OrgUser = request.orguser
+    if orguser.org is None:
+        raise HttpError(400, "create an organization first")
     had_logo = bool(orguser.org.logo_url)

Apply the same guard in upload_logo_from_url.

Also applies to: 1378-1379

🤖 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/user_org_api.py` around lines 1340 - 1341, Guard nullable
orguser.org before accessing logo_url in both had_logo expressions, including
upload_logo_from_url. Ensure users without an organization receive the existing
client-error response rather than an AttributeError, and apply the guard before
either expression executes outside the try block.

872-876: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not assign organization-scoped audit events to .first() membership.

OrgUser.user allows multiple organization memberships. Neither endpoint provides an organization selector. .first() can therefore store a login or password-reset event under the wrong organization and omit it from the other organizations. Emit a user-level event, emit one event per membership, or use an explicitly selected organization.

Also applies to: 1235-1239

🤖 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/user_org_api.py` around lines 872 - 876, Update the audit-log
handling around the user lookup and OrgUser membership resolution in both
affected endpoints so it does not use OrgUser.objects.filter(user=user).first()
to choose an organization. Emit a user-level event, create an event for every
matching OrgUser membership, or require and honor an explicit organization
selection while preserving the existing event details.

1077-1083: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Enforce organization scoping for invitation operations.

  • ddpui/api/user_org_api.py#L1077-L1083: Filter with invited_by__org=orguser.org. Return 404 when no scoped invitation exists.
  • ddpui/api/user_org_api.py#L1047-L1049 and ddpui/core/orguserfunctions.py#L407-L425: Pass the current organization to resend_invitation and include it in the lookup. The current ID-only lookup can resend another organization’s invitation and send its invite URL.
🤖 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/user_org_api.py` around lines 1077 - 1083, Scope invitation
operations to the current organization: update the invitation lookup near the
deletion flow to filter by both ID and invited_by__org=orguser.org, returning
404 when no scoped invitation exists. Update the resend invitation call near the
existing endpoint and the orguserfunctions.py resend_invitation implementation
to accept the current organization and include it in its lookup, preventing
cross-organization invite URLs from being sent.

1125-1137: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Return and use the new organization membership as the audit actor.

ensure_orguser_for_org creates a new OrgUser but returns (None, None). The audit call stores the new organization ID with the requestor’s old membership ID. Return the created or updated OrgUser and pass it to create_audit_log.

🤖 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/user_org_api.py` around lines 1125 - 1137, Update
ensure_orguser_for_org to return the created or updated OrgUser instead of
(None, None), then capture that membership in the organization-creation flow and
pass it as orguser to create_audit_log. Preserve the existing organization
creation behavior while ensuring the audit actor is the new organization
membership.
🧹 Nitpick comments (1)
ddpui/api/user_org_api.py (1)

828-840: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Preserve the original ValueError context.

This handler raises HttpError without chaining the original exception. Add from error to preserve the traceback and satisfy Ruff B904.

Proposed fix
-        raise HttpError(400, str(error))
+        raise HttpError(400, str(error)) from error
🤖 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/user_org_api.py` around lines 828 - 840, Update the ValueError
handler around create_warehouse and create_audit_log to raise HttpError with
explicit exception chaining using the caught error, preserving the original
traceback while keeping the existing 400 status and message.

Source: Linters/SAST tools

🤖 Prompt for all review comments with 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.

Outside diff comments:
In `@ddpui/api/user_org_api.py`:
- Around line 1340-1341: Guard nullable orguser.org before accessing logo_url in
both had_logo expressions, including upload_logo_from_url. Ensure users without
an organization receive the existing client-error response rather than an
AttributeError, and apply the guard before either expression executes outside
the try block.
- Around line 872-876: Update the audit-log handling around the user lookup and
OrgUser membership resolution in both affected endpoints so it does not use
OrgUser.objects.filter(user=user).first() to choose an organization. Emit a
user-level event, create an event for every matching OrgUser membership, or
require and honor an explicit organization selection while preserving the
existing event details.
- Around line 1077-1083: Scope invitation operations to the current
organization: update the invitation lookup near the deletion flow to filter by
both ID and invited_by__org=orguser.org, returning 404 when no scoped invitation
exists. Update the resend invitation call near the existing endpoint and the
orguserfunctions.py resend_invitation implementation to accept the current
organization and include it in its lookup, preventing cross-organization invite
URLs from being sent.
- Around line 1125-1137: Update ensure_orguser_for_org to return the created or
updated OrgUser instead of (None, None), then capture that membership in the
organization-creation flow and pass it as orguser to create_audit_log. Preserve
the existing organization creation behavior while ensuring the audit actor is
the new organization membership.

---

Nitpick comments:
In `@ddpui/api/user_org_api.py`:
- Around line 828-840: Update the ValueError handler around create_warehouse and
create_audit_log to raise HttpError with explicit exception chaining using the
caught error, preserving the original traceback while keeping the existing 400
status and message.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 58fd57e9-5fa9-4803-b47a-ec8f3c7a28d5

📥 Commits

Reviewing files that changed from the base of the PR and between 3e1f864 and b47d539.

📒 Files selected for processing (9)
  • ddpui/api/dashboard_native_api.py
  • ddpui/api/user_org_api.py
  • ddpui/core/alerts/alert_service.py
  • ddpui/core/kpi/kpi_service.py
  • ddpui/core/metric/metric_service.py
  • ddpui/core/orguserfunctions.py
  • ddpui/core/reports/report_service.py
  • ddpui/services/chart_service.py
  • ddpui/services/dashboard_service.py
🚧 Files skipped from review as they are similar to previous changes (8)
  • ddpui/core/reports/report_service.py
  • ddpui/core/alerts/alert_service.py
  • ddpui/core/kpi/kpi_service.py
  • ddpui/services/chart_service.py
  • ddpui/core/metric/metric_service.py
  • ddpui/services/dashboard_service.py
  • ddpui/core/orguserfunctions.py
  • ddpui/api/dashboard_native_api.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with 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.

Inline comments:
In `@ddpui/models/metric.py`:
- Line 120: Register KPI in ResourceType and RTYPES, then update the KPI list,
detail, and data endpoint authorization flows to use access_control with KPI’s
is_private field in addition to can_view_kpis and organization filtering. Ensure
private KPIs are only accessible to authorized users while preserving existing
public KPI behavior.

In `@ddpui/models/resource_share.py`:
- Around line 97-109: Add a conditional UniqueConstraint to the ResourceShare
model’s constraints for org, resource_type, resource_id, and invitation when
invitation is non-null, preventing duplicate invitation-backed grants; add the
matching constraint operation in ddpui/migrations/0175_resource_sharing.py lines
109-116.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 46c29946-b705-449a-a293-ccfaab031bb5

📥 Commits

Reviewing files that changed from the base of the PR and between b47d539 and 4dc23cf.

📒 Files selected for processing (8)
  • ddpui/migrations/0175_resource_sharing.py
  • ddpui/migrations/0176_access_request.py
  • ddpui/models/dashboard.py
  • ddpui/models/metric.py
  • ddpui/models/org_preferences.py
  • ddpui/models/report.py
  • ddpui/models/resource_share.py
  • ddpui/models/visualization.py

Comment thread ddpui/models/metric.py
Comment on lines +97 to +109
constraints = [
# prevent duplicate direct grants for the same principal on the same resource
models.UniqueConstraint(
fields=["org", "resource_type", "resource_id", "principal_type", "principal_id"],
condition=models.Q(parent__isnull=True),
name="uq_resource_share_direct_grant",
),
# prevent duplicate cascade rows for the same principal+resource from the same parent share
models.UniqueConstraint(
fields=["org", "resource_type", "resource_id", "principal_type", "principal_id", "parent"],
condition=models.Q(parent__isnull=False),
name="uq_resource_share_cascade_grant",
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add uniqueness for invitation-backed grants.

add_grants identifies pending grants by (org, resource_type, resource_id, invitation_id). The current constraints only use nullable principal columns. Concurrent pending-email requests can create duplicate ResourceShare rows because NULL values do not collide in a unique constraint.

  • ddpui/models/resource_share.py#L97-L109: add a conditional UniqueConstraint for org, resource_type, resource_id, and invitation when invitation__isnull=False.
  • ddpui/migrations/0175_resource_sharing.py#L109-L116: add the matching migration operation.
🧰 Tools
🪛 Ruff (0.16.1)

[warning] 97-110: Mutable default value for class attribute

(RUF012)

📍 Affects 2 files
  • ddpui/models/resource_share.py#L97-L109 (this comment)
  • ddpui/migrations/0175_resource_sharing.py#L109-L116
🤖 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/models/resource_share.py` around lines 97 - 109, Add a conditional
UniqueConstraint to the ResourceShare model’s constraints for org,
resource_type, resource_id, and invitation when invitation is non-null,
preventing duplicate invitation-backed grants; add the matching constraint
operation in ddpui/migrations/0175_resource_sharing.py lines 109-116.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
ddpui/core/access/resource_share.py (1)

330-348: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Grant mutation endpoints authorize one resource and then mutate a share on any resource. The root cause is in the service layer: update_grant and remove_grant filter ResourceShare by id and org only. The API layer authorizes the caller against the {rtype}/{resource_id} path but never passes those values down, so a caller with Edit access on resource A can change or revoke a grant belonging to resource B in the same org.

  • ddpui/core/access/resource_share.py#L330-L348: add rtype and resource_id parameters to update_grant and include resource_type=rtype, resource_id=str(resource_id) in the lookup filter; apply the same change to remove_grant at lines 351-356.
  • ddpui/api/access_api.py#L93-L103: pass rtype and resource_id into resource_share.update_grant so the share lookup is scoped to the authorized resource.
  • ddpui/api/access_api.py#L110-L118: pass rtype and resource_id into resource_share.remove_grant so the share lookup is scoped to the authorized resource.
🤖 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/core/access/resource_share.py` around lines 330 - 348, Scope grant
mutations to the authorized resource: in ddpui/core/access/resource_share.py
lines 330-348, add rtype and resource_id to update_grant and filter by
resource_type and stringified resource_id; apply the same lookup scoping to
remove_grant at lines 351-356. In ddpui/api/access_api.py lines 93-103 and
110-118, pass rtype and resource_id into update_grant and remove_grant
respectively.
ddpui/api/user_org_api.py (4)

351-363: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not mark deactivated users as active.

The People query includes every OrgUser, but every returned row uses status="active". A user with User.is_active=False is therefore reported as active. Filter by user__is_active=True, or add an explicit inactive status to PeopleRow. (raw.githubusercontent.com)

Proposed fix
-OrgUser.objects.filter(org=org)
+OrgUser.objects.filter(org=org, user__is_active=True)
🤖 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/user_org_api.py` around lines 351 - 363, Update the OrgUser
queryset in the People-row construction to exclude deactivated users by
filtering on user__is_active=True before iterating, so the existing
status="active" assignment remains accurate.

Source: MCP tools


351-363: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the organization-membership timestamp.

PeopleRow.created_at uses curr_orguser.user.date_joined, while pending rows use the invitation timestamp. A user added to another organization will show the platform account creation time instead of the organization membership time. Use curr_orguser.created_at if this field represents when the person joined the organization. (raw.githubusercontent.com)

🤖 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/user_org_api.py` around lines 351 - 363, Update the PeopleRow
construction in the organization-user loop to set created_at from
curr_orguser.created_at, preserving the organization membership timestamp rather
than the platform user's date_joined.

Source: MCP tools


582-607: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Enforce case-insensitive group-name uniqueness at the database boundary.

OrgUserGroup has no database constraint, while both endpoints rely on exists() checks. Concurrent create or rename requests can create duplicate names within one organization. Add a PostgreSQL constraint on org and Lower("name"), add its migration, and map constraint conflicts to HTTP 400 in both endpoints.

🤖 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/user_org_api.py` around lines 582 - 607, Add a PostgreSQL unique
constraint to OrgUserGroup on org and Lower("name"), and create the
corresponding migration. Update create_user_group and the group-rename endpoint
to catch the resulting IntegrityError and return HTTP 400 using the existing
duplicate-name error response, while retaining their current validation checks.

533-561: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Enforce active membership uniqueness at the database boundary.

OrgUserGroupMember constrains only (group, invitation). Active rows use invitation=NULL, so concurrent calls can create duplicate (group, orguser) rows. Add a conditional database UniqueConstraint for (group, orguser) where orguser IS NOT NULL, and use conflict-safe inserts for both membership types.

🤖 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/user_org_api.py` around lines 533 - 561, Add a conditional
UniqueConstraint on OrgUserGroupMember for (group, orguser) when orguser is not
null, alongside the existing invitation constraint. Update _add_members_to_group
to use conflict-safe inserts for both orguser and invitation memberships,
preserving idempotent behavior under concurrent calls.
🧹 Nitpick comments (3)
ddpui/models/resource_share.py (1)

31-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the ambiguous loop variable l.

Ruff reports E741 on both lines. ddpui/core/access/resource_share.py already uses lvl for the same concept. Use lvl here for consistency.

♻️ Proposed rename
 def max_access_level(*levels: Optional[str]) -> Optional[str]:
     """Return the highest AccessLevel from args, skipping None. Returns None if all None."""
-    valid = [l for l in levels if l is not None]
-    return max(valid, key=lambda l: LEVEL_RANK[l]) if valid else None
+    valid = [lvl for lvl in levels if lvl is not None]
+    return max(valid, key=lambda lvl: LEVEL_RANK[lvl]) if valid else None
🤖 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/models/resource_share.py` around lines 31 - 34, Rename the ambiguous
loop variable l to lvl throughout max_access_level, including the list
comprehension and LEVEL_RANK lookup, matching the established naming in
resource-share access logic.

Source: Linters/SAST tools

ddpui/schemas/access/resource_share_schema.py (1)

137-141: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Narrow granted_level so an approval cannot write a deny grant.

AccessLevel includes no_access. respond_to_access_request in ddpui/api/access_api.py passes granted_level straight into add_grants and then sets the status to approved. A responder can therefore approve a request and store a no_access grant, which revokes the floor-based access the requester might otherwise get. AccessRequest.requested_level already restricts the model column to view/edit. Align the payload with that restriction.

♻️ Proposed narrowing
 class RespondToRequestPayload(Schema):
     """Body for ``POST /api/access/{rtype}/{id}/request-access/{req_id}/respond``."""
 
     decision: Literal["approved", "declined"]
-    granted_level: Optional[AccessLevel] = None  # defaults to requested_level when approved
+    # only view/edit may be granted — no_access is a revoke, not an approval
+    granted_level: Optional[Literal["view", "edit"]] = None
🤖 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/schemas/access/resource_share_schema.py` around lines 137 - 141, Update
RespondToRequestPayload.granted_level to accept only the view/edit access
levels, matching AccessRequest.requested_level and preventing approved responses
from creating no_access grants; leave the default-to-requested-level behavior
unchanged.
ddpui/api/user_org_api.py (1)

429-478: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid N+1 queries in group serialization.

_serialize_group_row calls group.members.count() once per group. The detail serializer selects invitation but not invitation__invited_new_role, so pending members can trigger one query each. Annotate member_count with Count("members") and include the invitation role in select_related. (raw.githubusercontent.com)

Proposed query changes
- "invitation"
+ "invitation__invited_new_role"

- member_count=group.members.count(),
+ member_count=group.member_count,

Add .annotate(member_count=Count("members")) to the group-list query.

Also applies to: 564-579

🤖 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/user_org_api.py` around lines 429 - 478, Eliminate per-group and
per-pending-member queries in the group serializers: update the group-list
queryset feeding _serialize_group_row to annotate member_count with
Count("members"), then use that annotation instead of group.members.count(). In
_serialize_group_detail, extend select_related to include
invitation__invited_new_role so pending member role access remains query-free.

Source: MCP tools

🤖 Prompt for all review comments with 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.

Inline comments:
In `@ddpui/api/access_api.py`:
- Around line 190-206: Update the access-request creation flow around
AccessRequest.objects.create to catch IntegrityError from the
uq_access_request_pending_per_user constraint and raise HttpError 409 with the
existing pending-request message. Preserve the preliminary exists() check while
ensuring concurrent duplicate requests return 409 instead of propagating a
server error.

In `@ddpui/api/dashboard_native_api.py`:
- Around line 100-101: Map get_user_access(...) returning None to each
resource’s established 404 not-found response instead of 403 across
ddpui/api/dashboard_native_api.py:100-101, ddpui/api/charts_api.py:1036-1038 and
1067-1068, ddpui/api/kpi_api.py:140-141 and 262-263, and
ddpui/api/report_api.py:150-151, 163-164, and 190-191. Add tests comparing
nonexistent and existing-but-ungranted resource IDs to verify both return the
same 404 response.

In `@ddpui/api/kpi_api.py`:
- Around line 140-142: Update get_kpi to store the result of get_user_access in
access, continue rejecting None access, and pass access as access_level to
KPIService.kpi_to_response so resolved permissions are preserved in the
response.

In `@ddpui/api/user_org_api.py`:
- Around line 669-674: Wrap the ResourceShare cleanup and group deletion in the
endpoint’s existing database transaction mechanism so both operations in the
deletion flow succeed or roll back together. Update the block containing
ResourceShare.objects.filter(...).delete() and group.delete(), preserving their
order and ensuring any failure in group.delete() restores the removed shares.

In `@ddpui/core/access/resource_share.py`:
- Around line 54-108: Refactor sync_dashboard_cascade to batch-create chart and
KPI child shares with bulk_create(ignore_conflicts=True) instead of
per-component get_or_create calls, and combine stale chart/KPI deletion
predicates with Q. Wrap each source write and its subsequent
sync_dashboard_cascade call in one transaction.atomic() block in
update_dashboard, add_grants, and update_grant; do not rely on a transaction
only inside the sync function.

In `@ddpui/core/kpi/kpi_service.py`:
- Around line 188-195: Update get_kpi_summary to apply
accessible_filter(orguser, ResourceType.KPI) when building its KPI query,
ensuring both KPI values and RAG status are restricted to resources accessible
by the requesting orguser rather than organization membership alone.

In `@ddpui/services/chart_service.py`:
- Around line 92-102: Update every service-layer list call to pass the existing
orguser argument, including test and equivalent callers of KPIService.list_kpis
and ReportService.list_snapshots that currently provide only org. Preserve the
service method signatures and ensure all callers satisfy the required
authorization context.

---

Outside diff comments:
In `@ddpui/api/user_org_api.py`:
- Around line 351-363: Update the OrgUser queryset in the People-row
construction to exclude deactivated users by filtering on user__is_active=True
before iterating, so the existing status="active" assignment remains accurate.
- Around line 351-363: Update the PeopleRow construction in the
organization-user loop to set created_at from curr_orguser.created_at,
preserving the organization membership timestamp rather than the platform user's
date_joined.
- Around line 582-607: Add a PostgreSQL unique constraint to OrgUserGroup on org
and Lower("name"), and create the corresponding migration. Update
create_user_group and the group-rename endpoint to catch the resulting
IntegrityError and return HTTP 400 using the existing duplicate-name error
response, while retaining their current validation checks.
- Around line 533-561: Add a conditional UniqueConstraint on OrgUserGroupMember
for (group, orguser) when orguser is not null, alongside the existing invitation
constraint. Update _add_members_to_group to use conflict-safe inserts for both
orguser and invitation memberships, preserving idempotent behavior under
concurrent calls.

In `@ddpui/core/access/resource_share.py`:
- Around line 330-348: Scope grant mutations to the authorized resource: in
ddpui/core/access/resource_share.py lines 330-348, add rtype and resource_id to
update_grant and filter by resource_type and stringified resource_id; apply the
same lookup scoping to remove_grant at lines 351-356. In ddpui/api/access_api.py
lines 93-103 and 110-118, pass rtype and resource_id into update_grant and
remove_grant respectively.

---

Nitpick comments:
In `@ddpui/api/user_org_api.py`:
- Around line 429-478: Eliminate per-group and per-pending-member queries in the
group serializers: update the group-list queryset feeding _serialize_group_row
to annotate member_count with Count("members"), then use that annotation instead
of group.members.count(). In _serialize_group_detail, extend select_related to
include invitation__invited_new_role so pending member role access remains
query-free.

In `@ddpui/models/resource_share.py`:
- Around line 31-34: Rename the ambiguous loop variable l to lvl throughout
max_access_level, including the list comprehension and LEVEL_RANK lookup,
matching the established naming in resource-share access logic.

In `@ddpui/schemas/access/resource_share_schema.py`:
- Around line 137-141: Update RespondToRequestPayload.granted_level to accept
only the view/edit access levels, matching AccessRequest.requested_level and
preventing approved responses from creating no_access grants; leave the
default-to-requested-level behavior unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dbdc0830-68a2-4636-9fb4-c02653e01f20

📥 Commits

Reviewing files that changed from the base of the PR and between 4dc23cf and e4c730a.

📒 Files selected for processing (19)
  • ddpui/api/access_api.py
  • ddpui/api/charts_api.py
  • ddpui/api/dashboard_native_api.py
  • ddpui/api/kpi_api.py
  • ddpui/api/org_preferences_api.py
  • ddpui/api/report_api.py
  • ddpui/api/user_org_api.py
  • ddpui/core/access/access_control.py
  • ddpui/core/access/ownership.py
  • ddpui/core/access/resource_share.py
  • ddpui/core/access/shareable_types.py
  • ddpui/core/kpi/kpi_service.py
  • ddpui/core/reports/report_service.py
  • ddpui/models/resource_share.py
  • ddpui/schemas/access/resource_share_schema.py
  • ddpui/schemas/chart_schemas/crud.py
  • ddpui/schemas/kpi_schema.py
  • ddpui/schemas/report_schema.py
  • ddpui/services/chart_service.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • ddpui/core/access/access_control.py
  • ddpui/api/org_preferences_api.py

Comment thread ddpui/api/access_api.py
Comment on lines +190 to +206
if AccessRequest.objects.filter(
org=orguser.org,
resource_type=rtype,
resource_id=str(resource_id),
requester=orguser,
status=AccessRequestStatus.PENDING,
).exists():
raise HttpError(409, "a pending request already exists for this resource")

req = AccessRequest.objects.create(
org=orguser.org,
resource_type=rtype,
resource_id=str(resource_id),
requester=orguser,
requested_level=payload.requested_level,
note=payload.note,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle the unique-constraint violation on concurrent access requests.

The exists() check and the create() are separate statements. AccessRequest now carries the conditional constraint uq_access_request_pending_per_user. Two concurrent requests from the same requester can both pass the check, and the second create() then raises IntegrityError, which surfaces as HTTP 500 instead of the intended 409.

🛡️ Proposed fix
+from django.db import IntegrityError
+
@@
-    req = AccessRequest.objects.create(
-        org=orguser.org,
-        resource_type=rtype,
-        resource_id=str(resource_id),
-        requester=orguser,
-        requested_level=payload.requested_level,
-        note=payload.note,
-    )
+    try:
+        req = AccessRequest.objects.create(
+            org=orguser.org,
+            resource_type=rtype,
+            resource_id=str(resource_id),
+            requester=orguser,
+            requested_level=payload.requested_level,
+            note=payload.note,
+        )
+    except IntegrityError as err:
+        raise HttpError(409, "a pending request already exists for this resource") from err
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if AccessRequest.objects.filter(
org=orguser.org,
resource_type=rtype,
resource_id=str(resource_id),
requester=orguser,
status=AccessRequestStatus.PENDING,
).exists():
raise HttpError(409, "a pending request already exists for this resource")
req = AccessRequest.objects.create(
org=orguser.org,
resource_type=rtype,
resource_id=str(resource_id),
requester=orguser,
requested_level=payload.requested_level,
note=payload.note,
)
if AccessRequest.objects.filter(
org=orguser.org,
resource_type=rtype,
resource_id=str(resource_id),
requester=orguser,
status=AccessRequestStatus.PENDING,
).exists():
raise HttpError(409, "a pending request already exists for this resource")
try:
req = AccessRequest.objects.create(
org=orguser.org,
resource_type=rtype,
resource_id=str(resource_id),
requester=orguser,
requested_level=payload.requested_level,
note=payload.note,
)
except IntegrityError as err:
raise HttpError(409, "a pending request already exists for this resource") from err
🤖 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/access_api.py` around lines 190 - 206, Update the access-request
creation flow around AccessRequest.objects.create to catch IntegrityError from
the uq_access_request_pending_per_user constraint and raise HttpError 409 with
the existing pending-request message. Preserve the preliminary exists() check
while ensuring concurrent duplicate requests return 409 instead of propagating a
server error.

Comment thread ddpui/api/dashboard_native_api.py Outdated
Comment thread ddpui/api/kpi_api.py Outdated
Comment thread ddpui/api/user_org_api.py
Comment on lines +669 to +674
ResourceShare.objects.filter(
org=group.org,
principal_type=ResourceSharePrincipalType.GROUP,
principal_id=group.id,
).delete()
group.delete()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make resource-share cleanup and group deletion atomic.

The endpoint deletes ResourceShare rows before group.delete(). If group deletion fails, the group remains but its grants are already gone. Wrap both operations in one transaction.

Proposed fix
-    ResourceShare.objects.filter(
-        org=group.org,
-        principal_type=ResourceSharePrincipalType.GROUP,
-        principal_id=group.id,
-    ).delete()
-    group.delete()
+    with transaction.atomic():
+        ResourceShare.objects.filter(
+            org=group.org,
+            principal_type=ResourceSharePrincipalType.GROUP,
+            principal_id=group.id,
+        ).delete()
+        group.delete()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ResourceShare.objects.filter(
org=group.org,
principal_type=ResourceSharePrincipalType.GROUP,
principal_id=group.id,
).delete()
group.delete()
with transaction.atomic():
ResourceShare.objects.filter(
org=group.org,
principal_type=ResourceSharePrincipalType.GROUP,
principal_id=group.id,
).delete()
group.delete()
🤖 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/user_org_api.py` around lines 669 - 674, Wrap the ResourceShare
cleanup and group deletion in the endpoint’s existing database transaction
mechanism so both operations in the deletion flow succeed or roll back together.
Update the block containing ResourceShare.objects.filter(...).delete() and
group.delete(), preserving their order and ensuring any failure in
group.delete() restores the removed shares.

Comment thread ddpui/core/access/resource_share.py Outdated
Comment on lines +54 to +108
def sync_dashboard_cascade(dashboard: Dashboard) -> None:
"""Full sync of cascade child rows for all shares on a dashboard.

After this call, cascade children exactly match the dashboard's current
tabs — correct level, correct set of charts/KPIs, stale rows removed.
Call after any share change (add/update) or any tabs change.
Invitation-backed shares are skipped — promoted on acceptance.
"""
inner = _parse_inner_ids(dashboard.tabs)
current_chart_ids = [str(cid) for cid in inner["chart_ids"]]
current_kpi_ids = [str(kid) for kid in inner["kpi_ids"]]

dashboard_shares = list(
ResourceShare.objects.filter(
org=dashboard.org,
resource_type=ResourceType.DASHBOARD,
resource_id=str(dashboard.id),
parent__isnull=True,
principal_type__isnull=False,
)
)
if not dashboard_shares:
return

for share in dashboard_shares:
ResourceShare.objects.filter(parent=share).update(access_level=share.access_level)

for chart_id in current_chart_ids:
ResourceShare.objects.get_or_create(
org=dashboard.org,
resource_type=ResourceType.CHART,
resource_id=chart_id,
principal_type=share.principal_type,
principal_id=share.principal_id,
parent=share,
defaults={"access_level": share.access_level, "created_by": share.created_by},
)
for kpi_id in current_kpi_ids:
ResourceShare.objects.get_or_create(
org=dashboard.org,
resource_type=ResourceType.KPI,
resource_id=kpi_id,
principal_type=share.principal_type,
principal_id=share.principal_id,
parent=share,
defaults={"access_level": share.access_level, "created_by": share.created_by},
)

ResourceShare.objects.filter(
parent=share, resource_type=ResourceType.CHART
).exclude(resource_id__in=current_chart_ids).delete()

ResourceShare.objects.filter(
parent=share, resource_type=ResourceType.KPI
).exclude(resource_id__in=current_kpi_ids).delete()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate files ---'
git ls-files | grep -E '(^|/)(resource_share\.py|dashboard_native_api\.py|.*models.*\.py)$' | head -200

printf '%s\n' '--- resource_share.py outline and relevant references ---'
ast-grep outline ddpui/core/access/resource_share.py --lang python || true
rg -n -C 5 'sync_dashboard_cascade|uq_resource_share_cascade_grant|class ResourceShare|UniqueConstraint|bulk_create|get_or_create|transaction' ddpui/core/access ddpui/api ddpui -g '*.py' | head -400

printf '%s\n' '--- dashboard update call sites ---'
rg -n -C 8 'sync_dashboard_cascade|payload\.tabs|tabs is not None' ddpui/api ddpui -g '*.py' | head -300

Repository: DalgoT4D/DDP_backend

Length of output: 41229


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- resource share implementation ---'
sed -n '1,125p' ddpui/core/access/resource_share.py
printf '%s\n' '--- ResourceShare model ---'
sed -n '1,260p' ddpui/models/resource_share.py
printf '%s\n' '--- migrations containing cascade constraint ---'
rg -n -C 12 'uq_resource_share_cascade_grant|cascade_grant|UniqueConstraint|unique' ddpui/migrations ddpui/models/resource_share.py -g '*.py' | head -300
printf '%s\n' '--- dashboard update flow ---'
sed -n '135,215p' ddpui/api/dashboard_native_api.py
sed -n '340,410p' ddpui/services/dashboard_service.py

Repository: DalgoT4D/DDP_backend

Length of output: 40484


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- migration inventory and constraint history ---'
git ls-files ddpui/migrations | tail -30
rg -n 'ResourceShare|resource_share|uq_resource_share' ddpui/migrations -g '*.py' | tail -200

printf '%s\n' '--- grant write flows ---'
sed -n '220,355p' ddpui/core/access/resource_share.py
printf '%s\n' '--- repository diff summary ---'
git diff --stat
git status --short

printf '%s\n' '--- behavioral verifier: ORM operation count and constraint fields ---'
python3 - <<'PY'
import ast
from pathlib import Path

path = Path("ddpui/core/access/resource_share.py")
tree = ast.parse(path.read_text())
fn = next(n for n in tree.body if isinstance(n, ast.FunctionDef)
          and n.name == "sync_dashboard_cascade")

calls = []
for node in ast.walk(fn):
    if isinstance(node, ast.Call):
        text = ast.unparse(node.func)
        if text.endswith(("filter", "update", "get_or_create", "delete")):
            calls.append(text)
print("ORM call sites:", calls)
print("per-share get_or_create call sites:",
      sum(x.endswith("get_or_create") for x in calls))
print("per-share update call sites:",
      sum(x.endswith("update") for x in calls))
print("per-share delete call sites:",
      sum(x.endswith("delete") for x in calls))

model = ast.parse(Path("ddpui/models/resource_share.py").read_text())
for node in ast.walk(model):
    if isinstance(node, ast.ClassDef) and node.name == "ResourceShare":
        for child in ast.walk(node):
            if isinstance(child, ast.Call) and ast.unparse(child.func) == "models.UniqueConstraint":
                keywords = {kw.arg: ast.literal_eval(kw.value) for kw in child.keywords
                            if kw.arg in {"fields", "name"}}
                print("constraint:", keywords)
PY

Repository: DalgoT4D/DDP_backend

Length of output: 8491


Batch cascade synchronization and make the source update atomic.

sync_dashboard_cascade performs one ORM lookup per component, plus per-share updates and deletes. Use bulk_create(..., ignore_conflicts=True) for missing child rows and combine stale-row deletes with Q.

Wrap the source write and cascade sync in the same transaction.atomic() block. update_dashboard, add_grants, and update_grant save the source row before calling sync_dashboard_cascade; a transaction inside sync_dashboard_cascade alone cannot prevent stale children if the sync fails. The uq_resource_share_cascade_grant constraint already covers the required fields.

🤖 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/core/access/resource_share.py` around lines 54 - 108, Refactor
sync_dashboard_cascade to batch-create chart and KPI child shares with
bulk_create(ignore_conflicts=True) instead of per-component get_or_create calls,
and combine stale chart/KPI deletion predicates with Q. Wrap each source write
and its subsequent sync_dashboard_cascade call in one transaction.atomic() block
in update_dashboard, add_grants, and update_grant; do not rely on a transaction
only inside the sync function.

Comment thread ddpui/core/kpi/kpi_service.py
Comment thread ddpui/services/chart_service.py
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.59322% with 238 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.70%. Comparing base (9025ac6) to head (cd3f696).

Files with missing lines Patch % Lines
ddpui/api/user_org_api.py 54.82% 89 Missing ⚠️
ddpui/api/public_api.py 32.78% 41 Missing ⚠️
ddpui/core/access/resource_share.py 86.14% 23 Missing ⚠️
ddpui/api/access_api.py 93.15% 15 Missing ⚠️
ddpui/core/notifications/triggers/share.py 84.44% 14 Missing ⚠️
ddpui/core/notifications/triggers/alert.py 73.91% 12 Missing ⚠️
ddpui/api/report_api.py 66.66% 10 Missing ⚠️
ddpui/core/access/access_control.py 93.26% 7 Missing ⚠️
ddpui/core/orguserfunctions.py 82.75% 5 Missing ⚠️
ddpui/auth.py 86.66% 4 Missing ⚠️
... and 10 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1436      +/-   ##
==========================================
+ Coverage   65.75%   66.70%   +0.95%     
==========================================
  Files         170      191      +21     
  Lines       19661    20751    +1090     
==========================================
+ Hits        12928    13842     +914     
- Misses       6733     6909     +176     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@siddhant3030 siddhant3030 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Three structural issues — all variants of the same "write it once" principle from the repo's own rules (layer architecture, empty init.py, service-delegation skill). Details inline.

return levels


def get_access_map_for_resource(org, rtype: str, resource_id) -> dict[int, str]:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Why can’t this decision logic be a single function? The same rule—owner/admin → edit; private → explicit grant only; otherwise max(grant, floor)—is duplicated three times in this file (get_user_access:93, get_user_access_map:121), and the no_access handling already differs.

Something like effective_level(...) would give us one place to read, change, and unit-test the rule, preventing the implementations from drifting.

def effective_level(is_owner_or_admin, grant, floor, is_private):
    if is_owner_or_admin:
        return AccessLevel.EDIT
    if is_private:
        return grant or AccessLevel.NO_ACCESS
    return max_access_level(grant, floor) or AccessLevel.NO_ACCESS

4. org-default floor -> default_analyst_level / default_member_level

``no_access`` at step 2 is the explicit deny — the only way to revoke one
principal below a permissive org floor. This is precedence, not max-merge,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This docstring promise isn't what the code does: _grants_map ends with a max-merge of user and group levels, so an explicit USER no_access loses to any group grant (and to a permissive floor in get_user_access). Either make the code precedence-based as documented, or fix the docstring — right now the most-read lines of the file describe an algorithm the file doesn't implement.

return prefs.default_member_level


def _grants_map(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

nit: this map keys by str(resource_id), so every caller writes grants.get(str(...)) and accessible_filter converts back with int(rid). Normalizing to one key type here would remove the juggling at all call sites.

``test_trial_shell_is_single_source_of_truth`` enforce the invariant.
"""

from ddpui.core.notifications.templates.alert import render_alert_email

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Repo invariant: core feature __init__.py stays empty (no re-exports, no __all__). This 58-line hub adds no behavior, puts an extra hop in every "where does this live" search, and re-exports two underscore-private names (_render_email_shell) as if they were public API. Suggest emptying it and importing from the concrete modules.

Comment on lines +228 to +229
allowed_ids = [int(rid) for rid, level in grants.items() if level != AccessLevel.NO_ACCESS]
denied_ids = [int(rid) for rid, level in grants.items() if level == AccessLevel.NO_ACCESS]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

resource_id is a CharField with no numeric constraint, so one malformed row ("", "12 ", a future writer bug) makes int(rid) raise — and since this filter runs in every list endpoint, that one row 500s every list page for that user (or a whole group, if it's a group grant). Q(id__in=...) accepts strings directly, so the cast can just be dropped — or skip-and-log non-numeric rows.

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