Commit da386cc7480a3e48a360f938602d7cf42150fc46
1 parent
4f42d270
feat(security): annotate inventory + orders list/get/create/update endpoints
Completes the @RequirePermission rollout that started in commit b174cf60. Every non-state-transition endpoint in pbc-inventory (Location CRUD), pbc-orders-sales, and pbc-orders-purchase is now guarded by the pre-declared permission keys from their respective metadata YAMLs. State-transition verbs (confirm/cancel/ship/receive) were annotated in the original P4.3 demo chunk; this one fills in the list/get/create/update gap. Inventory - LocationController: list/get/getByCode → inventory.location.read; create → inventory.location.create; update → inventory.location.update; deactivate → inventory.location.deactivate. - (StockBalanceController.adjust + StockMovementController.record were already annotated with inventory.stock.adjust.) Orders-sales - SalesOrderController: list/get/getByCode → orders.sales.read; create → orders.sales.create; update → orders.sales.update. (confirm/cancel/ship were already annotated.) Orders-purchase - PurchaseOrderController: list/get/getByCode → orders.purchase.read; create → orders.purchase.create; update → orders.purchase.update. (confirm/cancel/receive were already annotated.) No new permission keys. Every key this chunk consumes was already declared in the relevant metadata YAML since the respective PBC was first built — catalog + partners already shipped in this state, and the inventory/orders YAMLs declared their read/create/update keys from day one but the controllers hadn't started using them. Admin happy path still works (bootstrap admin has the wildcard `admin` role, same as after commit b174cf60). 230 unit tests still green — annotations are purely additive, no existing test hits the @RequirePermission path since service-level tests bypass the controller entirely. Combined with b174cf60, the framework now has full @RequirePermission coverage on every PBC controller except pbc-identity's user admin (which is a separate permission surface — user/role administration has its own security story). A minimum-privilege role like "sales-clerk" can now be granted exactly `orders.sales.read` + `orders.sales.create` + `partners.partner.read` and NOT accidentally see catalog admin, inventory movements, finance journals, or contact PII.
Showing
3 changed files
with
17 additions
and
0 deletions
pbc/pbc-inventory/src/main/kotlin/org/vibeerp/pbc/inventory/http/LocationController.kt
| ... | ... | @@ -19,6 +19,7 @@ import org.vibeerp.pbc.inventory.application.LocationService |
| 19 | 19 | import org.vibeerp.pbc.inventory.application.UpdateLocationCommand |
| 20 | 20 | import org.vibeerp.pbc.inventory.domain.Location |
| 21 | 21 | import org.vibeerp.pbc.inventory.domain.LocationType |
| 22 | +import org.vibeerp.platform.security.authz.RequirePermission | |
| 22 | 23 | import java.util.UUID |
| 23 | 24 | |
| 24 | 25 | /** |
| ... | ... | @@ -33,16 +34,19 @@ class LocationController( |
| 33 | 34 | ) { |
| 34 | 35 | |
| 35 | 36 | @GetMapping |
| 37 | + @RequirePermission("inventory.location.read") | |
| 36 | 38 | fun list(): List<LocationResponse> = |
| 37 | 39 | locationService.list().map { it.toResponse(locationService) } |
| 38 | 40 | |
| 39 | 41 | @GetMapping("/{id}") |
| 42 | + @RequirePermission("inventory.location.read") | |
| 40 | 43 | fun get(@PathVariable id: UUID): ResponseEntity<LocationResponse> { |
| 41 | 44 | val location = locationService.findById(id) ?: return ResponseEntity.notFound().build() |
| 42 | 45 | return ResponseEntity.ok(location.toResponse(locationService)) |
| 43 | 46 | } |
| 44 | 47 | |
| 45 | 48 | @GetMapping("/by-code/{code}") |
| 49 | + @RequirePermission("inventory.location.read") | |
| 46 | 50 | fun getByCode(@PathVariable code: String): ResponseEntity<LocationResponse> { |
| 47 | 51 | val location = locationService.findByCode(code) ?: return ResponseEntity.notFound().build() |
| 48 | 52 | return ResponseEntity.ok(location.toResponse(locationService)) |
| ... | ... | @@ -50,6 +54,7 @@ class LocationController( |
| 50 | 54 | |
| 51 | 55 | @PostMapping |
| 52 | 56 | @ResponseStatus(HttpStatus.CREATED) |
| 57 | + @RequirePermission("inventory.location.create") | |
| 53 | 58 | fun create(@RequestBody @Valid request: CreateLocationRequest): LocationResponse = |
| 54 | 59 | locationService.create( |
| 55 | 60 | CreateLocationCommand( |
| ... | ... | @@ -62,6 +67,7 @@ class LocationController( |
| 62 | 67 | ).toResponse(locationService) |
| 63 | 68 | |
| 64 | 69 | @PatchMapping("/{id}") |
| 70 | + @RequirePermission("inventory.location.update") | |
| 65 | 71 | fun update( |
| 66 | 72 | @PathVariable id: UUID, |
| 67 | 73 | @RequestBody @Valid request: UpdateLocationRequest, |
| ... | ... | @@ -78,6 +84,7 @@ class LocationController( |
| 78 | 84 | |
| 79 | 85 | @DeleteMapping("/{id}") |
| 80 | 86 | @ResponseStatus(HttpStatus.NO_CONTENT) |
| 87 | + @RequirePermission("inventory.location.deactivate") | |
| 81 | 88 | fun deactivate(@PathVariable id: UUID) { |
| 82 | 89 | locationService.deactivate(id) |
| 83 | 90 | } | ... | ... |
pbc/pbc-orders-purchase/src/main/kotlin/org/vibeerp/pbc/orders/purchase/http/PurchaseOrderController.kt
| ... | ... | @@ -43,16 +43,19 @@ class PurchaseOrderController( |
| 43 | 43 | ) { |
| 44 | 44 | |
| 45 | 45 | @GetMapping |
| 46 | + @RequirePermission("orders.purchase.read") | |
| 46 | 47 | fun list(): List<PurchaseOrderResponse> = |
| 47 | 48 | purchaseOrderService.list().map { it.toResponse(purchaseOrderService) } |
| 48 | 49 | |
| 49 | 50 | @GetMapping("/{id}") |
| 51 | + @RequirePermission("orders.purchase.read") | |
| 50 | 52 | fun get(@PathVariable id: UUID): ResponseEntity<PurchaseOrderResponse> { |
| 51 | 53 | val order = purchaseOrderService.findById(id) ?: return ResponseEntity.notFound().build() |
| 52 | 54 | return ResponseEntity.ok(order.toResponse(purchaseOrderService)) |
| 53 | 55 | } |
| 54 | 56 | |
| 55 | 57 | @GetMapping("/by-code/{code}") |
| 58 | + @RequirePermission("orders.purchase.read") | |
| 56 | 59 | fun getByCode(@PathVariable code: String): ResponseEntity<PurchaseOrderResponse> { |
| 57 | 60 | val order = purchaseOrderService.findByCode(code) ?: return ResponseEntity.notFound().build() |
| 58 | 61 | return ResponseEntity.ok(order.toResponse(purchaseOrderService)) |
| ... | ... | @@ -60,6 +63,7 @@ class PurchaseOrderController( |
| 60 | 63 | |
| 61 | 64 | @PostMapping |
| 62 | 65 | @ResponseStatus(HttpStatus.CREATED) |
| 66 | + @RequirePermission("orders.purchase.create") | |
| 63 | 67 | fun create(@RequestBody @Valid request: CreatePurchaseOrderRequest): PurchaseOrderResponse = |
| 64 | 68 | purchaseOrderService.create( |
| 65 | 69 | CreatePurchaseOrderCommand( |
| ... | ... | @@ -74,6 +78,7 @@ class PurchaseOrderController( |
| 74 | 78 | ).toResponse(purchaseOrderService) |
| 75 | 79 | |
| 76 | 80 | @PatchMapping("/{id}") |
| 81 | + @RequirePermission("orders.purchase.update") | |
| 77 | 82 | fun update( |
| 78 | 83 | @PathVariable id: UUID, |
| 79 | 84 | @RequestBody @Valid request: UpdatePurchaseOrderRequest, | ... | ... |
pbc/pbc-orders-sales/src/main/kotlin/org/vibeerp/pbc/orders/sales/http/SalesOrderController.kt
| ... | ... | @@ -54,16 +54,19 @@ class SalesOrderController( |
| 54 | 54 | ) { |
| 55 | 55 | |
| 56 | 56 | @GetMapping |
| 57 | + @RequirePermission("orders.sales.read") | |
| 57 | 58 | fun list(): List<SalesOrderResponse> = |
| 58 | 59 | salesOrderService.list().map { it.toResponse(salesOrderService) } |
| 59 | 60 | |
| 60 | 61 | @GetMapping("/{id}") |
| 62 | + @RequirePermission("orders.sales.read") | |
| 61 | 63 | fun get(@PathVariable id: UUID): ResponseEntity<SalesOrderResponse> { |
| 62 | 64 | val order = salesOrderService.findById(id) ?: return ResponseEntity.notFound().build() |
| 63 | 65 | return ResponseEntity.ok(order.toResponse(salesOrderService)) |
| 64 | 66 | } |
| 65 | 67 | |
| 66 | 68 | @GetMapping("/by-code/{code}") |
| 69 | + @RequirePermission("orders.sales.read") | |
| 67 | 70 | fun getByCode(@PathVariable code: String): ResponseEntity<SalesOrderResponse> { |
| 68 | 71 | val order = salesOrderService.findByCode(code) ?: return ResponseEntity.notFound().build() |
| 69 | 72 | return ResponseEntity.ok(order.toResponse(salesOrderService)) |
| ... | ... | @@ -71,6 +74,7 @@ class SalesOrderController( |
| 71 | 74 | |
| 72 | 75 | @PostMapping |
| 73 | 76 | @ResponseStatus(HttpStatus.CREATED) |
| 77 | + @RequirePermission("orders.sales.create") | |
| 74 | 78 | fun create(@RequestBody @Valid request: CreateSalesOrderRequest): SalesOrderResponse = |
| 75 | 79 | salesOrderService.create( |
| 76 | 80 | CreateSalesOrderCommand( |
| ... | ... | @@ -84,6 +88,7 @@ class SalesOrderController( |
| 84 | 88 | ).toResponse(salesOrderService) |
| 85 | 89 | |
| 86 | 90 | @PatchMapping("/{id}") |
| 91 | + @RequirePermission("orders.sales.update") | |
| 87 | 92 | fun update( |
| 88 | 93 | @PathVariable id: UUID, |
| 89 | 94 | @RequestBody @Valid request: UpdateSalesOrderRequest, | ... | ... |