Skip to content

fix missing authorization on multiple mutations#7240

Open
Amartuvshins0404 wants to merge 2 commits intoerxes:mainfrom
Amartuvshins0404:fix/missing-authorization-checks-7032
Open

fix missing authorization on multiple mutations#7240
Amartuvshins0404 wants to merge 2 commits intoerxes:mainfrom
Amartuvshins0404:fix/missing-authorization-checks-7032

Conversation

@Amartuvshins0404
Copy link

@Amartuvshins0404 Amartuvshins0404 commented Mar 21, 2026

three mutations had no permission checks. any logged-in user could call them regardless of role.

accounting plugin had nothing - no checkPermission calls on accountsAdd, accountsEdit, accountsRemove, or accountsMerge, and no permissions meta registered at all. added a permissions.ts with action definitions and admin/viewer default groups, then wired checkPermission into all four mutations.

dealsRemove in sales didn't even extract user from context. added user and the same canEditMemberIds stage check that dealsEdit already does.

cpUsersRemove in core-api let any authenticated team member delete client portal users. added checkPermission('clientPortalManage').

the issue also mentions the trpc task:tag procedure having no auth. skipped that one because the trpc context across the whole codebase doesn't carry user or checkPermission - it's an architectural thing, not a single-file fix.

tested with an owner user and an unprivileged user. owner passes through, unprivileged gets "permission required" on all three.

closes #7032

Summary by Sourcery

Enforce proper authorization on accounting, sales deal removal, and client portal user removal mutations.

Bug Fixes:

  • Restrict accounting account create, edit, remove, and merge mutations to users with the appropriate accounting permissions.
  • Prevent unauthorized users from removing sales deals by applying the same stage-based member edit checks as other deal mutations.
  • Require client portal management permission before allowing removal of client portal users.

Enhancements:

  • Introduce a structured permissions configuration for the accounting plugin, including actions and default admin/viewer permission groups.

Summary by CodeRabbit

  • New Features

    • Added a permissions configuration for the accounting plugin with admin and viewer roles for fine-grained account access.
  • Bug Fixes

    • Enforced authorization on admin user removal, account create/edit/remove/merge operations, and deal removals.
    • Deal removal now respects stage-based edit membership restrictions before proceeding.

Addresses erxes#7032:

1. Accounting mutations: add checkPermission calls to accountsAdd,
   accountsEdit, accountsRemove, accountsMerge with new permissions
   meta registration (admin/viewer default groups).

2. dealsRemove: extract user from context and add canEditMemberIds
   stage-level permission check, matching the existing editDeal pattern.

3. cpUsersRemove: add checkPermission('clientPortalManage') to enforce
   authorization before deleting client portal users.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@sourcery-ai
Copy link

sourcery-ai bot commented Mar 21, 2026

Reviewer's Guide

Adds missing authorization and permission checks to several GraphQL mutations across accounting, sales, and core-api client portal modules, and introduces a permissions configuration for the accounting plugin.

Sequence diagram for accounting account mutations authorization

sequenceDiagram
  actor Client
  participant GraphQLServer
  participant AuthService
  participant Models
  participant AccountsModel

  Client->>GraphQLServer: accountsAdd(doc)
  GraphQLServer->>AuthService: checkPermission(manageAccounts)
  AuthService-->>GraphQLServer: ok or error
  alt permission granted
    GraphQLServer->>AccountsModel: createAccount(doc)
    AccountsModel-->>GraphQLServer: account
    GraphQLServer-->>Client: account
  else permission denied
    GraphQLServer-->>Client: error Permission required
  end

  Client->>GraphQLServer: accountsEdit(_id, doc)
  GraphQLServer->>AuthService: checkPermission(manageAccounts)
  AuthService-->>GraphQLServer: ok or error
  alt permission granted
    GraphQLServer->>AccountsModel: getAccount(_id)
    AccountsModel-->>GraphQLServer: existingAccount
    GraphQLServer->>AccountsModel: updateAccount(_id, doc)
    AccountsModel-->>GraphQLServer: updatedAccount
    GraphQLServer-->>Client: updatedAccount
  else permission denied
    GraphQLServer-->>Client: error Permission required
  end

  Client->>GraphQLServer: accountsRemove(accountIds)
  GraphQLServer->>AuthService: checkPermission(removeAccounts)
  AuthService-->>GraphQLServer: ok or error
  alt permission granted
    GraphQLServer->>AccountsModel: removeAccounts(accountIds)
    AccountsModel-->>GraphQLServer: removeResult
    GraphQLServer-->>Client: removeResult
  else permission denied
    GraphQLServer-->>Client: error Permission required
  end

  Client->>GraphQLServer: accountsMerge(accountIds, accountFields)
  GraphQLServer->>AuthService: checkPermission(accountsMerge)
  AuthService-->>GraphQLServer: ok or error
  alt permission granted
    GraphQLServer->>AccountsModel: mergeAccounts(accountIds, accountFields)
    AccountsModel-->>GraphQLServer: mergedAccount
    GraphQLServer-->>Client: mergedAccount
  else permission denied
    GraphQLServer-->>Client: error Permission required
  end
Loading

Sequence diagram for dealsRemove authorization based on stage permissions

sequenceDiagram
  actor User
  participant GraphQLServer
  participant Models
  participant DealsModel
  participant StagesModel
  participant SubscriptionSystem

  User->>GraphQLServer: dealsRemove(_id)
  GraphQLServer->>DealsModel: findOne({_id})
  DealsModel-->>GraphQLServer: deal or null
  alt deal not found
    GraphQLServer-->>User: error Deal not found
  else deal found
    GraphQLServer->>StagesModel: getStage(deal.stageId)
    StagesModel-->>GraphQLServer: stage
    GraphQLServer->>GraphQLServer: read stage.canEditMemberIds
    alt canEditMemberIds is set and does not include user._id
      GraphQLServer-->>User: error Permission denied
    else user allowed to edit stage
      GraphQLServer->>DealsModel: removeDeals([deal._id])
      DealsModel-->>GraphQLServer: removedDeals
      GraphQLServer->>SubscriptionSystem: subscriptionWrapper(models, payload)
      SubscriptionSystem-->>GraphQLServer: ok
      GraphQLServer-->>User: removedDeals
    end
  end
Loading

Sequence diagram for cpUsersRemove client portal management permission

sequenceDiagram
  actor TeamMember
  participant GraphQLServer
  participant AuthService
  participant Models
  participant CPUserModel

  TeamMember->>GraphQLServer: cpUsersRemove(_id)
  GraphQLServer->>AuthService: checkPermission(clientPortalManage)
  AuthService-->>GraphQLServer: ok or error
  alt permission granted
    GraphQLServer->>CPUserModel: removeUser(_id, models)
    CPUserModel-->>GraphQLServer: void
    GraphQLServer-->>TeamMember: {_id}
  else permission denied
    GraphQLServer-->>TeamMember: error Permission required
  end
Loading

File-Level Changes

Change Details Files
Enforce permission checks on all accounting accounts GraphQL mutations using new accounting plugin permission actions.
  • Inject checkPermission into accountsAdd and require manageAccounts before creating accounts.
  • Inject checkPermission into accountsEdit and require manageAccounts before updating accounts.
  • Inject checkPermission into accountsRemove and require removeAccounts before deleting accounts.
  • Inject checkPermission into accountsMerge and require accountsMerge before merging accounts.
backend/plugins/accounting_api/src/modules/accounting/graphql/resolvers/mutations/accounts.ts
Register accounting plugin permissions and default groups to support new authorization checks.
  • Create permissions meta defining accounting module, scopes, and actions for reading, managing, removing, and merging accounts.
  • Define Accounting Admin default group with full accounting permissions and all scope.
  • Define Accounting Viewer default group with read-only accountsRead permission and all scope.
  • Wire the new permissions meta into the accounting plugin startup meta configuration.
backend/plugins/accounting_api/src/meta/permissions.ts
backend/plugins/accounting_api/src/main.ts
Align dealsRemove authorization with dealsEdit by enforcing stage-based edit permissions.
  • Include user in GraphQL context for dealsRemove resolver.
  • Load the deal stage and read canEditMemberIds from it.
  • Block deletion when canEditMemberIds is non-empty and does not contain the current user, throwing a Permission denied error.
backend/plugins/sales_api/src/modules/sales/graphql/resolvers/mutations/deals.ts
Restrict client portal user deletion to users with explicit manage permission.
  • Inject checkPermission into cpUsersRemove resolver context.
  • Require clientPortalManage permission before allowing CP user deletion.
backend/core-api/src/modules/clientportal/graphql/resolvers/mutations/cpUser/admin.ts

Assessment against linked issues

Issue Objective Addressed Explanation
#7032 Enforce proper permission checks on all accounting account mutations (accountsAdd, accountsEdit, accountsRemove, accountsMerge) in the accounting plugin.
#7032 Add ownership/membership-based authorization to the dealsRemove mutation so that only authorized users (e.g., based on canEditMemberIds) can delete deals.
#7032 Add client portal ownership validation to cpUsersRemove so that a user can only delete client portal users belonging to the appropriate clientPortalId, not just based on a generic permission. The PR adds a generic permission check (checkPermission('clientPortalManage')) to cpUsersRemove but does not implement any validation tying the CPUser to a specific clientPortalId or verifying ownership of that client portal, which the issue explicitly called out as missing.
#7032 Add user-context-based authorization to the tRPC task:tag procedure so that task tags cannot be updated on arbitrary task IDs without proper auth. The PR explicitly states that the task:tag tRPC procedure was skipped due to architectural limitations in the tRPC context, and no related changes appear in the diff.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@deepsource-io
Copy link

deepsource-io bot commented Mar 21, 2026

DeepSource Code Review

We reviewed changes in ac9897f...28703cd on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
JavaScript Mar 21, 2026 5:39p.m. Review ↗
Docker Mar 21, 2026 5:39p.m. Review ↗

@coderabbitai
Copy link

coderabbitai bot commented Mar 21, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3152351e-e9e8-4a9c-a7a1-12ae4358abf1

📥 Commits

Reviewing files that changed from the base of the PR and between f7c765f and 28703cd.

📒 Files selected for processing (1)
  • backend/plugins/accounting_api/src/meta/permissions.ts
✅ Files skipped from review due to trivial changes (1)
  • backend/plugins/accounting_api/src/meta/permissions.ts

📝 Walkthrough

Walkthrough

Added explicit authorization checks and a new permissions config: client-portal user removal now awaits permission validation; accounting plugin exposes a permissions manifest and its account mutations enforce scoped checks; deals removal enforces stage-level member edit restrictions before deletion.

Changes

Cohort / File(s) Summary
Client Portal Authorization
backend/core-api/src/modules/clientportal/graphql/resolvers/mutations/cpUser/admin.ts
Destructured checkPermission from IContext and await checkPermission('clientPortalManage') in cpUsersRemove before calling model removal.
Accounting Plugin Metadata
backend/plugins/accounting_api/src/main.ts, backend/plugins/accounting_api/src/meta/permissions.ts
Added permissions export (IPermissionConfig) describing accounting module actions/scopes and default groups; included permissions in plugin meta passed to startPlugin.
Accounting Mutations Authorization
backend/plugins/accounting_api/src/modules/accounting/graphql/resolvers/mutations/accounts.ts
Resolvers now destructure checkPermission and await permission checks (manageAccounts, removeAccounts, accountsMerge) in accountsAdd, accountsEdit, accountsRemove, and accountsMerge before model operations.
Sales Mutations Authorization
backend/plugins/sales_api/src/modules/sales/graphql/resolvers/mutations/deals.ts
dealsRemove now accesses user, retrieves stage via models.Stages.getStage(...), and rejects removal when stage.canEditMemberIds exists and user._id is not included; removal proceeds only after this check.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped through code with careful paws,
Checking gates and sealing flaws,
Accounts, portals, deals in line—
Permissions set, the system's fine! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix missing authorization on multiple mutations' accurately captures the main objective of the PR, which is adding missing authorization checks across multiple mutation resolvers.
Linked Issues check ✅ Passed The PR successfully addresses three of four requirements from #7032: accounting mutations now have permission checks via IPermissionConfig, dealsRemove enforces canEditMemberIds checks, and cpUsersRemove requires clientPortalManage permission. The tRPC task:tag procedure was intentionally deferred as an architectural task.
Out of Scope Changes check ✅ Passed All changes directly support the PR objectives: permission configuration and checks for accounting, authorization checks for dealsRemove, and permission enforcement for cpUsersRemove. No unrelated refactoring or scope creep detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Tip

Migrating from UI to YAML configuration.

Use the @coderabbitai configuration command in a PR comment to get a dump of all your UI settings in YAML format. You can then edit this YAML file and upload it to the root of your repository to configure CodeRabbit programmatically.

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • In dealsRemove, consider handling the case where models.Stages.getStage(item.stageId) returns null/undefined before destructuring canEditMemberIds to avoid a runtime error when a deal has an invalid or missing stage.
  • The canEditMemberIds permission logic in dealsRemove duplicates what dealsEdit already does; consider extracting this check into a shared helper to keep the authorization rules consistent and easier to maintain.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `dealsRemove`, consider handling the case where `models.Stages.getStage(item.stageId)` returns `null`/`undefined` before destructuring `canEditMemberIds` to avoid a runtime error when a deal has an invalid or missing stage.
- The `canEditMemberIds` permission logic in `dealsRemove` duplicates what `dealsEdit` already does; consider extracting this check into a shared helper to keep the authorization rules consistent and easier to maintain.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

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.

🧹 Nitpick comments (1)
backend/plugins/sales_api/src/modules/sales/graphql/resolvers/mutations/deals.ts (1)

154-157: Consider consistency: dealsCopy lacks stage-level authorization check.

The dealsCopy mutation creates a copy of a deal but doesn't enforce the canEditMemberIds check that dealsRemove and editDeal now use. If copying a deal should be restricted to authorized stage members, consider adding the same check here in a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/plugins/sales_api/src/modules/sales/graphql/resolvers/mutations/deals.ts`
around lines 154 - 157, The dealsCopy mutation is missing the same stage-level
authorization check used in dealsRemove and editDeal; update the dealsCopy
resolver to fetch the deal's stage (via the deal record or models/stages) and
invoke the existing authorization helper (canEditMemberIds or the project's
equivalent) with the current user and that stage before proceeding,
returning/throwing an authorization error when the check fails so only
authorized stage members can copy deals.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@backend/plugins/sales_api/src/modules/sales/graphql/resolvers/mutations/deals.ts`:
- Around line 154-157: The dealsCopy mutation is missing the same stage-level
authorization check used in dealsRemove and editDeal; update the dealsCopy
resolver to fetch the deal's stage (via the deal record or models/stages) and
invoke the existing authorization helper (canEditMemberIds or the project's
equivalent) with the current user and that stage before proceeding,
returning/throwing an authorization error when the check fails so only
authorized stage members can copy deals.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 774adc84-aeaa-49e9-94cf-95ad7f3ba417

📥 Commits

Reviewing files that changed from the base of the PR and between ac9897f and f7c765f.

📒 Files selected for processing (5)
  • backend/core-api/src/modules/clientportal/graphql/resolvers/mutations/cpUser/admin.ts
  • backend/plugins/accounting_api/src/main.ts
  • backend/plugins/accounting_api/src/meta/permissions.ts
  • backend/plugins/accounting_api/src/modules/accounting/graphql/resolvers/mutations/accounts.ts
  • backend/plugins/sales_api/src/modules/sales/graphql/resolvers/mutations/deals.ts

Copy link
Author

@Amartuvshins0404 Amartuvshins0404 left a comment

Choose a reason for hiding this comment

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

getStage is a getter that throws if the stage doesn't exist, so it won't return null - the deal would fail with "stage not found" before reaching the destructure.

on the shared helper - it's 5 lines in two places. extracting it adds abstraction for something that doesn't need it yet. if the pattern shows up more, happy to pull it out then. for a security fix i'd rather keep it small and obvious.

extract action names into constants and derive defaultGroups
from them instead of repeating the strings.
@sonarqubecloud
Copy link

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.

Security: Commented-out permission checks + missing authorization on multiple mutations (CWE-862)

1 participant