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,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,