Commit b174cf606893e7cea05d8f87acb530d529903f96
1 parent
7ec2a40a
feat(security): annotate catalog + partners endpoints with @RequirePermission
Closes the P4.3 permission-rollout gap for the two oldest PBCs that
were never updated when the @RequirePermission aspect landed. The
catalog and partners metadata YAMLs already declared all the needed
permission keys — the controllers just weren't consuming them.
Catalog
- ItemController: list/get/getByCode → catalog.item.read;
create → catalog.item.create; update → catalog.item.update;
deactivate → catalog.item.deactivate.
- UomController: list/get/getByCode → catalog.uom.read;
create → catalog.uom.create; update → catalog.uom.update.
Partners (including the PII boundary)
- PartnerController: list/get/getByCode → partners.partner.read;
create → partners.partner.create; update → partners.partner.update.
(deactivate was already annotated in the P4.3 demo chunk.)
- AddressController: all five verbs annotated with
partners.address.{read,create,update,delete}.
- ContactController: all five verbs annotated with
partners.contact.{read,create,update,deactivate}. The
"TODO once P4.3 lands" note in the class KDoc was removed; P4.3
is live and the annotations are now in place. This is the PII
boundary that CLAUDE.md flagged as incomplete after the original
P4.3 rollout.
No new permission keys were added — all 14 keys this touches were
already declared in pbc-catalog/catalog.yml and
pbc-partners/partners.yml when those PBCs were first built. The
metadata loader has been serving them to the SPA/OpenAPI/MCP
introspection endpoint since day one; this change just starts
enforcing them at the controller.
Smoke-tested end-to-end against real Postgres
- Fresh DB + fresh boot.
- Admin happy path (bootstrap admin has wildcard `admin` role):
GET /api/v1/catalog/items → 200
POST /api/v1/catalog/items → 201 (SMOKE-1 created)
GET /api/v1/catalog/uoms → 200
POST /api/v1/partners/partners → 201 (SMOKE-P created)
POST /api/v1/partners/.../contacts → 201 (contact created)
GET /api/v1/partners/.../contacts → 200 (PII read)
- Anonymous negative path (no Bearer token):
GET /api/v1/catalog/items → 401
GET /api/v1/partners/.../contacts → 401
- 230 unit tests still green (annotations are purely additive,
no existing test hit the @RequirePermission path since the
service-level tests bypass the controller entirely).
Why this is a genuine security improvement
- Before: any authenticated user (including the eventual "Alice
from reception", the contractor's read-only service account,
the AI-agent MCP client) could read PII, create partners, and
create catalog items.
- After: those operations require explicit role-permission grants
through metadata__role_permission. The bootstrap admin still
has unconditional access via the wildcard admin role, so
nothing in a fresh deployment is broken; but a real operator
granting minimum-privilege roles now has the columns they need
in the database to do it.
- The contact PII boundary in particular is GDPR-relevant: before
this change, any logged-in user could enumerate every contact's
name + email + phone. After, only users with partners.contact.read
can see them.
What's still NOT annotated
- pbc-inventory's Location create/update/deactivate endpoints
(only stock.adjust and movement.create are annotated).
- pbc-orders-sales and pbc-orders-purchase list/get/create/update
endpoints (only the state-transition verbs are annotated).
- pbc-identity's user admin endpoints.
These are the next cleanup chunk. This one stays focused on
catalog + partners because those were the two PBCs that predated
P4.3 entirely and hadn't been touched since.
Showing
5 changed files
with
36 additions
and
5 deletions
pbc/pbc-catalog/src/main/kotlin/org/vibeerp/pbc/catalog/http/ItemController.kt
| @@ -19,6 +19,7 @@ import org.vibeerp.pbc.catalog.application.ItemService | @@ -19,6 +19,7 @@ import org.vibeerp.pbc.catalog.application.ItemService | ||
| 19 | import org.vibeerp.pbc.catalog.application.UpdateItemCommand | 19 | import org.vibeerp.pbc.catalog.application.UpdateItemCommand |
| 20 | import org.vibeerp.pbc.catalog.domain.Item | 20 | import org.vibeerp.pbc.catalog.domain.Item |
| 21 | import org.vibeerp.pbc.catalog.domain.ItemType | 21 | import org.vibeerp.pbc.catalog.domain.ItemType |
| 22 | +import org.vibeerp.platform.security.authz.RequirePermission | ||
| 22 | import java.util.UUID | 23 | import java.util.UUID |
| 23 | 24 | ||
| 24 | /** | 25 | /** |
| @@ -40,16 +41,19 @@ class ItemController( | @@ -40,16 +41,19 @@ class ItemController( | ||
| 40 | ) { | 41 | ) { |
| 41 | 42 | ||
| 42 | @GetMapping | 43 | @GetMapping |
| 44 | + @RequirePermission("catalog.item.read") | ||
| 43 | fun list(): List<ItemResponse> = | 45 | fun list(): List<ItemResponse> = |
| 44 | itemService.list().map { it.toResponse() } | 46 | itemService.list().map { it.toResponse() } |
| 45 | 47 | ||
| 46 | @GetMapping("/{id}") | 48 | @GetMapping("/{id}") |
| 49 | + @RequirePermission("catalog.item.read") | ||
| 47 | fun get(@PathVariable id: UUID): ResponseEntity<ItemResponse> { | 50 | fun get(@PathVariable id: UUID): ResponseEntity<ItemResponse> { |
| 48 | val item = itemService.findById(id) ?: return ResponseEntity.notFound().build() | 51 | val item = itemService.findById(id) ?: return ResponseEntity.notFound().build() |
| 49 | return ResponseEntity.ok(item.toResponse()) | 52 | return ResponseEntity.ok(item.toResponse()) |
| 50 | } | 53 | } |
| 51 | 54 | ||
| 52 | @GetMapping("/by-code/{code}") | 55 | @GetMapping("/by-code/{code}") |
| 56 | + @RequirePermission("catalog.item.read") | ||
| 53 | fun getByCode(@PathVariable code: String): ResponseEntity<ItemResponse> { | 57 | fun getByCode(@PathVariable code: String): ResponseEntity<ItemResponse> { |
| 54 | val item = itemService.findByCode(code) ?: return ResponseEntity.notFound().build() | 58 | val item = itemService.findByCode(code) ?: return ResponseEntity.notFound().build() |
| 55 | return ResponseEntity.ok(item.toResponse()) | 59 | return ResponseEntity.ok(item.toResponse()) |
| @@ -57,6 +61,7 @@ class ItemController( | @@ -57,6 +61,7 @@ class ItemController( | ||
| 57 | 61 | ||
| 58 | @PostMapping | 62 | @PostMapping |
| 59 | @ResponseStatus(HttpStatus.CREATED) | 63 | @ResponseStatus(HttpStatus.CREATED) |
| 64 | + @RequirePermission("catalog.item.create") | ||
| 60 | fun create(@RequestBody @Valid request: CreateItemRequest): ItemResponse = | 65 | fun create(@RequestBody @Valid request: CreateItemRequest): ItemResponse = |
| 61 | itemService.create( | 66 | itemService.create( |
| 62 | CreateItemCommand( | 67 | CreateItemCommand( |
| @@ -70,6 +75,7 @@ class ItemController( | @@ -70,6 +75,7 @@ class ItemController( | ||
| 70 | ).toResponse() | 75 | ).toResponse() |
| 71 | 76 | ||
| 72 | @PatchMapping("/{id}") | 77 | @PatchMapping("/{id}") |
| 78 | + @RequirePermission("catalog.item.update") | ||
| 73 | fun update( | 79 | fun update( |
| 74 | @PathVariable id: UUID, | 80 | @PathVariable id: UUID, |
| 75 | @RequestBody @Valid request: UpdateItemRequest, | 81 | @RequestBody @Valid request: UpdateItemRequest, |
| @@ -86,6 +92,7 @@ class ItemController( | @@ -86,6 +92,7 @@ class ItemController( | ||
| 86 | 92 | ||
| 87 | @DeleteMapping("/{id}") | 93 | @DeleteMapping("/{id}") |
| 88 | @ResponseStatus(HttpStatus.NO_CONTENT) | 94 | @ResponseStatus(HttpStatus.NO_CONTENT) |
| 95 | + @RequirePermission("catalog.item.deactivate") | ||
| 89 | fun deactivate(@PathVariable id: UUID) { | 96 | fun deactivate(@PathVariable id: UUID) { |
| 90 | itemService.deactivate(id) | 97 | itemService.deactivate(id) |
| 91 | } | 98 | } |
pbc/pbc-catalog/src/main/kotlin/org/vibeerp/pbc/catalog/http/UomController.kt
| @@ -17,6 +17,7 @@ import org.vibeerp.pbc.catalog.application.CreateUomCommand | @@ -17,6 +17,7 @@ import org.vibeerp.pbc.catalog.application.CreateUomCommand | ||
| 17 | import org.vibeerp.pbc.catalog.application.UomService | 17 | import org.vibeerp.pbc.catalog.application.UomService |
| 18 | import org.vibeerp.pbc.catalog.application.UpdateUomCommand | 18 | import org.vibeerp.pbc.catalog.application.UpdateUomCommand |
| 19 | import org.vibeerp.pbc.catalog.domain.Uom | 19 | import org.vibeerp.pbc.catalog.domain.Uom |
| 20 | +import org.vibeerp.platform.security.authz.RequirePermission | ||
| 20 | import java.util.UUID | 21 | import java.util.UUID |
| 21 | 22 | ||
| 22 | /** | 23 | /** |
| @@ -38,16 +39,19 @@ class UomController( | @@ -38,16 +39,19 @@ class UomController( | ||
| 38 | ) { | 39 | ) { |
| 39 | 40 | ||
| 40 | @GetMapping | 41 | @GetMapping |
| 42 | + @RequirePermission("catalog.uom.read") | ||
| 41 | fun list(): List<UomResponse> = | 43 | fun list(): List<UomResponse> = |
| 42 | uomService.list().map { it.toResponse() } | 44 | uomService.list().map { it.toResponse() } |
| 43 | 45 | ||
| 44 | @GetMapping("/{id}") | 46 | @GetMapping("/{id}") |
| 47 | + @RequirePermission("catalog.uom.read") | ||
| 45 | fun get(@PathVariable id: UUID): ResponseEntity<UomResponse> { | 48 | fun get(@PathVariable id: UUID): ResponseEntity<UomResponse> { |
| 46 | val uom = uomService.findById(id) ?: return ResponseEntity.notFound().build() | 49 | val uom = uomService.findById(id) ?: return ResponseEntity.notFound().build() |
| 47 | return ResponseEntity.ok(uom.toResponse()) | 50 | return ResponseEntity.ok(uom.toResponse()) |
| 48 | } | 51 | } |
| 49 | 52 | ||
| 50 | @GetMapping("/by-code/{code}") | 53 | @GetMapping("/by-code/{code}") |
| 54 | + @RequirePermission("catalog.uom.read") | ||
| 51 | fun getByCode(@PathVariable code: String): ResponseEntity<UomResponse> { | 55 | fun getByCode(@PathVariable code: String): ResponseEntity<UomResponse> { |
| 52 | val uom = uomService.findByCode(code) ?: return ResponseEntity.notFound().build() | 56 | val uom = uomService.findByCode(code) ?: return ResponseEntity.notFound().build() |
| 53 | return ResponseEntity.ok(uom.toResponse()) | 57 | return ResponseEntity.ok(uom.toResponse()) |
| @@ -55,6 +59,7 @@ class UomController( | @@ -55,6 +59,7 @@ class UomController( | ||
| 55 | 59 | ||
| 56 | @PostMapping | 60 | @PostMapping |
| 57 | @ResponseStatus(HttpStatus.CREATED) | 61 | @ResponseStatus(HttpStatus.CREATED) |
| 62 | + @RequirePermission("catalog.uom.create") | ||
| 58 | fun create(@RequestBody @Valid request: CreateUomRequest): UomResponse = | 63 | fun create(@RequestBody @Valid request: CreateUomRequest): UomResponse = |
| 59 | uomService.create( | 64 | uomService.create( |
| 60 | CreateUomCommand( | 65 | CreateUomCommand( |
| @@ -65,6 +70,7 @@ class UomController( | @@ -65,6 +70,7 @@ class UomController( | ||
| 65 | ).toResponse() | 70 | ).toResponse() |
| 66 | 71 | ||
| 67 | @PatchMapping("/{id}") | 72 | @PatchMapping("/{id}") |
| 73 | + @RequirePermission("catalog.uom.update") | ||
| 68 | fun update( | 74 | fun update( |
| 69 | @PathVariable id: UUID, | 75 | @PathVariable id: UUID, |
| 70 | @RequestBody @Valid request: UpdateUomRequest, | 76 | @RequestBody @Valid request: UpdateUomRequest, |
pbc/pbc-partners/src/main/kotlin/org/vibeerp/pbc/partners/http/AddressController.kt
| @@ -19,6 +19,7 @@ import org.vibeerp.pbc.partners.application.CreateAddressCommand | @@ -19,6 +19,7 @@ import org.vibeerp.pbc.partners.application.CreateAddressCommand | ||
| 19 | import org.vibeerp.pbc.partners.application.UpdateAddressCommand | 19 | import org.vibeerp.pbc.partners.application.UpdateAddressCommand |
| 20 | import org.vibeerp.pbc.partners.domain.Address | 20 | import org.vibeerp.pbc.partners.domain.Address |
| 21 | import org.vibeerp.pbc.partners.domain.AddressType | 21 | import org.vibeerp.pbc.partners.domain.AddressType |
| 22 | +import org.vibeerp.platform.security.authz.RequirePermission | ||
| 22 | import java.util.UUID | 23 | import java.util.UUID |
| 23 | 24 | ||
| 24 | /** | 25 | /** |
| @@ -38,10 +39,12 @@ class AddressController( | @@ -38,10 +39,12 @@ class AddressController( | ||
| 38 | ) { | 39 | ) { |
| 39 | 40 | ||
| 40 | @GetMapping | 41 | @GetMapping |
| 42 | + @RequirePermission("partners.address.read") | ||
| 41 | fun list(@PathVariable partnerId: UUID): List<AddressResponse> = | 43 | fun list(@PathVariable partnerId: UUID): List<AddressResponse> = |
| 42 | addressService.listFor(partnerId).map { it.toResponse() } | 44 | addressService.listFor(partnerId).map { it.toResponse() } |
| 43 | 45 | ||
| 44 | @GetMapping("/{id}") | 46 | @GetMapping("/{id}") |
| 47 | + @RequirePermission("partners.address.read") | ||
| 45 | fun get( | 48 | fun get( |
| 46 | @PathVariable partnerId: UUID, | 49 | @PathVariable partnerId: UUID, |
| 47 | @PathVariable id: UUID, | 50 | @PathVariable id: UUID, |
| @@ -53,6 +56,7 @@ class AddressController( | @@ -53,6 +56,7 @@ class AddressController( | ||
| 53 | 56 | ||
| 54 | @PostMapping | 57 | @PostMapping |
| 55 | @ResponseStatus(HttpStatus.CREATED) | 58 | @ResponseStatus(HttpStatus.CREATED) |
| 59 | + @RequirePermission("partners.address.create") | ||
| 56 | fun create( | 60 | fun create( |
| 57 | @PathVariable partnerId: UUID, | 61 | @PathVariable partnerId: UUID, |
| 58 | @RequestBody @Valid request: CreateAddressRequest, | 62 | @RequestBody @Valid request: CreateAddressRequest, |
| @@ -72,6 +76,7 @@ class AddressController( | @@ -72,6 +76,7 @@ class AddressController( | ||
| 72 | ).toResponse() | 76 | ).toResponse() |
| 73 | 77 | ||
| 74 | @PatchMapping("/{id}") | 78 | @PatchMapping("/{id}") |
| 79 | + @RequirePermission("partners.address.update") | ||
| 75 | fun update( | 80 | fun update( |
| 76 | @PathVariable partnerId: UUID, | 81 | @PathVariable partnerId: UUID, |
| 77 | @PathVariable id: UUID, | 82 | @PathVariable id: UUID, |
| @@ -93,6 +98,7 @@ class AddressController( | @@ -93,6 +98,7 @@ class AddressController( | ||
| 93 | 98 | ||
| 94 | @DeleteMapping("/{id}") | 99 | @DeleteMapping("/{id}") |
| 95 | @ResponseStatus(HttpStatus.NO_CONTENT) | 100 | @ResponseStatus(HttpStatus.NO_CONTENT) |
| 101 | + @RequirePermission("partners.address.delete") | ||
| 96 | fun delete( | 102 | fun delete( |
| 97 | @PathVariable partnerId: UUID, | 103 | @PathVariable partnerId: UUID, |
| 98 | @PathVariable id: UUID, | 104 | @PathVariable id: UUID, |
pbc/pbc-partners/src/main/kotlin/org/vibeerp/pbc/partners/http/ContactController.kt
| @@ -18,6 +18,7 @@ import org.vibeerp.pbc.partners.application.ContactService | @@ -18,6 +18,7 @@ import org.vibeerp.pbc.partners.application.ContactService | ||
| 18 | import org.vibeerp.pbc.partners.application.CreateContactCommand | 18 | import org.vibeerp.pbc.partners.application.CreateContactCommand |
| 19 | import org.vibeerp.pbc.partners.application.UpdateContactCommand | 19 | import org.vibeerp.pbc.partners.application.UpdateContactCommand |
| 20 | import org.vibeerp.pbc.partners.domain.Contact | 20 | import org.vibeerp.pbc.partners.domain.Contact |
| 21 | +import org.vibeerp.platform.security.authz.RequirePermission | ||
| 21 | import java.util.UUID | 22 | import java.util.UUID |
| 22 | 23 | ||
| 23 | /** | 24 | /** |
| @@ -28,11 +29,12 @@ import java.util.UUID | @@ -28,11 +29,12 @@ import java.util.UUID | ||
| 28 | * relationship — every operation is implicitly scoped to a specific | 29 | * relationship — every operation is implicitly scoped to a specific |
| 29 | * partner. | 30 | * partner. |
| 30 | * | 31 | * |
| 31 | - * **PII boundary.** Contact data is personal information. Once the | ||
| 32 | - * permission system lands (P4.3) this controller will be guarded by | ||
| 33 | - * `partners.contact.read` etc. Until then, plain authentication is the | ||
| 34 | - * only gate, and that is a known short-term posture, NOT the long-term | ||
| 35 | - * one — see the metadata YAML for the planned permission keys. | 32 | + * **PII boundary.** Contact data is personal information. Every |
| 33 | + * endpoint is guarded by a dedicated `partners.contact.*` permission | ||
| 34 | + * so role admin can grant read/write access independently of the | ||
| 35 | + * wider `partners.partner.*` permissions. This is the stronger | ||
| 36 | + * posture that the earlier "TODO: annotate once P4.3 lands" note | ||
| 37 | + * referred to; P4.3 is live and the annotations are now in place. | ||
| 36 | */ | 38 | */ |
| 37 | @RestController | 39 | @RestController |
| 38 | @RequestMapping("/api/v1/partners/partners/{partnerId}/contacts") | 40 | @RequestMapping("/api/v1/partners/partners/{partnerId}/contacts") |
| @@ -41,10 +43,12 @@ class ContactController( | @@ -41,10 +43,12 @@ class ContactController( | ||
| 41 | ) { | 43 | ) { |
| 42 | 44 | ||
| 43 | @GetMapping | 45 | @GetMapping |
| 46 | + @RequirePermission("partners.contact.read") | ||
| 44 | fun list(@PathVariable partnerId: UUID): List<ContactResponse> = | 47 | fun list(@PathVariable partnerId: UUID): List<ContactResponse> = |
| 45 | contactService.listFor(partnerId).map { it.toResponse() } | 48 | contactService.listFor(partnerId).map { it.toResponse() } |
| 46 | 49 | ||
| 47 | @GetMapping("/{id}") | 50 | @GetMapping("/{id}") |
| 51 | + @RequirePermission("partners.contact.read") | ||
| 48 | fun get( | 52 | fun get( |
| 49 | @PathVariable partnerId: UUID, | 53 | @PathVariable partnerId: UUID, |
| 50 | @PathVariable id: UUID, | 54 | @PathVariable id: UUID, |
| @@ -56,6 +60,7 @@ class ContactController( | @@ -56,6 +60,7 @@ class ContactController( | ||
| 56 | 60 | ||
| 57 | @PostMapping | 61 | @PostMapping |
| 58 | @ResponseStatus(HttpStatus.CREATED) | 62 | @ResponseStatus(HttpStatus.CREATED) |
| 63 | + @RequirePermission("partners.contact.create") | ||
| 59 | fun create( | 64 | fun create( |
| 60 | @PathVariable partnerId: UUID, | 65 | @PathVariable partnerId: UUID, |
| 61 | @RequestBody @Valid request: CreateContactRequest, | 66 | @RequestBody @Valid request: CreateContactRequest, |
| @@ -72,6 +77,7 @@ class ContactController( | @@ -72,6 +77,7 @@ class ContactController( | ||
| 72 | ).toResponse() | 77 | ).toResponse() |
| 73 | 78 | ||
| 74 | @PatchMapping("/{id}") | 79 | @PatchMapping("/{id}") |
| 80 | + @RequirePermission("partners.contact.update") | ||
| 75 | fun update( | 81 | fun update( |
| 76 | @PathVariable partnerId: UUID, | 82 | @PathVariable partnerId: UUID, |
| 77 | @PathVariable id: UUID, | 83 | @PathVariable id: UUID, |
| @@ -90,6 +96,7 @@ class ContactController( | @@ -90,6 +96,7 @@ class ContactController( | ||
| 90 | 96 | ||
| 91 | @DeleteMapping("/{id}") | 97 | @DeleteMapping("/{id}") |
| 92 | @ResponseStatus(HttpStatus.NO_CONTENT) | 98 | @ResponseStatus(HttpStatus.NO_CONTENT) |
| 99 | + @RequirePermission("partners.contact.deactivate") | ||
| 93 | fun deactivate( | 100 | fun deactivate( |
| 94 | @PathVariable partnerId: UUID, | 101 | @PathVariable partnerId: UUID, |
| 95 | @PathVariable id: UUID, | 102 | @PathVariable id: UUID, |
pbc/pbc-partners/src/main/kotlin/org/vibeerp/pbc/partners/http/PartnerController.kt
| @@ -40,16 +40,19 @@ class PartnerController( | @@ -40,16 +40,19 @@ class PartnerController( | ||
| 40 | ) { | 40 | ) { |
| 41 | 41 | ||
| 42 | @GetMapping | 42 | @GetMapping |
| 43 | + @RequirePermission("partners.partner.read") | ||
| 43 | fun list(): List<PartnerResponse> = | 44 | fun list(): List<PartnerResponse> = |
| 44 | partnerService.list().map { it.toResponse(partnerService) } | 45 | partnerService.list().map { it.toResponse(partnerService) } |
| 45 | 46 | ||
| 46 | @GetMapping("/{id}") | 47 | @GetMapping("/{id}") |
| 48 | + @RequirePermission("partners.partner.read") | ||
| 47 | fun get(@PathVariable id: UUID): ResponseEntity<PartnerResponse> { | 49 | fun get(@PathVariable id: UUID): ResponseEntity<PartnerResponse> { |
| 48 | val partner = partnerService.findById(id) ?: return ResponseEntity.notFound().build() | 50 | val partner = partnerService.findById(id) ?: return ResponseEntity.notFound().build() |
| 49 | return ResponseEntity.ok(partner.toResponse(partnerService)) | 51 | return ResponseEntity.ok(partner.toResponse(partnerService)) |
| 50 | } | 52 | } |
| 51 | 53 | ||
| 52 | @GetMapping("/by-code/{code}") | 54 | @GetMapping("/by-code/{code}") |
| 55 | + @RequirePermission("partners.partner.read") | ||
| 53 | fun getByCode(@PathVariable code: String): ResponseEntity<PartnerResponse> { | 56 | fun getByCode(@PathVariable code: String): ResponseEntity<PartnerResponse> { |
| 54 | val partner = partnerService.findByCode(code) ?: return ResponseEntity.notFound().build() | 57 | val partner = partnerService.findByCode(code) ?: return ResponseEntity.notFound().build() |
| 55 | return ResponseEntity.ok(partner.toResponse(partnerService)) | 58 | return ResponseEntity.ok(partner.toResponse(partnerService)) |
| @@ -57,6 +60,7 @@ class PartnerController( | @@ -57,6 +60,7 @@ class PartnerController( | ||
| 57 | 60 | ||
| 58 | @PostMapping | 61 | @PostMapping |
| 59 | @ResponseStatus(HttpStatus.CREATED) | 62 | @ResponseStatus(HttpStatus.CREATED) |
| 63 | + @RequirePermission("partners.partner.create") | ||
| 60 | fun create(@RequestBody @Valid request: CreatePartnerRequest): PartnerResponse = | 64 | fun create(@RequestBody @Valid request: CreatePartnerRequest): PartnerResponse = |
| 61 | partnerService.create( | 65 | partnerService.create( |
| 62 | CreatePartnerCommand( | 66 | CreatePartnerCommand( |
| @@ -73,6 +77,7 @@ class PartnerController( | @@ -73,6 +77,7 @@ class PartnerController( | ||
| 73 | ).toResponse(partnerService) | 77 | ).toResponse(partnerService) |
| 74 | 78 | ||
| 75 | @PatchMapping("/{id}") | 79 | @PatchMapping("/{id}") |
| 80 | + @RequirePermission("partners.partner.update") | ||
| 76 | fun update( | 81 | fun update( |
| 77 | @PathVariable id: UUID, | 82 | @PathVariable id: UUID, |
| 78 | @RequestBody @Valid request: UpdatePartnerRequest, | 83 | @RequestBody @Valid request: UpdatePartnerRequest, |