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,6 +19,7 @@ import org.vibeerp.pbc.inventory.application.LocationService | ||
| 19 | import org.vibeerp.pbc.inventory.application.UpdateLocationCommand | 19 | import org.vibeerp.pbc.inventory.application.UpdateLocationCommand |
| 20 | import org.vibeerp.pbc.inventory.domain.Location | 20 | import org.vibeerp.pbc.inventory.domain.Location |
| 21 | import org.vibeerp.pbc.inventory.domain.LocationType | 21 | import org.vibeerp.pbc.inventory.domain.LocationType |
| 22 | +import org.vibeerp.platform.security.authz.RequirePermission | ||
| 22 | import java.util.UUID | 23 | import java.util.UUID |
| 23 | 24 | ||
| 24 | /** | 25 | /** |
| @@ -33,16 +34,19 @@ class LocationController( | @@ -33,16 +34,19 @@ class LocationController( | ||
| 33 | ) { | 34 | ) { |
| 34 | 35 | ||
| 35 | @GetMapping | 36 | @GetMapping |
| 37 | + @RequirePermission("inventory.location.read") | ||
| 36 | fun list(): List<LocationResponse> = | 38 | fun list(): List<LocationResponse> = |
| 37 | locationService.list().map { it.toResponse(locationService) } | 39 | locationService.list().map { it.toResponse(locationService) } |
| 38 | 40 | ||
| 39 | @GetMapping("/{id}") | 41 | @GetMapping("/{id}") |
| 42 | + @RequirePermission("inventory.location.read") | ||
| 40 | fun get(@PathVariable id: UUID): ResponseEntity<LocationResponse> { | 43 | fun get(@PathVariable id: UUID): ResponseEntity<LocationResponse> { |
| 41 | val location = locationService.findById(id) ?: return ResponseEntity.notFound().build() | 44 | val location = locationService.findById(id) ?: return ResponseEntity.notFound().build() |
| 42 | return ResponseEntity.ok(location.toResponse(locationService)) | 45 | return ResponseEntity.ok(location.toResponse(locationService)) |
| 43 | } | 46 | } |
| 44 | 47 | ||
| 45 | @GetMapping("/by-code/{code}") | 48 | @GetMapping("/by-code/{code}") |
| 49 | + @RequirePermission("inventory.location.read") | ||
| 46 | fun getByCode(@PathVariable code: String): ResponseEntity<LocationResponse> { | 50 | fun getByCode(@PathVariable code: String): ResponseEntity<LocationResponse> { |
| 47 | val location = locationService.findByCode(code) ?: return ResponseEntity.notFound().build() | 51 | val location = locationService.findByCode(code) ?: return ResponseEntity.notFound().build() |
| 48 | return ResponseEntity.ok(location.toResponse(locationService)) | 52 | return ResponseEntity.ok(location.toResponse(locationService)) |
| @@ -50,6 +54,7 @@ class LocationController( | @@ -50,6 +54,7 @@ class LocationController( | ||
| 50 | 54 | ||
| 51 | @PostMapping | 55 | @PostMapping |
| 52 | @ResponseStatus(HttpStatus.CREATED) | 56 | @ResponseStatus(HttpStatus.CREATED) |
| 57 | + @RequirePermission("inventory.location.create") | ||
| 53 | fun create(@RequestBody @Valid request: CreateLocationRequest): LocationResponse = | 58 | fun create(@RequestBody @Valid request: CreateLocationRequest): LocationResponse = |
| 54 | locationService.create( | 59 | locationService.create( |
| 55 | CreateLocationCommand( | 60 | CreateLocationCommand( |
| @@ -62,6 +67,7 @@ class LocationController( | @@ -62,6 +67,7 @@ class LocationController( | ||
| 62 | ).toResponse(locationService) | 67 | ).toResponse(locationService) |
| 63 | 68 | ||
| 64 | @PatchMapping("/{id}") | 69 | @PatchMapping("/{id}") |
| 70 | + @RequirePermission("inventory.location.update") | ||
| 65 | fun update( | 71 | fun update( |
| 66 | @PathVariable id: UUID, | 72 | @PathVariable id: UUID, |
| 67 | @RequestBody @Valid request: UpdateLocationRequest, | 73 | @RequestBody @Valid request: UpdateLocationRequest, |
| @@ -78,6 +84,7 @@ class LocationController( | @@ -78,6 +84,7 @@ class LocationController( | ||
| 78 | 84 | ||
| 79 | @DeleteMapping("/{id}") | 85 | @DeleteMapping("/{id}") |
| 80 | @ResponseStatus(HttpStatus.NO_CONTENT) | 86 | @ResponseStatus(HttpStatus.NO_CONTENT) |
| 87 | + @RequirePermission("inventory.location.deactivate") | ||
| 81 | fun deactivate(@PathVariable id: UUID) { | 88 | fun deactivate(@PathVariable id: UUID) { |
| 82 | locationService.deactivate(id) | 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,16 +43,19 @@ class PurchaseOrderController( | ||
| 43 | ) { | 43 | ) { |
| 44 | 44 | ||
| 45 | @GetMapping | 45 | @GetMapping |
| 46 | + @RequirePermission("orders.purchase.read") | ||
| 46 | fun list(): List<PurchaseOrderResponse> = | 47 | fun list(): List<PurchaseOrderResponse> = |
| 47 | purchaseOrderService.list().map { it.toResponse(purchaseOrderService) } | 48 | purchaseOrderService.list().map { it.toResponse(purchaseOrderService) } |
| 48 | 49 | ||
| 49 | @GetMapping("/{id}") | 50 | @GetMapping("/{id}") |
| 51 | + @RequirePermission("orders.purchase.read") | ||
| 50 | fun get(@PathVariable id: UUID): ResponseEntity<PurchaseOrderResponse> { | 52 | fun get(@PathVariable id: UUID): ResponseEntity<PurchaseOrderResponse> { |
| 51 | val order = purchaseOrderService.findById(id) ?: return ResponseEntity.notFound().build() | 53 | val order = purchaseOrderService.findById(id) ?: return ResponseEntity.notFound().build() |
| 52 | return ResponseEntity.ok(order.toResponse(purchaseOrderService)) | 54 | return ResponseEntity.ok(order.toResponse(purchaseOrderService)) |
| 53 | } | 55 | } |
| 54 | 56 | ||
| 55 | @GetMapping("/by-code/{code}") | 57 | @GetMapping("/by-code/{code}") |
| 58 | + @RequirePermission("orders.purchase.read") | ||
| 56 | fun getByCode(@PathVariable code: String): ResponseEntity<PurchaseOrderResponse> { | 59 | fun getByCode(@PathVariable code: String): ResponseEntity<PurchaseOrderResponse> { |
| 57 | val order = purchaseOrderService.findByCode(code) ?: return ResponseEntity.notFound().build() | 60 | val order = purchaseOrderService.findByCode(code) ?: return ResponseEntity.notFound().build() |
| 58 | return ResponseEntity.ok(order.toResponse(purchaseOrderService)) | 61 | return ResponseEntity.ok(order.toResponse(purchaseOrderService)) |
| @@ -60,6 +63,7 @@ class PurchaseOrderController( | @@ -60,6 +63,7 @@ class PurchaseOrderController( | ||
| 60 | 63 | ||
| 61 | @PostMapping | 64 | @PostMapping |
| 62 | @ResponseStatus(HttpStatus.CREATED) | 65 | @ResponseStatus(HttpStatus.CREATED) |
| 66 | + @RequirePermission("orders.purchase.create") | ||
| 63 | fun create(@RequestBody @Valid request: CreatePurchaseOrderRequest): PurchaseOrderResponse = | 67 | fun create(@RequestBody @Valid request: CreatePurchaseOrderRequest): PurchaseOrderResponse = |
| 64 | purchaseOrderService.create( | 68 | purchaseOrderService.create( |
| 65 | CreatePurchaseOrderCommand( | 69 | CreatePurchaseOrderCommand( |
| @@ -74,6 +78,7 @@ class PurchaseOrderController( | @@ -74,6 +78,7 @@ class PurchaseOrderController( | ||
| 74 | ).toResponse(purchaseOrderService) | 78 | ).toResponse(purchaseOrderService) |
| 75 | 79 | ||
| 76 | @PatchMapping("/{id}") | 80 | @PatchMapping("/{id}") |
| 81 | + @RequirePermission("orders.purchase.update") | ||
| 77 | fun update( | 82 | fun update( |
| 78 | @PathVariable id: UUID, | 83 | @PathVariable id: UUID, |
| 79 | @RequestBody @Valid request: UpdatePurchaseOrderRequest, | 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,16 +54,19 @@ class SalesOrderController( | ||
| 54 | ) { | 54 | ) { |
| 55 | 55 | ||
| 56 | @GetMapping | 56 | @GetMapping |
| 57 | + @RequirePermission("orders.sales.read") | ||
| 57 | fun list(): List<SalesOrderResponse> = | 58 | fun list(): List<SalesOrderResponse> = |
| 58 | salesOrderService.list().map { it.toResponse(salesOrderService) } | 59 | salesOrderService.list().map { it.toResponse(salesOrderService) } |
| 59 | 60 | ||
| 60 | @GetMapping("/{id}") | 61 | @GetMapping("/{id}") |
| 62 | + @RequirePermission("orders.sales.read") | ||
| 61 | fun get(@PathVariable id: UUID): ResponseEntity<SalesOrderResponse> { | 63 | fun get(@PathVariable id: UUID): ResponseEntity<SalesOrderResponse> { |
| 62 | val order = salesOrderService.findById(id) ?: return ResponseEntity.notFound().build() | 64 | val order = salesOrderService.findById(id) ?: return ResponseEntity.notFound().build() |
| 63 | return ResponseEntity.ok(order.toResponse(salesOrderService)) | 65 | return ResponseEntity.ok(order.toResponse(salesOrderService)) |
| 64 | } | 66 | } |
| 65 | 67 | ||
| 66 | @GetMapping("/by-code/{code}") | 68 | @GetMapping("/by-code/{code}") |
| 69 | + @RequirePermission("orders.sales.read") | ||
| 67 | fun getByCode(@PathVariable code: String): ResponseEntity<SalesOrderResponse> { | 70 | fun getByCode(@PathVariable code: String): ResponseEntity<SalesOrderResponse> { |
| 68 | val order = salesOrderService.findByCode(code) ?: return ResponseEntity.notFound().build() | 71 | val order = salesOrderService.findByCode(code) ?: return ResponseEntity.notFound().build() |
| 69 | return ResponseEntity.ok(order.toResponse(salesOrderService)) | 72 | return ResponseEntity.ok(order.toResponse(salesOrderService)) |
| @@ -71,6 +74,7 @@ class SalesOrderController( | @@ -71,6 +74,7 @@ class SalesOrderController( | ||
| 71 | 74 | ||
| 72 | @PostMapping | 75 | @PostMapping |
| 73 | @ResponseStatus(HttpStatus.CREATED) | 76 | @ResponseStatus(HttpStatus.CREATED) |
| 77 | + @RequirePermission("orders.sales.create") | ||
| 74 | fun create(@RequestBody @Valid request: CreateSalesOrderRequest): SalesOrderResponse = | 78 | fun create(@RequestBody @Valid request: CreateSalesOrderRequest): SalesOrderResponse = |
| 75 | salesOrderService.create( | 79 | salesOrderService.create( |
| 76 | CreateSalesOrderCommand( | 80 | CreateSalesOrderCommand( |
| @@ -84,6 +88,7 @@ class SalesOrderController( | @@ -84,6 +88,7 @@ class SalesOrderController( | ||
| 84 | ).toResponse(salesOrderService) | 88 | ).toResponse(salesOrderService) |
| 85 | 89 | ||
| 86 | @PatchMapping("/{id}") | 90 | @PatchMapping("/{id}") |
| 91 | + @RequirePermission("orders.sales.update") | ||
| 87 | fun update( | 92 | fun update( |
| 88 | @PathVariable id: UUID, | 93 | @PathVariable id: UUID, |
| 89 | @RequestBody @Valid request: UpdateSalesOrderRequest, | 94 | @RequestBody @Valid request: UpdateSalesOrderRequest, |