Commit da386cc7480a3e48a360f938602d7cf42150fc46

Authored by zichun
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.
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,
... ...