# Per Tenant Setting [Traction]
### Resolves
- Issue [Traction #540](https://github.com/bcgov/traction/issues/540)
With [issue #394](https://github.com/bcgov/traction/issues/394), support the ability to allow a tenant to connect to endorser and/or create a public DID. With [issue #393](https://github.com/bcgov/traction/issues/393), based on the description [here](https://github.com/bcgov/traction/blob/develop/docs/USE-CASE-ONBOARD.md#make-teanant-an-issuer), this will require connection to endorser and creating/registering a public did [covered above]
### Design
- Update `InnkeeperWalletConfig` to allow the following bool settings/flags:
- `connect_to_endorser`
- `create_public_did`
- `issuer`
- If `issuer` option is set as True then both `connect_to_endorser` and `create_public_did` are set as True
- `create_innkeeper` function in `TenantManager` is where these additional flags in config are processed and accordingly new utility functions [`connect_to_endorser` and `create_public_did`] are called upon.
### Implementation
In `plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/config.py`
1. Update `InnkeeperWalletConfig`
```
class InnkeeperWalletConfig(BaseModel):
tenant_id: Optional[str] # real world, this is a UUID
wallet_name: Optional[str]
wallet_key: Optional[str]
print_key: bool = False
print_token: bool = False
connect_to_endorser: bool = False
create_public_did: bool = False
issuer: bool = False
class Config:
alias_generator = _alias_generator
allow_population_by_field_name = True
@classmethod
def default(cls):
# consider this for local development only...
return cls(
tenant_id="innkeeper",
wallet_key="MySecretKey123",
wallet_name="traction_innkeeper_v1_0",
print_key=False,
print_token=False,
connect_to_endorser=False,
create_public_did=False,
issuer=False,
)
```
In `plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/tenant_manager.py`
1. add the following functions [note: create_public_did() is incomplete]:
```
def process_per_tenant_issuer_settings(self, config: InnkeeperWalletConfig):
if not config.issuer:
return config.connect_to_endorser, config.create_public_did
else:
return True, True
async def connect_to_endorser(self):
endorser_srv = self._profile.inject_or(EndorserConnectionService)
if not endorser_srv:
raise Exception(
"No EndorserConnectionService instance is binded with the tenant profile."
)
info = endorser_srv.endorser_info(self._profile)
if not info:
raise Exception("Endorser is not configured")
await endorser_srv.connect_with_endorser(self._profile, self._profile.context.injector)
async def create_public_did(self, method="sov", key_type="ed25519", seed=None, did=None):
# Sub wallet
wallet = self._profile.inject_or(BaseWallet)
if not wallet:
raise Exception(
"No subwallet found or BaseWallet instance is binded with the tenant profile."
)
# Creating local DID
try:
await wallet.create_local_did(
method=method, key_type=key_type, seed=seed, did=did
)
except WalletError as err:
self._logger.error(err)
raise err
# Registering DID
# Derive code from register_ledger_nym function in /ledger/register-nym [ACAPy]
# Assign DID as public DID
# Derive code from wallet_set_public_did function in /wallet/did/public [ACAPy]
```
2. Update `create_innkeeper` function:
```
async def create_innkeeper(self):
config = self._config.innkeeper_wallet
tenant_id = config.tenant_id
wallet_name = config.wallet_name
wallet_key = config.wallet_key
connect_to_endorser, create_public_did = self.process_per_tenant_issuer_settings(config)
if connect_to_endorser:
await self.connect_to_endorser()
if create_public_did:
await self.create_public_did()
...
```
# Per Tenant Setting [ACAPy]
### Resolves
- Issue [Traction #547](https://github.com/bcgov/traction/issues/547)
- Issue [ACA-Py #2209]([2209](https://github.com/hyperledger/aries-cloudagent-python/issues/2209))
✅ Supports `agent lifecycle configuration flags`
❌ multi-write ledger support is more complex. In ACA-Py, I had originally started work on this but ultimately just implemented multi-read support.
### Design
`POST /multitenancy/wallet`
- add `per_tenant_setting` dict field to the request schema. For instance:
```
{
...
"ACAPY_LOG_LEVEL": "info",
"ACAPY_INVITE_PUBLIC": True,
"ACAPY_PUBLIC_INVITES": False,
...
}
```
- The `per_tenant_setting` dict will update the other wallet settings when calling the endpoint and then execute `BaseMultitenantManager -> create_wallet` to apply the per tenant + wallet settings
- `per_tenant_setting` will support the following flags as defined in the issue:
- ACAPY_LOG_LEVEL
- ACAPY_INVITE_PUBLIC
- ACAPY_PUBLIC_INVITES
- ACAPY_AUTO_ACCEPT_INVITES
- ACAPY_AUTO_ACCEPT_REQUESTS
- ACAPY_AUTO_PING_CONNECTION
- ACAPY_MONITOR_PING
- ACAPY_AUTO_RESPOND_MESSAGES
- ACAPY_AUTO_RESPOND_CREDENTIAL_OFFER
- ACAPY_AUTO_RESPOND_CREDENTIAL_REQUEST
- ACAPY_AUTO_VERIFY_PRESENTATION
- ACAPY_NOTIFY_REVOCATION
- ACAPY_AUTO_REQUEST_ENDORSEMENT
- ACAPY_AUTO_WRITE_TRANSACTIONS
- ACAPY_CREATE_REVOCATION_TRANSACTIONS
- Note: `ACAPY_AUTO_REQUEST_ENDORSEMENT`, `ACAPY_AUTO_WRITE_TRANSACTIONS`, `ACAPY_CREATE_REVOCATION_TRANSACTIONS` depends upon the endorser role being an Author. To support this `per_tenant_setting` can also include `ACAPY_ENDORSER_ROLE`. If `ACAPY_ENDORSER_ROLE` is missing or if it not set as `author` then the 3 config flags will be ignored.
### Implementation
In `./aries_cloudagent/multitenant/admin/routes.py`
1. add the following:
```
ACAPY_LIFECYCLE_CONFIG_FLAG_MAP = {
"ACAPY_LOG_LEVEL": "log.level",
"ACAPY_INVITE_PUBLIC": "debug.invite_public",
"ACAPY_PUBLIC_INVITES": "public_invites",
"ACAPY_AUTO_ACCEPT_INVITES": "debug.auto_accept_invites",
"ACAPY_AUTO_ACCEPT_REQUESTS": "debug.auto_accept_requests",
"ACAPY_AUTO_PING_CONNECTION": "auto_ping_connection",
"ACAPY_MONITOR_PING": "debug.monitor_ping",
"ACAPY_AUTO_RESPOND_MESSAGES": "debug.auto_respond_messages",
"ACAPY_AUTO_RESPOND_CREDENTIAL_OFFER": "debug.auto_resopnd_credential_offer",
"ACAPY_AUTO_RESPOND_CREDENTIAL_REQUEST": "debug.auto_respond_credential_request",
"ACAPY_AUTO_VERIFY_PRESENTATION": "debug.auto_verify_presentation",
"ACAPY_NOTIFY_REVOCATION": "revocation.notify",
"ACAPY_AUTO_REQUEST_ENDORSEMENT": "endorser.auto_request",
"ACAPY_AUTO_WRITE_TRANSACTIONS": "endorser.auto_write",
"ACAPY_CREATE_REVOCATION_TRANSACTIONS": "endorser.auto_create_rev_reg",
"ACAPY_ENDORSER_ROLE": "endorser.protocol_role",
}
ACAPY_ENDORSER_FLGAS_DEPENDENT_ON_AUTHOR_ROLE = [
"ACAPY_AUTO_REQUEST_ENDORSEMENT",
"ACAPY_AUTO_WRITE_TRANSACTIONS",
"ACAPY_CREATE_REVOCATION_TRANSACTIONS",
]
def get_extra_settings_dict_per_tenant(tenant_settings: dict) -> dict:
"""Get per tenant settings to be applied when creating wallet."""
endorser_role_flag = tenant_settings.get("ACAPY_ENDORSER_ROLE")
extra_settings = {}
if endorser_role_flag == "author":
extra_settings["endorser.author"] = True
elif endorser_role_flag == "endorser":
extra_settings["endorser.endorser"] = True
for flag in tenant_settings.keys():
if flag in ACAPY_ENDORSER_FLGAS_DEPENDENT_ON_AUTHOR_ROLE and endorser_role_flag != "author":
# These flags require endorser role as author, if not set as author then
# this setting will be ignored.
continue
if flag != "ACAPY_ENDORSER_ROLE":
map_flag = ACAPY_LIFECYCLE_CONFIG_FLAG_MAP[flag]
extra_settings[map_flag] = tenant_settings[flag]
return extra_settings
```
2. update `CreateWalletRequestSchema(OpenAPISchema)` and add the following field:
```
per_tenant_setting = fields.Dict(
keys=fields.Str(description="Agent Config Flag"),
values=fields.Str(description="Parameter"),
allow_none=True,
)
```
3. update `wallet_create()` [snippet]:
```
per_tenant_setting = body.get("per_tenant_setting") or {}
# If no webhooks specified, then dispatch only to base webhook targets
if wallet_webhook_urls == []:
wallet_dispatch_type = "base"
settings = {
"wallet.type": body.get("wallet_type") or "in_memory",
"wallet.name": body.get("wallet_name"),
"wallet.key": wallet_key,
"wallet.webhook_urls": wallet_webhook_urls,
"wallet.dispatch_type": wallet_dispatch_type,
}
extra_tenant_setting = get_extra_settings_dict_per_tenant(per_tenant_setting)
settings.update(extra_tenant_setting)
```