Modeling the delegation from Employer to Employee

Would the following serve as a good example of how to model the way that employers assign roles to employees?

import Daml.Script
import DA.List 

template DocumentManager with
    employer : Party
    employee : Party
  where
    signatory employer
    observer employee

    nonconsuming choice CreateDocument : ContractId Document with
      controller employee
      do create Document with
          observers = [employee]
          value = 1
          ..

    nonconsuming choice EditDocument : ContractId Document with
        doc : ContractId Document
      controller employee
      do exercise doc Edit
    

template Document with
    employer : Party
    value : Int
    observers : [Party]
  where
    signatory employer
    observer observers

    choice Edit : ContractId Document
      controller employer
      do create this with value = value + 1

    choice ChangeAcceess : ContractId Document with
        removes : [Party]
        adds : [Party]
      controller employer
      do 
        create this with
          observers = dedup $ foldr delete (adds ++ observers) removes


demo : Script ()
demo = script do
  bank <- allocateParty "Bank"

  -- bob joins the company and does some work
  bob <- allocateParty "Bob"
  bobRole <- submit bank $ createCmd DocumentManager with employer = bank, employee = bob
  doc1 <- submit bob $ exerciseCmd bobRole CreateDocument 
  doc1 <- submit bob $ exerciseCmd bobRole EditDocument with doc = doc1
  doc1 <- submit bob $ exerciseCmd bobRole EditDocument with doc = doc1
  doc1 <- submit bob $ exerciseCmd bobRole EditDocument with doc = doc1
  doc1 <- submit bob $ exerciseCmd bobRole EditDocument with doc = doc1

  -- alice joins the company and does some work
  alice <- allocateParty "Alice"
  aliceRole <- submit bank $ createCmd DocumentManager with employer = bank, employee = alice
  doc2 <- submit alice $ exerciseCmd aliceRole CreateDocument 
  doc2 <- submit alice $ exerciseCmd aliceRole EditDocument with doc = doc2
  doc2 <- submit alice $ exerciseCmd aliceRole EditDocument with doc = doc2

  -- alice initially cannot edit bob's docs
  submitMustFail alice $ exerciseCmd aliceRole EditDocument with doc = doc1

  -- alice get's access to bob's docs and does some work
  doc1 <- submit bank $ exerciseCmd doc1 ChangeAcceess with adds = [alice], removes = [] 
  doc1 <- submit alice $ exerciseCmd aliceRole EditDocument with doc = doc1

  -- bob leaves the company
  submit bank $ archiveCmd bobRole
  doc1 <- submit bank $ exerciseCmd doc1 ChangeAcceess with removes = [bob], adds = []
  submitMustFail bob $ exerciseCmd bobRole EditDocument with doc = doc1

  -- alice continues work
  doc1 <- submit alice $ exerciseCmd aliceRole EditDocument with doc = doc1
  doc2 <- submit alice $ exerciseCmd aliceRole EditDocument with doc = doc2
  
  pure()

For write access this is fine, but for read access you can do better with Users. How would you modify the example to avoid maintaining an observers list on Document?

Thanks for looking at this with me, @Leonid_Rozenberg

Here is a User-based implementation. Instead of having a Daml template (like DocumentManager above) to model the delegation, we use a party (like docManager below) and the built-in actAs field. This implementation does not require an observers : [Party].

import Daml.Script
import DA.Stack (HasCallStack)


template Document with
    party : Party
    value : Int
  where
    signatory party
  
    choice Edit : ContractId Document
      controller party
      do create this with value = value + 1


demo : Script ()
demo = script do
  docManager <- allocateParty "Document Manager"

  -- bob joins the company
  bob <- allocatePartyWithHint "Bob" (PartyIdHint "Bob")
  bobId <- validateUserId "bob"
  createUser (User bobId (Some bob)) [CanActAs bob, CanActAs docManager]

  -- bob does some work
  doc1 <- submitUser bobId $ createCmd Document with value = 1, party = docManager
  doc1 <- submitUser bobId $ exerciseCmd doc1 Edit
  doc1 <- submitUser bobId $ exerciseCmd doc1 Edit
  doc1 <- submitUser bobId $ exerciseCmd doc1 Edit
  doc1 <- submitUser bobId $ exerciseCmd doc1 Edit

  -- alice joins the company and does some work
  alice <- allocatePartyWithHint "Alice" (PartyIdHint "Alice")
  aliceId <- validateUserId "alice"
  createUser (User aliceId (Some alice) ) [CanActAs alice, CanActAs docManager]
  doc2 <- submitUser bobId $ createCmd Document with value = 1, party = docManager
  doc2 <- submitUser bobId $ exerciseCmd doc2 Edit
  doc2 <- submitUser bobId $ exerciseCmd doc2 Edit

  -- sadly alice can modify bob's docs
  doc1 <- submitUser aliceId $ exerciseCmd doc1 Edit
  
  -- bob leaves the company
  revokeUserRights bobId [CanActAs docManager]
  submitUserMustFail bobId $ exerciseCmd doc1 Edit
  deleteUser bobId
submitUserMustFail implementation
-- https://discuss.daml.com/t/why-is-there-no-submitusermustfail/4978/3?u=wallacekelly
submitUserMustFail : HasCallStack => UserId -> Commands a -> Script ()
submitUserMustFail userId cmds = do
  rights <- listUserRights userId
  let actAs = [ p | CanActAs p <- rights ]
  let readAs = [ p | CanReadAs p <- rights ]
  submitMultiMustFail actAs readAs cmds

This code is much simpler and shorter. However, I do not see a way to limit Alice’s access to Bob’s documents. (See the comment, “sadly alice can modify bob’s docs”.)

As best I can tell, if we need to support user-level visibility to contracts, then we will end up with user-level parties in an observer’s list.

Am I wrong?

You need to combine both approaches. The first approach to control the right to modify and the second to control the right to read.