Acquisition Units

Overview

The notion of acquisitions units is being introduced to restrict a user's ability to CRUD records unless they belong to the unit associated with that record. This is applied in the business logic layer of the various acquisitions apps.

NOTE:  This functionality was at one point referred to as "Teams", so the two terms are generally synonymous within acquisitions.  

Here's an example: (UUIDs have been replaced with integers for brevity)

Acquisitions Units:

idnameprotectCreateprotectReadprotectUpdateprotectDelete
12345maintruefalsetrue

true

23456lawtruetruetruetrue


This means that:

  • Only members of the "main" acquisitions unit can create, update or delete records associated with the "main" acquisitions unit, but anyone can view those records.
  • Only members of the "law" acquisitions unit can create, read, update, or delete records associated with the "law" acquisitions unit. This includes searching and direct access via "get by id" endpoints.
  • Records not associated with an acquisitions unit can be created, read, updated, or deleted by anyone with the necessary permissions to call the related endpoints.

Memberships: (UUIDs have been replaced with integers for brevity)

id (UUID)userId (UUID)acquisitionsUnitId (UUID)
1119000 (Bob)12345
2229111 (Ben)23456
3339222 (Brenda)12345
4449222 (Brenda)

23456


This means that:

  • Bob belongs to the "main" acquisitions unit and:
    • Can do anything with records associated with "main"
    • Can do anything with records not associated with any acquisitions unit at all.
    • Can't do anything (even view) "law" records.
  • Ben belongs to the "law" acquisitions unit and:
    • Can do anything with "law" records
    • Can do anything with records not associated with any acquisitions unit at all
    • Can view/search for "main" records but can't create/edit/delete them
  • Brenda belongs to both "law" and "main" and:
    • Can do anything with "law" records
    • Can do anything with "main" records
    • Can do anything with records not associated with any acquisitions unit at all

Verbs/Actions apply as follows:

  • The verbs (CRUD) apply to the record being acted upon (PO, Piece, PoLine, etc.)
  • The acquisitions units are sometimes explicit (like PO) and sometimes implicit/inherited (like PoLine/Piece)
  • Even when dealing with records with implicit acquisitions unit assignment, the verbs still apply to the record being acted upon, not to the record in which the unit was inherited from

So during check-in, if I'm creating a piece, "Create" applies. If I'm in receiving and I'm updating a piece "Update" applies, even though in both cases you're logically "updating" the order

In case it wasn't clear, this logic is applied in conjunction with the existing FOLIO API level permissions (e.g. orders.order-lines.collection.get).  Acquisitions units control access at the data level.  The ability to CRUD a record associated with an acquisitions unit depends on having appropriate FOLIO permissions to use the corresponding API and acquisition unit assignment.

Conflicts and Precedence

It's possible for a record to be associated with more than one acquisitions unit.  This can lead to ambiguities in cases where those acquisitions units have different or conflicting protected operations.

For example, invoice X (recordId 9444) belongs to both "main" and "law" acquisitions units.  The latter protects read but the former doesn't.  So if I'm "Joe" and don't belong to either of those acquisitions units, should I be able to view the invoice or not?  What about if I'm 
"Bob" and belong to "main" but not "law"?

In these situations we perform an AND operation for each of the protect* fields across all of the acquisitions units associated with the record.  This in effect equates to "the least restrictive wins". 

Acq. UnitprotectCreateprotectReadprotectUpdateprotectDelete
maintruefalsetruetrue
lawtruetruetruetrue
Effectivetruefalsetruetrue
  • Joe, not belonging to either acquisitions unit can only view this record
  • Bob, Ben, and Brenda all belong to at least one of the acquisitions units and can therefore perform any actions on this record.

See the Appendix for the other options considered.

APIs

New APIs are needed in order to manage acquisition units and their assignments.  These APIs will live under new interfaces "acquisitions-units" and "acquisitions-unit-storage".  For now these interfaces will be implemented in mod-orders/mod-orders-storage, but eventually may be split out into a separate module since acquisitions units are not specific to orders.  

The usual pattern we follow of defining APIs in both the storage and business logic modules is used.  Clients should only interact with the business logic module.

Additional details can be found in the API listing document (orders tab)

Acquisitions Unit Creation/Management

The following APIs will be used to create, read, update, and delete acquisition unit records.  These will typically be called from a new section in the UI settings area for managing acquisitions units.

  • POST, GET (query), GET (by Id), PUT, DELETE for the following:
    • /acquisitions-units/units
    • /acquisitions-unit-storage/units

Assignment Management

When it comes to managing acquisitions unit memberships, the following APIs will be used.  These APIs will also be called from the new section in the UI settings area for managing acquisitions units.

  • POST, GET (query), GET (by Id), PUT, DELETE for the following:
    • /acquisitions-units/memberships
    • /acquisitions-unit-storage/memberships

Restricting Creation/Modification of Records

In order to support the ability to prevent a record from being created/updated based on the acquisition units provided and that the user is a member of, we need to update our schemas to allow for acquisitions units to be specified along with the record (e.g. add acquisitions units array field to composite-purchase-order).  The business logic layer for each app would then check that the user has the appropriate acq. unit memberships to create or updated the record w/ the provided assignments, and if so, create/modify the record and assignments.  Otherwise, don't create/modify either and return an appropriate error.

  • We return an error w/o having created the record
  • Several schema changes are needed:
    • composite_purchase_order needs to be updated
    • new acquisitionUnits fields will be needed for all first-order records (e.g. orders, invoices, funds.  See "App Specifics" below and Appendix - Chicken & Egg Problem for details.
  • This simplifies things for the client (UI) as only a single API call would be needed
  • Having a schema with both the record and the acquisition unit may prove helpful in the context of another requirement - the ability to show the record's acq. units in search results.

NOTE:  Specifics of how to handle the modification of which acquisition units are assigned to a record have not been ironed out yet.  This refers to situations like an order is being updated and in the payload the set of acq. units differs from what's in storage.  Should modifications be limited to only allowing removal/addition of acq. units that the user performing the operation belongs to?

App Specifics

Up to this point, this document has been intentionally vague in its use of "records" since the concept will apply to several parts of acquisitions, not just orders. Here's are some of the app-specific details:

Users

  • Users will be assigned to acquisitions units in a new section of the UI settings area and will call the POST/GET/PUT/DELETE /acquisitions-units/assignments endpoints.
  • The "users" app will not be modified for this work.

Orders

  • We've decided to remove the acquisition-unit-assignment APIs and include an acquisitionUnits field directly into the purchase_order schema. See Appedix - Chicken & Egg Problem for details.
  • Purchase orders can be associated with one or more acquisitions units via the new acquisitions-unit-assignments APIs. This indicates the acquisitions unit(s) for not only the purchase order, but also associated poLines and pieces.
    • POST, GET (query), GET (by Id), PUT, DELETE for the following:
      • /orders/acquisitions-unit-assignments
      • /order-storage/acquisitions-unit-assignments
    • Schema has three fields: 

      id (UUID)recordId (UUID)acquisitionsUnitId (UUID)
      5558675 (PO A)12345
      7778309 (PO B)12345
      8888309 (PO B)23456
  • Views will need to be created in the storage module and will be used when querying purchase-orders, poLines, pieces, and receiving-history.
    • Join the purchaseOrder and acqUnitAssignments tables on purchaseOrder.id == recordId (Requires DISTINCT ON)
    • Join the poLine and acqUnitAssignments tables on poLine.purchaseOrderId == recordId (Requires DISTINCT ON)
  • The receiving history view will need to be updated to incorporate the acqUnitAssignments table on poLine.purchaseOrderId == recordId
  • The composite-purchase-order schema needs to be updated to include an "acquisitionsUnits" field - an array of acquisitions unit UUIDs
  • NOTE:  Even though mod-orders will have a desiredPermission related to acquisitions-unit-assignments.  I's important that we don't grant mod-orders module permissions allowing it to create assignments.  The permissions of the user calling mod-orders to create the order will be used when attempting to assign orders to acq. units.  Resulting errors need to be caught and handled appropriately
  • Since mod-gobi is a consumer of mod-orders, it needs to perform similar actions to that of ui-orders
    • generate a UUID for the order
    • lookup the appropriate acquisitions units to use based on the account (or eventually by fund)
    • place the order with mod-orders 
    • Additional note:  the institutional user will need appropriate acquisitions unit membership
  • The following flowcharts start with the orders API endpoints, but it might make more sense to think about or implement some or all of these from the point of view of interaction with the storage module.


Invoices

  • Invoices can be associated with one or more acquisitions units via the assignment APIs.  This indicates the acquisitions unit(s) for the invoice and all associated invoiceLines.
  • The general approach is very similar to that of orders... 
    • For search, we're inserting a prefix to queries
    • For get by id, we're retrieving the record from storage, and then making a decision on whether to return it the user or not
    • For put/post also work the same way
  • An acquisitionUnits field will be added to the invoice schema (array of UUIDs)
  • A new schema needs to be introduced which combines invoice and an acquisitionUnits field containing an array of acquisition unit UUIDs. 
    • This will be used in POST/PUT and can be phased into the GET APIs
    • The business logic layer will be responsible for making calls to both invoice-storage.invoices and invoice-storage.acquisitions-unit-assignments APIs
  • Views will need to be created in the storage module and will be used when querying invoices and invoiceLines.
    • Join the invoice and acqUnitAssignments tables on invoice.id == recordId (Requires DISTINCT ON)
    • Join the invoiceLine and invoice tables on invoiceLine.invoiceId == invoice.id (Requires DISTINCT ON)

Vouchers

  • Vouchers can be associated with one or more acquisitions units via a new acquisitionUnits field the assignment table.  This indicates the acquisitions unit(s) for the voucher and all associated voucherLines.
  • The voucher's acquisitions units will often be the same as the invoice's, though this will not necessarily be true for "aggregate" and "batch" vouchers which relate to multiple invoices.
  • When generating vouchers upon invoice transition to "Approved", transpose the invoice's acquisitions units onto the voucher's.
  • The general approach is very similar to that of orders... 
    • For search, we're inserting a prefix to queries
    • For get by id, we're retrieving the record from storage, and then making a decision on whether to return it the user or not
    • For put/post also work the same way
    • NOTE: not all CRUD operations for vouchers are currently exposed at the business logic layer.  As such, not all of the bullets above make much sense.  Endpoints for these operations will be needed for "aggregate" and "batch" vouchers, and will eventually be added.

Organizations

  • Organization can be associated with one or more acquisitions units a new acquisitionUnits field ("acqUnitIds"). 
  • Make necessary adjustments in the business logic module and UI to support this change.
  • Once this is in place the usual acquisition units handling can be performed like in other areas (orders/invoices/etc.)

Note: The field ("acqUnitIds") will also be added to the account section of the organizations schema, but will not be used to restrict permissions for accounts or organizations in an way. This information will be leveraged during the import of orders and invoices to assign acquisitions units to new orders and invoices as need.

Funds

  • TBD - This is a WIP and is subject to change.
  • funds can be associated with one or more acquisitions units via the assignment APIs.  This indicates the acquisitions unit(s) for the fund.
  • The general approach is very similar to that of orders... 
    • For search, we're inserting a prefix to queries
    • For get by id, we're retrieving the record from storage, and then making a decision on whether to return it the user or not
    • For put/post also work the same way
  • A new schema needs to be introduced which combines fund and an acquisitionUnits field containing an array of acquisition unit UUIDs. 
    • This will be used in POST/PUT and can be phased into the GET APIs
    • The business logic layer (not implemented yet) will be responsible for making calls to both finance-storage.funds and finance-storage.acquisitions-unit-assignments APIs
  • Views will need to be created in the storage module and will be used when querying funds.
    • Join the fund and acqUnitAssignments tables on fund.id == recordId (Requires DISTINCT ON)

Fund Use Cases

Acq. UnitprotectCreateprotectReadprotectUpdateprotectDelete
FundAllowFundViewAcqUnitfalsefalsetruetrue
RestrictFundViewAcqUnittruetruefalsetrue
Effectivefalsefalsefalsetrue
FundAcqUnits
FundRistrictView1RestrictFundViewAcqUnit
FundRistrictView2RestrictFundViewAcqUnit, FundAllowFundViewAcqUnit
FundAllowViewFundAllowFundViewAcqUnit
FundWithoutAcqUnits

Fund view 

 1. Scenario - Show funds on Fund View

    • Given user belongs to RestrictFundViewAcqUnit
    • When open "Fund View"
    • Then Fund(s) are displayed :
          1. WITHOUT ANY acq units (FundWithoutAcqUnits)
          2. Have at least one of acq unit(s) as the user (FundRistrictView1, FundRistrictView2)
          3. Have acq unit(s) which DON'T protect read (FundAllowView)

2. Scenario - Show funds on Fund View

    • Given user belongs to RestrictFundViewAcqUnit
    • And "Fund View" opened
    • When user select RestrictFundViewAcqUnit in the filter "Acquisition units"
    • Then Only Fund(s) with acquisition unit RestrictFundViewAcqUnit are displayed (FundRistrictView1, FundRistrictView2)

Add/Update PO line

Show funds in filter list 

 1. Scenario

    • Given user belongs to RestrictFundViewAcqUnit
    • And Open "Add PO Line" form
    • When Selecting Funds from the filter list
    • Then Fund(s) are displayed :
          1. WITHOUT ANY acq units (FundWithoutAcqUnits)
          2. Have at least one of acq unit(s) as the user (FundRistrictView1, FundRistrictView2)
          3. Have acq unit(s) which DON'T protect read (FundAllowView)

 2. Scenario

    • Given user belongs to FundAllowFundViewAcqUnit
    • And Open "Add PO Line" form
    • When Selecting Funds from the filter list
    • Then Fund(s) are displayed :
          1. WITHOUT ANY acq units (FundWithoutAcqUnits)
          2. Have at least one of acq unit(s) as the user (FundRistrictView2)
          3. Have acq unit(s) which DON'T protect read (FundAllowView)

3. Scenario

    • Given user belongs to tehn one acq units : RestrictFundViewAcqUnit and FundAllowFundViewAcqUnit
    • AND Open "Add PO Line" form
    • When Selecting Funds from the filter list
    • Then Fund(s) are displayed :
          1. WITHOUT ANY acq units (FundWithoutAcqUnits)
          2. Have at least one of acq unit(s) as the user (FundRistrictView1, FundRistrictView2)
          3. Have acq unit(s) which DON'T protect read (FundAllowView)
Save PO line from back-end perspective like a POST operation

1. Scenario

    • Given user belongs to RestrictFundViewAcqUnit
    • And fundDistributions contain FundRistrictView1, FundRistrictView2
    • When Save PO line
    • Then PO line save successfully

2. Scenario

    • Given user belongs to RestrictFundViewAcqUnit
    • And fundDistributions contain FundRistrictView1, FundWithoutAcqUnits
    • When Save PO line
    • Then PO line save successfully

3. Scenario

    • Given user belongs to RestrictFundViewAcqUnit
    • And fundDistributions contain FundRistrictView1, FundAllowView
    • When Save PO line
    • Then PO line save successfully

4. Scenario

    • Given user belongs to FundAllowFundViewAcqUnit
    • And fundDistributions contain FundAllowView, FundWithoutAcqUnits
    • When Save PO line
    • Then PO line save successfully

5. Scenario

    • Given user belongs to FundAllowFundViewAcqUnit
    • And fundDistributions contain FundRistrictView2
    • When Save PO line
    • Then PO line save successfully

6. Scenario

    • Given user belongs to FundAllowFundViewAcqUnit
    • And fundDistributions contain FundRistrictView1
    • When Save PO line
    • Then Error must be return "Not allowed to add funds : FundRistrictView1"

7. Scenario

    • Given user belongs to FundAllowFundViewAcqUnit
    • And fundDistributions contain FundAllowView, FundRistrictView1
    • When Save PO line
    • Then Error must be return "Not allowed to add funds : FundRistrictView1"

Add/Update Invoice line

Show funds in filter list 

The same as for PO line

Save invoice line from back-end perspective like a POST operation

The same as for PO line

JIRA

A convenient place to put links to relevant JIRA epics/features/stories/bugs/etc.

Open Issues

  • Permissions Escalation - if a user belongs to multiple acquisitions units, and it's determined that they need the ability to perform some action for records of one of them, additional FOLIO permissions might be granted to them.  What may not be evident to the person granting those permissions is that this would also give that user additional rights for all their acquisitions units.  Just because Brenda needs to update "main" orders, she might not need to ever update "law" orders.  So if the "edit order" permission is granted to Brenda, she can now edit both "main" and "law" orders since she has the appropriate FOLIO permission and belongs to both acquisitions units. 

Appendix

Restricting Creation/Modification of Records Options

There are a couple different approaches we can take here... Only one option will be chosen and documented above.

(error) Option #1 - Keep record creation and acquisition unit assignment completely separate

In this approach we keep the two operations separate.  If a user has permissions to create an order, they should be able to create the order, regardless of whether they also have permission to assign that order to an acquisition unit.

  • This doesn't require schema changes
  • Client would need to create the record (e.g. order), then create the assignments - potentially multiple API calls
  • The downside is that the user experience might suffer.  If the UI creates the record successfully but fails to create the assignment is that ideal?  The error message displayed would need to be clear to avoid misunderstandings and confusion.

(tick) Option #2 - Combine the record creation and acq. unit assignment into a single call, handle in the business logic layer

In this approach we update our schemas to allow for acquisitions units to be specified along with the record (e.g. add acquisitions units array field to composite-purchase-order).  The business logic layer would then check that the user has the appropriate acq. unit memberships to create the record w/ the provided assignments, and if so, create the record and assignments.  Otherwise, don't create either and return an appropriate error.

  • We return an error w/o having created the record - this sounds like it's the preferred behavior
  • A whole bunch of schema changes are needed
    • composite_purchase_order needs to be updated
    • new schemas are needed for invoices, funds, and any other types of records which are assigned to acquisitions units.  See "App Specifics" below for details.
  • This simplifies things for the client (UI) as only a single API call would be needed
  • Having a schema with both the record and the acquisition unit may prove helpful in the context of another requirement - the ability to show the record's acq. units in search results.

Order of Operations / Chicken & Egg Problem

While implementing MODORDERS-251 - Restrict creation of PO, POL, Piece records based upon acquisition unit, we ran into a sort of chicken/egg problem with creation of the order record and the acquisition unit assignment record.  Additional context can be found in the comments section of the PR PR #183.  Note that this also affects PUT and DELETE or orders/assignments.  Here I'll lay out several options for consideration:

(error) Option #1 - Create the order first, assignment second

In this approach we create the order first, then create the assignment record(s).  

PROS:

  • Keeps things simple
  • Can keep the foreign key constraint in acquisition_unit_assignments.recordId → purchase_order.id

CONS:

  • If the call to create the assignment fails (due to insufficient permissions, or any other reason), the order already exists and needs to be cleaned up.

(error) Option #2 - Create the assignment first, order second

In this approach we create the assignment first, then the order record if that succeeds

PROS:

  • Avoids the problem of having to clean up the order if we can't create the assignment

CONS:

  • Requires that we remove the foreign key constraint in acquisition_unit_assignments.recordId → purchase_order.id
  • Now you potentially have to deal with cleaning up an assignment record if the order fails
  • If an order Id is provided, we could try to "find or create" the assignment, but that would require making the order id required, a breaking change.
  • Generating a UUID if one isn't provided allows us to create the assignment, but now you have to remove the assignment record if the order creation fails.

(tick) Option #3 - Store assignments in the purchase order record

In this approach we remove the acquisition unit assignments API/table and store the assignments directly in the purchase order record.  See the comments section for details.

(error) Option #4 - Handle this in a transaction at the storage layer

In this approach we move the problem to the storage module which has a clean/convenient way to handle the rollback on failure - transactions.  

mod-orders-storage:

Add new endpoints:

POST /orders-storage/protected-purchase-orders

  • takes purchase_order w/ acquisitionUnits (schema TBD - use composite-purchase-order and ignore poLines if present?  new schema?)
  • in a transaction:
    • create purchase_order
    • create acquisition_unit_assignment(s)
  • rolls back if either fails and returns appropriate error
  • requires permissions:
    • orders-storage.purchase-order.item.post
    • orders-storage.acquisitions-unit-assignments.item.post

DELETE /orders-storage/protected-purchase-orders/<purchaseOrderId>

  • in a transaction:
    • delete acquisition_unit_assignment(s)
    • delete purchase_order
  • rolls back if either fails and returns appropriate error
  • requires permissions:
    • orders-storage.purchase-order.item.delete
    • orders-storage.acquisitions-unit-assignments.item.delete

PUT /orders-storage/protected-purchase-orders/<purchaseOrderId>

  • takes purchase_order w/ acquisitionUnits (schema TBD - use composite-purchase-order and ignore poLines if present?  new schema?)
  • in a transaction:
    • delete acquisition_unit_assignments
    • create acquisition_unit_assignments
    • update purchase_order
  • rolls back if either fails and returns appropriate error
  • requires permissions:
    • orders-storage.purchase-order.item.put
    • orders-storage.acquisitions-unit-assignments.item.put

mod-orders:

Update endpoints:

POST /orders/composite-orders

  • performs all validations including those related to acquisition units
  • check if acquisitionsUnits exists in provided order
    • if so, call POST /orders-storage/protected-purchase-orders
    • else call POST /orders-storage/purchase-orders
  • requires only orders.item.post

DELETE /orders/composite-orders/<purchaseOrderId>

  • performs all validations related to acquisition units
  • check if order has any acquisitionsUnits
    • if so, call DELETE /orders-storage/protected-purchase-orders
    • else call DELETE /orders-storage/purchase-orders
  • requires only orders.item.delete

PUT /orders/composite-orders/<purchaseOrderId>

  • performs all validations including those related to acquisition units
  • check if either provided, or stored order has any acquisitionsUnits
    • if so, call PUT /orders-storage/protected-purchase-orders
    • else call PUT /orders-storage/purchase-orders
  • requires only orders.item.put

Remove endpoints since they are no longer needed...  Management of order assignments happens internally

  • POST /orders/acquisitions-units-assignments/<id>
  • DELETE /orders/acquisitions-units-assignments/<id>
  • PUT /orders/acquisitions-units-assignments/<id>

PROS:

  • no need to remove FK constraint
  • no need for complex exception handling in BL module
  • no need for decentralized permission enforcement
  • still maintains separate permissions for order creation and acquisition unit assignments

CONS:

  • difficult to find a good endpoint name
  • makes the storage module API slightly more confusing - separate endpoint for "protected" orders? 
    • can be mitigated via documentation
    • not a big deal since storage module is intended for internal use only.
  • still isn't obvious that the user needs the acquisitions-unit-assignment.item.post permission to create an order w/ acquisitionUnits
    • can also be mitigated with documentation and clear error messages
    • perhaps the notion of optional permissions will someday be introduced to FOLIO and we can make this more obvious              

(error) Option #5 - Hybrid of #3 and #4

In this approach we combine parts of options 3 and 4.  As in option 3 we, create new endpoints in the storage module that explicitly require assignment permissions, only instead of updating the orders/assignments separately in a transaction, we pull from option #4 and incorporate acquisitionUnits directly into the purchase_order.

The same API changes as option #3

PROS:

  • All the PROS from Option #3 and Option #4
  • Storage module is slightly simpler
    • no need for transactions
    • fewer tables, views

CONS:

  • All the CONS from option #4
  • lists of UUIDs in the order record.

Sidebar - Validation Endpoint(s)

One thing not mentioned above that was brought up in the aforementioned PR comments is the idea of implementing one or more endpoints for the sole purpose of checking that the calling user has specific permissions.  

For example:

POST /orders-storage/protected-purchase-orders/validate

  • requires permissions:
    • orders-storage.purchase-order.item.post
    • orders-storage.acquisitions-unit-assignments.item.post

DELETE /orders-storage/protected-purchase-orders/validate

  • requires permissions:
    • orders-storage.purchase-order.item.delete
    • orders-storage.acquisitions-unit-assignments.item.delete

PUT /orders-storage/protected-purchase-orders/validate

  • requires permissions:
    • orders-storage.purchase-order.item.put
    • orders-storage.acquisitions-unit-assignments.item.put

These endpoints literally only ever return 200 responses - if the user doesn't have the required permissions their request would never even make it to the module; they'd get a 403 from Okapi.

This could be helpful for fast-failing requests in the business logic layer before spending a bunch of time performing validation just for the call to persist changes to fail.

PROS:

  • allows mod-orders to fail fail requests that ultimately can't be completed successfully while avoiding explicit permission checks in the module itself.

CONS:

  • there's tight coupling between the validate endpoints and the corresponding endpoints which actually perform some action.  Care will need to be taken to keep these in sync
  • seems like more work than it's worth - in this use case the storage module is ultimately enforcing that the user has the necessary permissions.  The BL module is just trying to determine if it can avoid a bunch of extra work.  In this case I'm inclined to say that checking the permissions in X-Okapi-Permissions a simpler way to accomplish this.  The distinction between enforcement and optimization is subtle here...  

Sidebar - Requirement for viewing acquisition units assignments in search results

There's a requirement that purchase_order acq. unit assignments are shown in the order search results table.  This isn't currently possible w/o making changes to GET /orders/composite-orders.  I mention this here because some of the options above would help us meet this requirement.

  1. If we go with option #3 or #5, we'll have acquisitionUnits returned by that endpoint.
  2. if we go with option #4 we could update the view queried by GET /orders-storage/orders endpoint to have acquisitionUnits in the jsonb column and have this endpoint return a collection<composite_purchase_order>. 

I've asked for clarification on whether or not similar requirements exist for poLines and pieces, but the POs can't give a definite answer without talking to the small group.  This uncertainty is unfortunate as the direction we go here has implications for how we meet these requirements.

  1. If we assume that we need to display acquisition units assigned to each POLine in the search results, it means we'll likely have to do something like 2. above (adjust views and schemas returned in get by query endpoints.

Conflict/Precedence Options

The following lays out several options for purposes of discussion.  Eventually one approach will need to be selected.

Option A - Least restrictive wins (AND)

In this approach we perform an AND operation for each of the protect* fields across all of the acquisitions units associated with the record.  This in effect equates to "the least restrictive wins".


protectCreateprotectReadprotectUpdateprotectDelete
AU 9999truetruetruefalse
AU 8888truetruefalsefalse

AU 7777

truefalsetruefalse
AU 6666truefalsefalsefalse
Effectivetruefalsefalsefalse

Option B - Most restrictive wins (OR)

In this approach we perform an OR operation for each of the protect* fields across all of the acquisitions units associated with the record.  This in effect equates to "the most restrictive wins".


protectCreateprotectReadprotectUpdateprotectDelete
AU 9999truetruetruefalse
AU 8888truetruefalsefalse

AU 7777

truefalsetruefalse
AU 6666truefalsefalsefalse
Effectivetruetruetruefalse

Option C - Change from 'protect' to 'allow'

In this approach we move away from protecting certain actions and instead allow certain actions.  In other words:

  • We explicitly specify what members can do
  • The behavior of members changes from allowing members to do anything to only allowing them to perform certain actions.
  • The behavior of non-members changes to not being able to do anything.  

AND becomes "most restrictive wins" and would look like:


allowCreateallowReadallowUpdateallowDelete
AU 9999truetruetruefalse
AU 8888truetruefalsefalse

AU 7777

truefalsetruefalse
AU 6666truefalsefalsefalse
Effectivetruefalsefalsefalse

OR becomes "least restrictive wins" and would look like:


allowCreateallowReadallowUpdateallowDelete
AU 9999truetruetruefalse
AU 8888truetruefalsefalse

AU 7777

truefalsetruefalse
AU 6666truefalsefalsefalse
Effectivetruetruetruefalse

Option D - Explicitly specify what members and non-members can do

In this approach, we define both what non-members can do and what members can do.  This gives us greater flexibility but also adds significant complexity.

We'd need to implement this in a way which wouldn't restrict members more than non-members.

AND would look like:


allUsersCanCreateallUsersCanReadallUsersCanUpdateallUsersCanDeletemembersCanCreatemembersCanReadmembersCanUpdatemembersCanDelete
AU 9999truetruetruefalsetruetruetruefalse
AU 8888truetruefalsefalsetruetruefalsefalse

AU 7777

truefalsetruefalsetruefalsetruefalse
AU 6666truefalsefalsefalsetruefalsefalsefalse
Effectivetruefalsefalsefalsetruefalsefalsefalse

OR would look like:


allUsersCanCreateallUsersCanReadallUsersCanUpdateallUsersCanDeletemembersCanCreatemembersCanReadmembersCanUpdatemembersCanDelete
AU 9999truetruetruefalsetruetruetruefalse
AU 8888truetruefalsefalsetruetruefalsefalse

AU 7777

truefalsetruefalsetruefalsetruefalse
AU 6666truefalsefalsefalsetruefalsefalsefalse
Effectivetruetruetruefalsetruetruetruefalse

Other Considerations:

  • If we choose to explicitly control what members can do (Options C/D), we need to somehow handle the scenario where records are created in a way which prevents them from ever being viewed, updated or deleted.  Certainly this should be prevented as it would serve no purpose to have these hidden, immutable records.
  • How do we want to handle adding additional assignments to a records... is this covered by create? update?  Do we need a separate "permission" for this? 
    • Example:  Invoice X is created and associated with the "main" acquisitions unit. 
      • What's required to later associate invoice X with the "law" acquisitions unit?  The user needs to be part of the "main" and "law" units?  If going with Options C/D, what if membersCanUpdate is false?  membersCanCreate?
      • What's required to later remove invoice X's association with the "main" acquisitions unit?  The user needs to be part of the "main" unit?  If going with Options C/D, what if membersCanUpdate is false?  membersCanDelete?
  • Do we need separate "permissions" for the "owner/creator" of the record?  Should the user that created the record always be able to CRUD it?  What if their acquisitions unit assignment changes and they're no longer a member of the appropriate unit(s)?
    • Working in owner/creator solves some problems, but raises others.