Skip to content

Conversation

@Roffenlund
Copy link
Contributor

@Roffenlund Roffenlund commented Aug 22, 2025

WIP - refactor ensure_x functions in the Team model

TODO:

  • Code/logic cleanup
  • Manual testing

Summary by CodeRabbit

  • Bug Fixes

    • More consistent, user-friendly permission error messages across team management, member edits/removals, service accounts, team disband/edit, package uploads, and package deprecation/wiki actions.
    • Clearer feedback for unauthenticated, deactivated, and service-account scenarios, with appropriate public/non-public visibility.
  • Refactor

    • Unified permission validation to return detailed results instead of generic exceptions, improving error clarity without changing allowed actions.
  • Tests

    • Updated and expanded coverage to reflect the new validation flow, including comprehensive service-account scenarios.

Remove and replace ensure_can_create_service_account in the Team model
with validate_can_create_service_account. The reasoning behind this
change is to remove the reliance one raising errors as control flow and
pass the responsibility of raising errors to the views.

Update can_user_create_service_accounts and update tests accordingly to
the changes made.

Update locations in the code which were dependent on
ensure_can_create_service_account.

Refs. TS-2590
Rewrite the ensure_user_can_manage_packages function and utilize this in
functions in the Package model which is dependent in the function.

Refs. TS-2590
@coderabbitai
Copy link

coderabbitai bot commented Aug 22, 2025

Walkthrough

Refactors permission checks from exception-based ensure_* methods to validate_* methods returning (errors, is_public). Updates forms, services, models, and uploads to consume validators and raise PermissionValidationError with context. Adds check_user_permissions utility. Adjusts team update/disband flows and tests to the new tuple-based validation API.

Changes

Cohort / File(s) Summary
Validator-based Team permission API
django/thunderstore/repository/models/team.py
Replaces ensure_* methods with validate_* counterparts returning Tuple[List[str], bool]; centralizes user checks via check_user_permissions; adapts can_user_* to boolean from validator results.
Forms using validators
django/thunderstore/account/forms.py, django/thunderstore/repository/forms/team.py, django/thunderstore/repository/package_upload.py
Swap ensure_* calls for validate_* returning (errors, is_public); raise PermissionValidationError or forms.ValidationError accordingly; add explicit team presence checks where needed; validate on clean and save paths.
Team services (Cyberstorm)
django/thunderstore/api/cyberstorm/services/team.py
Switch disband_team and update_team to validator chains; aggregate errors and raise PermissionValidationError with is_public; update flow to proceed only after validations; update_team now returns the saved team.
Package permission delegation
django/thunderstore/repository/models/package.py
Use owner.validate_user_can_manage_packages; raise PermissionValidationError with collected errors/is_public in manage_deprecation/manage_wiki checks.
Permission utilities
django/thunderstore/permissions/utils.py
Add check_user_permissions(user, allow_serviceaccount) -> Tuple[List[str], bool]; expand typing imports; keep existing validate_user behavior unchanged.
Tests migration to validators
django/thunderstore/repository/tests/test_team.py
Update tests from exception expectations to (errors, is_public) assertions; add service-account validation coverage; rename tests to validate_* pattern; adjust scenarios for new error signaling.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User/Agent
  participant F as Form/Service
  participant T as Team/Owner Model
  participant E as PermissionValidationError

  U->>F: Trigger action (e.g., submit, API call)
  F->>T: validate_* (user)
  alt Errors found
    T-->>F: (errors, is_public=false|true)
    F->>E: Raise PermissionValidationError(errors, is_public)
    E-->>U: Error surfaced (public/private signaled)
  else No errors
    T-->>F: ([], is_public=true)
    F->>T: Perform action (save/update/disband/upload)
    T-->>F: Success
    F-->>U: OK
  end
Loading
sequenceDiagram
  autonumber
  actor A as Agent
  participant S as TeamService
  participant T as Team
  participant E as PermissionValidationError

  A->>S: update_team(agent, data)
  S->>T: validate_user_can_access(agent)
  T-->>S: (errors1, is_public1)
  S->>T: validate_user_can_edit_info(agent)
  T-->>S: (errors2, is_public2)
  alt Any errors
    S->>E: Raise with combined errors, is_public = any(true|false)
    E-->>A: Error
  else Valid
    S->>T: Apply changes (donation_link, etc.)
    T-->>S: Saved
    S-->>A: Return updated team
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I hopped through code with careful cheer,
Swapped throws for lists of faults to hear.
A tuple’s tale, both truth and light,
is_public flags the public sight.
Teams now validate their stance—
Permissions dance, advance, enhance.
Thump-thump! Approved by rabbit’s prance. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-team-esnure-functions

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@Roffenlund Roffenlund marked this pull request as draft August 22, 2025 13:14
@codecov
Copy link

codecov bot commented Aug 22, 2025

Codecov Report

❌ Patch coverage is 95.32164% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.25%. Comparing base (8e892ab) to head (21db4b9).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
django/thunderstore/repository/forms/team.py 84.00% 3 Missing and 1 partial ⚠️
django/thunderstore/repository/package_upload.py 42.85% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1174      +/-   ##
==========================================
+ Coverage   92.23%   92.25%   +0.02%     
==========================================
  Files         331      331              
  Lines       10087    10179      +92     
  Branches      927      947      +20     
==========================================
+ Hits         9304     9391      +87     
- Misses        657      661       +4     
- Partials      126      127       +1     

☔ View full report in Codecov by Sentry.
📢 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (1)
django/thunderstore/repository/tests/test_team.py (1)

389-438: Correct malformed assert that mistakenly compares a tuple.

assert errors, is_public == [...] evaluates as a tuple assertion and is not doing what’s intended. Remove it and rely on the subsequent per-branch assertions (which you already have), or split into two asserts as appropriate.

-            assert errors, is_public == ["Must be a member of team to upload package"]
+            # Asserts follow in the specific branches below; no combined tuple assert here.

Alternatively, if you prefer asserting immediately for non-deactivated users, restructure as:

-            errors, is_public = team.validate_can_upload_package(user)
-            # ...
-            if user_type == TestUserTypes.deactivated_user:
+            errors, is_public = team.validate_can_upload_package(user)
+            if user_type == TestUserTypes.deactivated_user:
                 assert is_public is False
                 assert errors == ["User has been deactivated"]
             else:
                 assert is_public is True
                 assert errors == ["Must be a member of team to upload package"]
🧹 Nitpick comments (7)
django/thunderstore/api/cyberstorm/services/team.py (1)

15-21: Validator pattern adoption looks good; consider unifying the loop into a helper

The switch to validator functions returning (errors, is_public) is clear and maintains atomicity on failure.

To DRY this across services, consider a small helper:

def validate_or_raise(validators, actor):
    for v in validators:
        errors, is_public = v(actor)
        if errors:
            raise PermissionValidationError(errors, is_public=is_public)

Usage:

validators = [team.validate_user_can_access, team.validate_user_can_disband]
validate_or_raise(validators, user)
django/thunderstore/repository/package_upload.py (1)

210-213: Avoid duplicate permission checks in save; centralize or trust form cleaning

clean_team already validates upload permission. Re-checking in save duplicates DB work and validator calls.

Option A (preferred): centralize in a helper and call it from both places if you want belt-and-suspenders:

def _ensure_upload_permission(self, team: Team) -> None:
    errors, is_public = team.validate_can_upload_package(self.user)
    if errors:
        raise PermissionValidationError(errors, is_public=is_public)

Then:

- errors, is_public = team.validate_can_upload_package(self.user)
- if errors:
-     raise PermissionValidationError(errors, is_public=is_public)
+ self._ensure_upload_permission(team)

Option B: trust the form lifecycle (i.e., is_valid() precedes save()) and remove the save()-side check entirely to reduce duplicated work.

django/thunderstore/repository/tests/test_team.py (1)

313-350: Rename test to match new API naming (ensure → validate).

The test name still uses "ensure" while the implementation and rest of the suite have migrated to "validate". This avoids confusion when grepping and aligns with the new contract.

-def test_team_ensure_user_can_manage_members(
+def test_team_validate_user_can_manage_members(
     team: Team, user_type: str, role: str
 ) -> None:
django/thunderstore/repository/forms/team.py (1)

37-41: Minor: redundant lower() with __iexact.

__iexact already normalizes case; removing .lower() avoids unnecessary transformations and keeps comparisons symmetric.

-        if Team.objects.filter(name__iexact=name.lower()).exists():
+        if Team.objects.filter(name__iexact=name).exists():
             raise ValidationError(f"A team with the provided name already exists")
-        if Namespace.objects.filter(name__iexact=name.lower()).exists():
+        if Namespace.objects.filter(name__iexact=name).exists():
             raise ValidationError("A namespace with the provided name already exists")
django/thunderstore/repository/models/team.py (3)

14-14: Remove unused import.

validate_user is not used in this module; drop it to satisfy linters (F401) and keep imports tidy.

-from thunderstore.permissions.utils import check_user_permissions, validate_user
+from thunderstore.permissions.utils import check_user_permissions

370-380: Combine nested condition per SIM102.

Slight simplification without behavior change.

-        if new_role != TeamMemberRole.owner:
-            if self.is_last_owner(member):
-                return ["Cannot remove last owner from team"], public_error
+        if new_role != TeamMemberRole.owner and self.is_last_owner(member):
+            return ["Cannot remove last owner from team"], public_error

382-394: Type hint mismatch and variable naming consistency in validate_user_can_disband.

The return type should be Tuple[List[str], bool] (you return lists elsewhere). Also rename local error to errors to match the rest of the file for readability.

-    def validate_user_can_disband(self, user: Optional[UserType]) -> Tuple[str, bool]:
-        error, public_error = check_user_permissions(user)
-        if error:
-            return error, public_error
+    def validate_user_can_disband(self, user: Optional[UserType]) -> Tuple[List[str], bool]:
+        errors, public_error = check_user_permissions(user)
+        if errors:
+            return errors, public_error
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f06b9b4 and 21db4b9.

📒 Files selected for processing (8)
  • django/thunderstore/account/forms.py (4 hunks)
  • django/thunderstore/api/cyberstorm/services/team.py (2 hunks)
  • django/thunderstore/permissions/utils.py (2 hunks)
  • django/thunderstore/repository/forms/team.py (4 hunks)
  • django/thunderstore/repository/models/package.py (2 hunks)
  • django/thunderstore/repository/models/team.py (3 hunks)
  • django/thunderstore/repository/package_upload.py (3 hunks)
  • django/thunderstore/repository/tests/test_team.py (13 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
django/thunderstore/repository/package_upload.py (2)
django/thunderstore/core/exceptions.py (1)
  • PermissionValidationError (4-7)
django/thunderstore/repository/models/team.py (1)
  • validate_can_upload_package (321-336)
django/thunderstore/account/forms.py (2)
django/thunderstore/core/exceptions.py (1)
  • PermissionValidationError (4-7)
django/thunderstore/repository/models/team.py (3)
  • validate_can_create_service_account (250-263)
  • validate_can_delete_service_account (280-293)
  • validate_can_edit_service_account (265-278)
django/thunderstore/api/cyberstorm/services/team.py (2)
django/thunderstore/repository/models/team.py (3)
  • validate_user_can_access (308-319)
  • validate_user_can_disband (382-393)
  • validate_user_can_edit_info (395-406)
django/thunderstore/core/exceptions.py (1)
  • PermissionValidationError (4-7)
django/thunderstore/permissions/utils.py (2)
django/conftest.py (1)
  • user (115-120)
django/thunderstore/core/types.py (1)
  • UserType (13-16)
django/thunderstore/repository/tests/test_team.py (2)
django/conftest.py (7)
  • team (135-136)
  • user (115-120)
  • TestUserTypes (550-585)
  • get_user_by_type (564-585)
  • fake_users (560-561)
  • service_account (406-417)
  • package (186-187)
django/thunderstore/repository/models/team.py (15)
  • validate_can_manage_members (295-306)
  • TeamMemberRole (19-21)
  • Team (92-448)
  • validate_user_can_access (308-319)
  • validate_can_upload_package (321-336)
  • validate_member_can_be_removed (351-363)
  • validate_member_role_can_be_changed (365-380)
  • validate_user_can_disband (382-393)
  • validate_can_create_service_account (250-263)
  • TeamMember (42-79)
  • create (189-196)
  • validate_can_edit_service_account (265-278)
  • validate_can_delete_service_account (280-293)
  • validate_user_can_edit_info (395-406)
  • validate_user_can_manage_packages (338-349)
django/thunderstore/repository/models/package.py (3)
django/thunderstore/core/exceptions.py (1)
  • PermissionValidationError (4-7)
django/thunderstore/repository/models/team.py (1)
  • validate_user_can_manage_packages (338-349)
django/thunderstore/core/utils.py (1)
  • check_validity (55-60)
django/thunderstore/repository/forms/team.py (2)
django/thunderstore/repository/models/team.py (3)
  • validate_can_manage_members (295-306)
  • validate_member_can_be_removed (351-363)
  • validate_user_can_access (308-319)
django/thunderstore/core/exceptions.py (1)
  • PermissionValidationError (4-7)
django/thunderstore/repository/models/team.py (1)
django/thunderstore/permissions/utils.py (2)
  • check_user_permissions (21-35)
  • validate_user (7-18)
🪛 Ruff (0.12.2)
django/thunderstore/repository/tests/test_team.py

349-349: Pointless comparison. Did you mean to assign a value? Otherwise, prepend assert or remove it.

(B015)

django/thunderstore/repository/models/team.py

14-14: thunderstore.permissions.utils.validate_user imported but unused

Remove unused import: thunderstore.permissions.utils.validate_user

(F401)


376-377: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)

🪛 Flake8 (7.2.0)
django/thunderstore/repository/models/team.py

[error] 14-14: 'thunderstore.permissions.utils.validate_user' imported but unused

(F401)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Test pytest (6)
  • GitHub Check: Test pytest (3)
  • GitHub Check: Test pytest (1)
  • GitHub Check: Test pytest (4)
  • GitHub Check: Test pytest (5)
  • GitHub Check: Test pytest (2)
  • GitHub Check: Test missing migrations
  • GitHub Check: Test mypy
  • GitHub Check: Build docker image
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (30)
django/thunderstore/api/cyberstorm/services/team.py (1)

43-51: LGTM on update_team validators and explicit return

Consistent use of (errors, is_public) and returning the updated team is clear and side-effect free outside the transaction block.

django/thunderstore/repository/package_upload.py (2)

11-11: Import of PermissionValidationError is appropriate for permission-scope errors

Aligns with the new validator-based API and preserves is_public propagation.


114-116: LGTM: Form-level permission validation with is_public context

Raising PermissionValidationError(errors, is_public=is_public) from clean_team aligns with the new validation contract and ensures consistent error handling in views/forms.

django/thunderstore/repository/models/package.py (3)

19-19: Importing PermissionValidationError is correct for structured permission failures

Keeps is_public attached to raised validation errors, interoperating with callers that inspect visibility.


339-342: LGTM: Switch to validator + PermissionValidationError for deprecation management

Clear delegation to Team.validate_user_can_manage_packages and consistent error raising semantics.


347-349: LGTM: Consistent permission validation for wiki management

Same validator flow and error structure as above; readable and maintainable.

django/thunderstore/account/forms.py (3)

5-5: Import of PermissionValidationError is fine

Used in subsequent validators to carry is_public through form errors.


48-52: LGTM: Delete permission uses validator + PermissionValidationError

Consistent with the new pattern; is_public is propagated.


71-75: LGTM: Edit permission uses validator + PermissionValidationError

Matches delete flow and encapsulates permission visibility.

django/thunderstore/repository/tests/test_team.py (17)

319-326: LGTM: unauthenticated/deactivated branches assert error payload and visibility.

The assertions correctly verify both error content and the public/private visibility flag for non-authenticated and deactivated users.


337-340: LGTM: service account branch consistent with permissions utility.

Error and is_public values align with check_user_permissions(allow_serviceaccount=False).


355-383: LGTM: validate_user_can_access tests are thorough.

Covers fake, deactivated, member, and non-member paths with correct is_public expectations.


443-456: LGTM: happy path for member removal.

Correctly ensures not-last-owner scenario passes with empty errors and public visibility.


459-467: LGTM: wrong-team member removal returns public error.

Matches model’s validator contract.


470-477: LGTM: no-member removal yields public “Invalid member”.

Covers the None path explicitly.


480-492: LGTM: last-owner removal blocked with public error.

Asserts align with validator semantics.


496-504: LGTM: role change on wrong-team member returns public error.

Consistent with validator behavior.


508-515: LGTM: role change with no member returns public error.

Correct edge-case coverage.


519-527: LGTM: invalid role inputs correctly rejected.

Validates both None and invalid string.


530-539: LGTM: last owner demotion is rejected.

Matches business rule.


544-557: LGTM: role change matrix covers last-owner guard.

Good comprehensive coverage with dynamic setup.


562-596: LGTM: disband validator scenarios are well covered.

Asserts for fake, deactivated, service account, non-owner, and owner pass cases are correct.


599-607: LGTM: disband blocked when team has packages.

Correct message and visibility.


651-685: LGTM: create service account validator.

Error mapping per user type and role is accurate; success path validated.


690-716: LGTM: edit service account validator.

Table-driven expected_errors keeps the tests concise and robust.


721-753: LGTM: delete service account validator + can_user mirror assertions.

Nice addition asserting the can_user_* method coherence with validator output.

django/thunderstore/repository/forms/team.py (2)

83-89: LGTM: enforce team presence before permission checks.

Early explicit “Team is required” improves UX and avoids Nones downstream.


103-110: LGTM: permission + domain validation composition is correct.

You first ensure actor can manage members, then validate member removability; error raising preserves is_public.

django/thunderstore/repository/models/team.py (2)

250-362: LGTM: new validator methods replace ensure_ with clear (errors, is_public) contract.*

Consistent use of check_user_permissions + membership/role checks reads cleanly and is easy to propagate to forms/services.


408-449: LGTM: can_user_ wrappers properly mirror validator outcomes.*

Each wrapper defers to its validator and returns True iff errors are empty.

Comment on lines +22 to +24
errors, _ = team.validate_can_create_service_account(self.user)
if errors:
raise forms.ValidationError(errors)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use PermissionValidationError to keep is_public context for create flow

For consistency with delete/edit forms and to retain is_public, raise PermissionValidationError instead of a plain forms.ValidationError.

-        errors, _ = team.validate_can_create_service_account(self.user)
-        if errors:
-            raise forms.ValidationError(errors)
+        errors, is_public = team.validate_can_create_service_account(self.user)
+        if errors:
+            raise PermissionValidationError(errors, is_public=is_public)
📝 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
errors, _ = team.validate_can_create_service_account(self.user)
if errors:
raise forms.ValidationError(errors)
errors, is_public = team.validate_can_create_service_account(self.user)
if errors:
raise PermissionValidationError(errors, is_public=is_public)
🤖 Prompt for AI Agents
In django/thunderstore/account/forms.py around lines 22-24, replace raising
forms.ValidationError with raising PermissionValidationError so the create flow
preserves is_public context; update the code to raise
PermissionValidationError(errors, is_public=self.is_public) (or the equivalent
used elsewhere in delete/edit forms) and add the corresponding import for
PermissionValidationError from the module used by the other account forms.

Comment on lines +15 to +21
validators = [team.validate_user_can_access, team.validate_user_can_disband]

for validator in validators:
errors, is_public = validator(user)
if errors:
raise PermissionValidationError(errors, is_public=is_public)

Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Type annotation mismatch in Team.validate_user_can_disband may break type-checking

This caller expects validators to return Tuple[List[str], bool]. In repository/models/team.py, validate_user_can_disband is annotated as Tuple[str, bool] in the provided snippet while returning lists. Align annotation to Tuple[List[str], bool].

Proposed fix in django/thunderstore/repository/models/team.py:

def validate_user_can_disband(self, user: Optional[UserType]) -> Tuple[List[str], bool]:
    # ...

Run to verify annotations across all team validators:


🏁 Script executed:

#!/bin/bash
# Ensure all validate_* methods on Team return Tuple[List[str], bool]
rg -nP --type=py '(def\s+validate_[a-zA-Z_]+\s*\(.*\)\s*->\s*Tuple\[[^\]]+\])' -C1 | sed -n '1,200p'

Length of output: 397


Action: Correct the return‐type annotation of validate_user_can_disband

The current signature in django/thunderstore/repository/models/team.py (line 382) is:

def validate_user_can_disband(self, user: Optional[UserType]) -> Tuple[str, bool]:
    …

but callers expect Tuple[List[str], bool]. Update it to:

-    def validate_user_can_disband(self, user: Optional[UserType]) -> Tuple[str, bool]:
+    def validate_user_can_disband(self, user: Optional[UserType]) -> Tuple[List[str], bool]:
         # ...

Also ensure you have:

from typing import Tuple, List, Optional

so that List is recognized.

After this change, rerun your type‐checker to confirm no further mismatches.

🤖 Prompt for AI Agents
In django/thunderstore/repository/models/team.py around line 382 the function
validate_user_can_disband currently declares its return type as Tuple[str, bool]
but callers expect Tuple[List[str], bool]; update the signature to return
Tuple[List[str], bool] and adjust any internal return sites to return a list of
error strings (even if single element) instead of a bare string; also ensure the
module imports Tuple, List, Optional from typing (add "from typing import Tuple,
List, Optional" if missing) and then rerun the type-checker to verify there are
no remaining mismatches.

💡 Verification agent

🧩 Analysis chain

Bug risk: create_team uses getattr(user, "service_account", None) which can raise DoesNotExist

Reverse OneToOne access can raise ServiceAccount.DoesNotExist, even when a default is provided to getattr. Use an existence query or a guarded try/except.

Outside this hunk, update the check in create_team(...):

from thunderstore.account.models import ServiceAccount
# ...
if ServiceAccount.objects.filter(user=user).exists():
    raise PermissionValidationError("Service accounts cannot create teams")

Alternatively, guarded access:

from django.core.exceptions import ObjectDoesNotExist
try:
    user.service_account
    is_sa = True
except (AttributeError, ObjectDoesNotExist):
    is_sa = False
if is_sa:
    raise PermissionValidationError("Service accounts cannot create teams")

To find other unsafe probes of service_account:


🏁 Script executed:

#!/bin/bash
rg -nP --type=py -C2 '(hasattr\([^,]+,\s*[\'"]service_account[\'"]\)|getattr\([^,]+,\s*[\'"]service_account[\'"])'

Length of output: 297


🏁 Script executed:

#!/bin/bash
# Search for the definition and implementation of create_team
rg -nP --type=py "def create_team" -C5

echo
echo "Searching for unsafe getattr on service_account"
rg -nP --type=py -C2 "getattr\([^,]+,\s*['\"]service_account['\"]" || true

echo
echo "Searching for hasattr on service_account"
rg -nP --type=py -C2 "hasattr\([^,]+,\s*['\"]service_account['\"]" || true

echo
echo "Searching for direct attribute access .service_account"
rg -nP --type=py -C2 "\.service_account\b" || true

Length of output: 23347


Prevent ServiceAccount.DoesNotExist by avoiding getattr(user, "service_account")

The current checks use getattr(user, "service_account", None), but Django’s reverse OneToOne lookup raises ServiceAccount.DoesNotExist (a subclass of ObjectDoesNotExist) when no related instance exists—this isn’t caught by getattr’s default, so it will error instead of returning None. We need to update both occurrences to safely detect service accounts.

Affected locations:

  • django/thunderstore/api/cyberstorm/services/team.py (lines 29–30)
  • django/thunderstore/repository/forms/team.py (lines 46–47)

Replace this pattern:

# current unsafe check
if getattr(user, "service_account", None) is not None:
    raise PermissionValidationError("Service accounts cannot create teams")

With either an existence query:

from thunderstore.account.models import ServiceAccount

if ServiceAccount.objects.filter(user=user).exists():
    raise PermissionValidationError("Service accounts cannot create teams")

Or a guarded attribute lookup:

from django.core.exceptions import ObjectDoesNotExist

try:
    # will raise ServiceAccount.DoesNotExist if none exists
    user.service_account
    is_sa = True
except (AttributeError, ObjectDoesNotExist):
    is_sa = False

if is_sa:
    raise PermissionValidationError("Service accounts cannot create teams")

Please update both files accordingly to eliminate the crash risk.

🤖 Prompt for AI Agents
In django/thunderstore/api/cyberstorm/services/team.py around lines 15 to 21
(and also update django/thunderstore/api/cyberstorm/services/team.py lines 29–30
as referenced) replace the unsafe getattr(user, "service_account", None) check
which can raise ServiceAccount.DoesNotExist with a safe existence check: either
use ServiceAccount.objects.filter(user=user).exists() to detect service
accounts, or perform a guarded lookup that catches ObjectDoesNotExist (and
AttributeError) around user.service_account; then raise
PermissionValidationError when a service account exists. Apply the same change
to django/thunderstore/repository/forms/team.py around lines 46–47 so both
locations use the same safe pattern.

@@ -1,4 +1,4 @@
from typing import Optional
from typing import List, Optional, Tuple
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Import ObjectDoesNotExist to safely probe reverse OneToOne relations

Accessing user.service_account can raise ServiceAccount.DoesNotExist, which is a subclass of ObjectDoesNotExist. We need this import for a safe try/except around the service account check below.

 from typing import List, Optional, Tuple
+from django.core.exceptions import ObjectDoesNotExist
📝 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
from typing import List, Optional, Tuple
from typing import List, Optional, Tuple
from django.core.exceptions import ObjectDoesNotExist
🤖 Prompt for AI Agents
In django/thunderstore/permissions/utils.py around line 1, the module currently
imports typing only and does not import ObjectDoesNotExist; accessing
user.service_account can raise ServiceAccount.DoesNotExist (a subclass of
ObjectDoesNotExist), so add "from django.core.exceptions import
ObjectDoesNotExist" to the imports and update the service account check to wrap
the access in a try/except that catches ObjectDoesNotExist (instead of or in
addition to ServiceAccount.DoesNotExist) to safely handle missing reverse
OneToOne relations and avoid unhandled exceptions.

Comment on lines +21 to +35
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix: hasattr/getattr on reverse OneToOne may raise DoesNotExist (crashes permission checks)

hasattr(user, "service_account") on a User without a related ServiceAccount raises ServiceAccount.DoesNotExist (not AttributeError), causing this function to error instead of returning a validation error. Replace with a safe try/except check.

 def check_user_permissions(
     user: Optional[UserType], allow_serviceaccount: bool = False
 ) -> Tuple[List[str], bool]:
     errors = []
     public_error = True  # Set to False if you want to hide certain errors
 
     if not user or not user.is_authenticated:
         errors.append("Must be authenticated")
     elif not user.is_active:
         errors.append("User has been deactivated")
         public_error = False
-    elif hasattr(user, "service_account") and not allow_serviceaccount:
-        errors.append("Service accounts are unable to perform this action")
+    else:
+        # Reverse OneToOne access may raise DoesNotExist when the relation is missing
+        try:
+            getattr(user, "service_account")
+            is_service_account = True
+        except (AttributeError, ObjectDoesNotExist):
+            is_service_account = False
+        if is_service_account and not allow_serviceaccount:
+            errors.append("Service accounts are unable to perform this action")
 
     return errors, public_error

As a follow-up, mirror the same safe pattern in validate_user(...) to prevent identical crashes there:

# In validate_user(...)
try:
    getattr(user, "service_account")
    is_service_account = True
except (AttributeError, ObjectDoesNotExist):
    is_service_account = False
if is_service_account and not allow_serviceaccount:
    raise PermissionValidationError("Service accounts are unable to perform this action")

I can send a separate PR patch touching both places if helpful.

🤖 Prompt for AI Agents
In django/thunderstore/permissions/utils.py around lines 21 to 35, using
hasattr(user, "service_account") can raise ServiceAccount.DoesNotExist (an
ObjectDoesNotExist subclass) and crash permission checks; replace the hasattr
call with a safe getattr/try-except that catches AttributeError and
django.core.exceptions.ObjectDoesNotExist, set a boolean like is_service_account
accordingly, and append the service account error only when is_service_account
is True and allow_serviceaccount is False; also apply the same safe
getattr/try-except pattern in validate_user(...) to avoid the identical crash
there.

Comment on lines +133 to +139
if not team:
raise ValidationError("Team is missing")

error, is_public = team.validate_member_role_be_changed(self.instance, new_role)
if error:
raise PermissionValidationError(error, is_public=is_public)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix method name: call validate_member_role_can_be_changed.

Current call uses a non-existent name (validate_member_role_be_changed), which will raise AttributeError at runtime.

-        error, is_public = team.validate_member_role_be_changed(self.instance, new_role)
+        error, is_public = team.validate_member_role_can_be_changed(self.instance, new_role)
📝 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 not team:
raise ValidationError("Team is missing")
error, is_public = team.validate_member_role_be_changed(self.instance, new_role)
if error:
raise PermissionValidationError(error, is_public=is_public)
if not team:
raise ValidationError("Team is missing")
error, is_public = team.validate_member_role_can_be_changed(self.instance, new_role)
if error:
raise PermissionValidationError(error, is_public=is_public)
🤖 Prompt for AI Agents
django/thunderstore/repository/forms/team.py around lines 133 to 139: the code
calls a non-existent method validate_member_role_be_changed which causes
AttributeError at runtime; change the call to
validate_member_role_can_be_changed (preserving the same args and handling of
returned error and is_public), and run tests/lint to ensure no other references
to the old name remain.

Comment on lines +179 to 189
error, is_public = self.instance.validate_user_can_access(self.user)
if error:
raise PermissionValidationError(error, is_public=is_public)
return super().clean()

@transaction.atomic
def save(self, **kwargs):
self.instance.ensure_user_can_disband(self.user)
error, is_public = self.instance.validate_user_can_access(self.user)
if error:
raise PermissionValidationError(error, is_public=is_public)
self.instance.delete()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Security: use validate_user_can_disband, not validate_user_can_access.

Disband requires ownership (and no packages). Using “access” would allow any member to disband a team. Swap to the stricter validator.

-        error, is_public = self.instance.validate_user_can_access(self.user)
+        error, is_public = self.instance.validate_user_can_disband(self.user)
         if error:
             raise PermissionValidationError(error, is_public=is_public)
📝 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
error, is_public = self.instance.validate_user_can_access(self.user)
if error:
raise PermissionValidationError(error, is_public=is_public)
return super().clean()
@transaction.atomic
def save(self, **kwargs):
self.instance.ensure_user_can_disband(self.user)
error, is_public = self.instance.validate_user_can_access(self.user)
if error:
raise PermissionValidationError(error, is_public=is_public)
self.instance.delete()
@transaction.atomic
def save(self, **kwargs):
error, is_public = self.instance.validate_user_can_disband(self.user)
if error:
raise PermissionValidationError(error, is_public=is_public)
self.instance.delete()
🤖 Prompt for AI Agents
In django/thunderstore/repository/forms/team.py around lines 179 to 189, the
code calls validate_user_can_access for both cleaning and saving (deletion),
which is too permissive for disbanding a team; replace those calls with
validate_user_can_disband so the stricter ownership-and-no-packages check is
enforced, and keep raising PermissionValidationError(error, is_public=is_public)
when the disband validator returns an error.

Comment on lines +186 to 189
error, is_public = self.instance.validate_user_can_access(self.user)
if error:
raise PermissionValidationError(error, is_public=is_public)
self.instance.delete()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Apply the same disband validator in save().

Mirror the clean() logic to enforce owner/no-packages at execution time.

-        error, is_public = self.instance.validate_user_can_access(self.user)
+        error, is_public = self.instance.validate_user_can_disband(self.user)
         if error:
             raise PermissionValidationError(error, is_public=is_public)
📝 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
error, is_public = self.instance.validate_user_can_access(self.user)
if error:
raise PermissionValidationError(error, is_public=is_public)
self.instance.delete()
error, is_public = self.instance.validate_user_can_disband(self.user)
if error:
raise PermissionValidationError(error, is_public=is_public)
self.instance.delete()
🤖 Prompt for AI Agents
In django/thunderstore/repository/forms/team.py around lines 186 to 189, the
form's save() currently deletes the team without re-checking the same disband
validator used in clean(); update save() to call
self.instance.validate_user_can_access(self.user) before performing the delete,
raise PermissionValidationError(error, is_public=is_public) if it returns an
error, and only then proceed to self.instance.delete() so the owner/no-packages
check is enforced at execution time.

Comment on lines +344 to +349
assert errors == []
else:
assert team.can_user_manage_members(user) is False
with pytest.raises(ValidationError) as e:
team.ensure_user_can_manage_members(user)
assert "Must be an owner to manage team members" in str(e.value)
errors, is_public = team.validate_can_manage_members(user)
errors = ["Must be an owner to manage team members"]
is_public is True
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix stray assignment and ineffective comparison; assert the expectations.

The current code reassigns errors and uses a pointless comparison on is_public without asserting, causing the test to pass/fail incorrectly. Also flagged by static analysis (B015).

-                errors, is_public = team.validate_can_manage_members(user)
-                errors = ["Must be an owner to manage team members"]
-                is_public is True
+                errors, is_public = team.validate_can_manage_members(user)
+                assert errors == ["Must be an owner to manage team members"]
+                assert is_public is True
📝 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
assert errors == []
else:
assert team.can_user_manage_members(user) is False
with pytest.raises(ValidationError) as e:
team.ensure_user_can_manage_members(user)
assert "Must be an owner to manage team members" in str(e.value)
errors, is_public = team.validate_can_manage_members(user)
errors = ["Must be an owner to manage team members"]
is_public is True
assert errors == []
else:
assert team.can_user_manage_members(user) is False
errors, is_public = team.validate_can_manage_members(user)
assert errors == ["Must be an owner to manage team members"]
assert is_public is True
🧰 Tools
🪛 Ruff (0.12.2)

349-349: Pointless comparison. Did you mean to assign a value? Otherwise, prepend assert or remove it.

(B015)

🤖 Prompt for AI Agents
In django/thunderstore/repository/tests/test_team.py around lines 344 to 349,
the test mistakenly reassigns the errors variable and uses a bare boolean
expression for is_public instead of assertions; replace the stray assignment
with an equality assertion comparing the returned errors to ["Must be an owner
to manage team members"] (do not overwrite the errors variable), and change the
bare "is_public is True" expression to an actual assertion (e.g., assert
is_public is True) so both expectations are properly asserted.

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.

1 participant