From 6bd49c2d3ed3559955b006da93b94d3443a81d3e Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Sun, 28 Jun 2026 17:36:48 +0330 Subject: [PATCH] fix(security): make commission_percent & active admin-only on PATCH representation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A representation editing its own record could raise its own commission or self-activate (privilege escalation). Restrict both fields to ROLE_ADMIN and range-check commission (0–100). Owner can still edit name/city/bank. Co-Authored-By: Claude Opus 4.8 --- docs/api/representation.md | 4 + .../Controller/RepresentationController.php | 38 ++++++++-- .../RepresentationCommissionTest.php | 76 +++++++++++++++++++ 3 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 tests/Representation/RepresentationCommissionTest.php diff --git a/docs/api/representation.md b/docs/api/representation.md index 51f9f060..bfa10b9b 100644 --- a/docs/api/representation.md +++ b/docs/api/representation.md @@ -121,6 +121,8 @@ Update representation. All fields optional. +**Privileged fields:** `commission_percent` and `active` are **admin-only** — a representation editing its own record may change `full_name`, `city_id`, `bank_account` but **not** these two. `commission_percent` must be within `0–100`. + ### Response `200` Updated representation object. @@ -129,6 +131,8 @@ Updated representation object. |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_FORBIDDEN_001` | 403 | Not owner or admin | +| `ERR_AUTH_006` | 403 | Non-admin tried to change `commission_percent` or `active` | +| `ERR_VALIDATION_001` | 422 | `commission_percent` خارج از بازه ۰ تا ۱۰۰ (`field: commission_percent`) | | `ERR_NOT_FOUND_001` | 404 | Representation not found | --- diff --git a/src/Representation/Controller/RepresentationController.php b/src/Representation/Controller/RepresentationController.php index 15d659cf..ba9ca29c 100644 --- a/src/Representation/Controller/RepresentationController.php +++ b/src/Representation/Controller/RepresentationController.php @@ -107,6 +107,13 @@ class RepresentationController extends BaseController return $this->error(ErrorCodes::ERR_CONFLICT_001, 'این کاربر قبلاً نماینده است', 409); } + if (!empty($data['commission_percent'])) { + $commission = (float) $data['commission_percent']; + if ($commission < 0 || $commission > 100) { + return $this->error(ErrorCodes::ERR_VALIDATION_001, 'درصد کمیسیون باید بین ۰ تا ۱۰۰ باشد', 422, 'commission_percent'); + } + } + $rep = new Representation($user, $fullName); if (isset($data['city_id'])) $rep->setCityId($data['city_id'] ? (int)$data['city_id'] : null); if (!empty($data['commission_percent'])) $rep->setCommissionPercent((string)$data['commission_percent']); @@ -202,12 +209,31 @@ class RepresentationController extends BaseController return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403); } - $data = json_decode($request->getContent(), true) ?? []; - if (array_key_exists('full_name', $data)) $rep->setFullName($data['full_name']); - if (array_key_exists('city_id', $data)) $rep->setCityId($data['city_id'] ? (int)$data['city_id'] : null); - if (array_key_exists('bank_account', $data)) $rep->setBankAccount($data['bank_account']); - if (array_key_exists('commission_percent', $data)) $rep->setCommissionPercent((string)$data['commission_percent']); - if (array_key_exists('active', $data)) $rep->setActive((bool)$data['active']); + $data = json_decode($request->getContent(), true) ?? []; + $isAdmin = $user->hasRole('ROLE_ADMIN'); + + if (array_key_exists('full_name', $data)) $rep->setFullName($data['full_name']); + if (array_key_exists('city_id', $data)) $rep->setCityId($data['city_id'] ? (int)$data['city_id'] : null); + if (array_key_exists('bank_account', $data)) $rep->setBankAccount($data['bank_account']); + + // commission_percent and active are privileged: a representative must not + // be able to raise their own commission or activate themselves. + if (array_key_exists('commission_percent', $data)) { + if (!$isAdmin) { + return $this->error(ErrorCodes::ERR_AUTH_006, 'تغییر درصد کمیسیون فقط توسط مدیر مجاز است', 403); + } + $commission = (float) $data['commission_percent']; + if ($commission < 0 || $commission > 100) { + return $this->error(ErrorCodes::ERR_VALIDATION_001, 'درصد کمیسیون باید بین ۰ تا ۱۰۰ باشد', 422, 'commission_percent'); + } + $rep->setCommissionPercent((string) $commission); + } + if (array_key_exists('active', $data)) { + if (!$isAdmin) { + return $this->error(ErrorCodes::ERR_AUTH_006, 'تغییر وضعیت فعال‌بودن فقط توسط مدیر مجاز است', 403); + } + $rep->setActive((bool) $data['active']); + } $this->representationRepo->save($rep); diff --git a/tests/Representation/RepresentationCommissionTest.php b/tests/Representation/RepresentationCommissionTest.php new file mode 100644 index 00000000..f9205da7 --- /dev/null +++ b/tests/Representation/RepresentationCommissionTest.php @@ -0,0 +1,76 @@ +createUser(['ROLE_USER', 'ROLE_REPRESENTATION']); + $rep = new Representation($owner, 'نماینده تست'); + $rep->setCommissionPercent('10'); + $this->em->persist($rep); + $this->em->flush(); + + return [$owner, $rep]; + } + + public function testOwnerCannotChangeOwnCommission(): void + { + [$owner, $rep] = $this->makeRep(); + $this->authJson('PATCH', '/api/v1/representation/' . $rep->getUuid(), $owner, [ + 'commission_percent' => 90, + ]); + $this->assertSame(403, $this->responseCode()); + + $this->em->clear(); + $fresh = $this->em->getRepository(Representation::class)->find($rep->getId()); + $this->assertSame(10.0, (float) $fresh->getCommissionPercent()); + } + + public function testOwnerCannotSelfActivate(): void + { + [$owner, $rep] = $this->makeRep(); + $this->authJson('PATCH', '/api/v1/representation/' . $rep->getUuid(), $owner, [ + 'active' => true, + ]); + $this->assertSame(403, $this->responseCode()); + } + + public function testAdminCanChangeCommission(): void + { + [, $rep] = $this->makeRep(); + $admin = $this->createUser(['ROLE_ADMIN']); + $this->authJson('PATCH', '/api/v1/representation/' . $rep->getUuid(), $admin, [ + 'commission_percent' => 25, + ]); + $this->assertSame(200, $this->responseCode()); + } + + public function testAdminRejectedOnOutOfRangeCommission(): void + { + [, $rep] = $this->makeRep(); + $admin = $this->createUser(['ROLE_ADMIN']); + $this->authJson('PATCH', '/api/v1/representation/' . $rep->getUuid(), $admin, [ + 'commission_percent' => 150, + ]); + $this->assertSame(422, $this->responseCode()); + } + + public function testOwnerCanStillEditOwnName(): void + { + [$owner, $rep] = $this->makeRep(); + $this->authJson('PATCH', '/api/v1/representation/' . $rep->getUuid(), $owner, [ + 'full_name' => 'نام جدید', + ]); + $this->assertSame(200, $this->responseCode()); + } +}