Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Configuration Settings on a per-tenant basis #2233

Merged
merged 12 commits into from
Jun 28, 2023
79 changes: 79 additions & 0 deletions Multitenancy.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ This allows ACA-Py to be used for a wider range of use cases. One use case could
- [Tenant Management](#tenant-management)
- [Update a tenant](#update-a-tenant)
- [Remove a tenant](#remove-a-tenant)
- [Per tenant settings](#per-tenant-settings)

## General Concept

Expand Down Expand Up @@ -375,3 +376,81 @@ curl -X POST "${ACAPY_ADMIN_URL}/multitenancy/wallet/{wallet_id}/remove" \
```jsonc
{}
```

### Per tenant settings
shaangill025 marked this conversation as resolved.
Show resolved Hide resolved

To allow configurablity of ACA-Py startup parameters/environment variables at a tenant/subwallet level. [PR#2233](https://github.com/hyperledger/aries-cloudagent-python/pull/2233) will provide the ability to update the following subset of settings when creating or updating the subwallet:

| Labels | | Setting |
|---|---|---|
| ACAPY_LOG_LEVEL | log-level | log.level |
shaangill025 marked this conversation as resolved.
Show resolved Hide resolved
| ACAPY_INVITE_PUBLIC | invite-public | debug.invite_public |
| ACAPY_PUBLIC_INVITES | public-invites | public_invites |
| ACAPY_AUTO_ACCEPT_INVITES | auto-accept-invites | debug.auto_accept_invites |
| ACAPY_AUTO_ACCEPT_REQUESTS | auto-accept-requests | debug.auto_accept_requests |
| ACAPY_AUTO_PING_CONNECTION | auto-ping-connection | auto_ping_connection |
| ACAPY_MONITOR_PING | monitor-ping | debug.monitor_ping |
| ACAPY_AUTO_RESPOND_MESSAGES | auto-respond-messages | debug.auto_respond_messages |
| ACAPY_AUTO_RESPOND_CREDENTIAL_OFFER | auto-respond-credential-offer | debug.auto_resopnd_credential_offer |
| ACAPY_AUTO_RESPOND_CREDENTIAL_REQUEST | auto-respond-credential-request | debug.auto_respond_credential_request |
| ACAPY_AUTO_VERIFY_PRESENTATION | auto-verify-presentation | debug.auto_verify_presentation |
| ACAPY_NOTIFY_REVOCATION | notify-revocation | revocation.notify |
| ACAPY_AUTO_REQUEST_ENDORSEMENT | auto-request-endorsement | endorser.auto_request |
| ACAPY_AUTO_WRITE_TRANSACTIONS | auto-write-transactions | endorser.auto_write |
| ACAPY_CREATE_REVOCATION_TRANSACTIONS | auto-create-revocation-transactions | endorser.auto_create_rev_reg |
| ACAPY_ENDORSER_ROLE | endorser-protocol-role | endorser.protocol_role |

- `POST /multitenancy/wallet`

Added `extra_settings` dict field to request schema. `extra_settings` can be configured in the request body as below:

**`Example Request`**
```
{
"wallet_name": " ... ",
"default_label": " ... ",
"wallet_type": " ... ",
"wallet_key": " ... ",
"key_management_mode": "managed",
"wallet_webhook_urls": [],
"wallet_dispatch_type": "base",
"extra_settings": {
"ACAPY_LOG_LEVEL": "INFO",
"ACAPY_INVITE_PUBLIC": true,
"public-invites": true
},
}
```

```sh
echo $new_tenant | curl -X POST "${ACAPY_ADMIN_URL}/multitenancy/wallet" \
-H "Content-Type: application/json" \
-H "X-Api-Key: $ACAPY_ADMIN_URL_API_KEY" \
-d @-
```

- `PUT /multitenancy/wallet/{wallet_id}`

Added `extra_settings` dict field to request schema.

**`Example Request`**
```
{
"wallet_webhook_urls": [ ... ],
"wallet_dispatch_type": "default",
"label": " ... ",
"image_url": " ... ",
"extra_settings": {
"ACAPY_LOG_LEVEL": "INFO",
"ACAPY_INVITE_PUBLIC": true,
"ACAPY_PUBLIC_INVITES": false
},
}
```

```sh
echo $update_tenant | curl -X PUT "${ACAPY_ADMIN_URL}/multitenancy/wallet/${WALLET_ID}" \
-H "Content-Type: application/json" \
-H "x-api-key: $ACAPY_ADMIN_URL_API_KEY" \
-d @-
```
16 changes: 15 additions & 1 deletion aries_cloudagent/admin/request_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@ def __init__(
profile: Profile,
*,
context: InjectionContext = None,
settings: Mapping[str, object] = None
settings: Mapping[str, object] = None,
root_profile: Profile = None,
metadata: dict = None
):
"""Initialize an instance of AdminRequestContext."""
self._context = (context or profile.context).start_scope("admin", settings)
self._profile = profile
self._root_profile = root_profile
self._metadata = metadata

@property
def injector(self) -> Injector:
Expand All @@ -39,6 +43,16 @@ def profile(self) -> Profile:
"""Accessor for the associated `Profile` instance."""
return self._profile

@property
def root_profile(self) -> Optional[Profile]:
"""Accessor for the associated root_profile instance."""
return self._root_profile

@property
def metadata(self) -> dict:
"""Accessor for the associated metadata."""
return self._metadata

@property
def settings(self) -> Settings:
"""Accessor for the context settings."""
Expand Down
23 changes: 21 additions & 2 deletions aries_cloudagent/admin/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ async def check_multitenant_authorization(request: web.Request, handler):
async def setup_context(request: web.Request, handler):
authorization_header = request.headers.get("Authorization")
profile = self.root_profile

meta_data = {}
# Multitenancy context setup
if self.multitenant_manager and authorization_header:
try:
Expand All @@ -397,6 +397,16 @@ async def setup_context(request: web.Request, handler):
profile = await self.multitenant_manager.get_profile_for_token(
self.context, token
)
(
walletid,
walletkey,
) = self.multitenant_manager.get_wallet_details_from_token(
token=token
)
meta_data = {
"wallet_id": walletid,
"wallet_key": walletkey,
}
except MultitenantManagerError as err:
raise web.HTTPUnauthorized(reason=err.roll_up)
except (jwt.InvalidTokenError, StorageNotFoundError):
Expand All @@ -411,7 +421,16 @@ async def setup_context(request: web.Request, handler):

# TODO may dynamically adjust the profile used here according to
# headers or other parameters
admin_context = AdminRequestContext(profile)
if self.multitenant_manager and authorization_header:
admin_context = AdminRequestContext(
profile=profile,
root_profile=self.root_profile,
metadata=meta_data,
)
else:
admin_context = AdminRequestContext(
profile=profile,
)

request["context"] = admin_context
request["outbound_message_router"] = responder.send
Expand Down
14 changes: 14 additions & 0 deletions aries_cloudagent/admin/tests/test_request_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,26 @@ def setUp(self):
self.ctx = test_module.AdminRequestContext(InMemoryProfile.test_profile())
assert self.ctx.__class__.__name__ in str(self.ctx)

self.ctx_with_added_attrs = test_module.AdminRequestContext(
profile=InMemoryProfile.test_profile(),
root_profile=InMemoryProfile.test_profile(),
metadata={"test_attrib_key": "test_attrib_value"},
)
assert self.ctx_with_added_attrs.__class__.__name__ in str(
self.ctx_with_added_attrs
)

def test_session_transaction(self):
sesn = self.ctx.session()
assert isinstance(sesn, ProfileSession)
txn = self.ctx.transaction()
assert isinstance(txn, ProfileSession)

sesn = self.ctx_with_added_attrs.session()
assert isinstance(sesn, ProfileSession)
txn = self.ctx_with_added_attrs.transaction()
assert isinstance(txn, ProfileSession)

async def test_session_inject_x(self):
test_ctx = test_module.AdminRequestContext.test_context({Collector: None})
async with test_ctx.session() as test_sesn:
Expand Down
4 changes: 4 additions & 0 deletions aries_cloudagent/config/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ def copy(self) -> "BaseSettings":
def extend(self, other: Mapping[str, Any]) -> "BaseSettings":
"""Merge another mapping to produce a new settings instance."""

@abstractmethod
def to_dict(self) -> dict:
"""Return a dict of the settings instance."""

def __repr__(self) -> str:
"""Provide a human readable representation of this object."""
items = ("{}={}".format(k, self[k]) for k in self)
Expand Down
1 change: 1 addition & 0 deletions aries_cloudagent/config/default_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ async def load_plugins(self, context: InjectionContext):
plugin_registry.register_plugin("aries_cloudagent.messaging.jsonld")
plugin_registry.register_plugin("aries_cloudagent.revocation")
plugin_registry.register_plugin("aries_cloudagent.resolver")
plugin_registry.register_plugin("aries_cloudagent.settings")
plugin_registry.register_plugin("aries_cloudagent.wallet")

if context.settings.get("multitenant.admin_enabled"):
Expand Down
14 changes: 7 additions & 7 deletions aries_cloudagent/config/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,11 +634,11 @@ def get_logger_with_handlers(
logger.addHandler(std_out_handler)
if did_ident:
logger = logging.LoggerAdapter(logger, {"did": did_ident})
# set log level
logger_level = (
(settings.get("log.level")).upper()
if settings.get("log.level")
else logging.INFO
)
logger.setLevel(logger_level)
# set log level
logger_level = (
(settings.get("log.level")).upper()
if settings.get("log.level")
else logging.INFO
)
logger.setLevel(logger_level)
return logger
7 changes: 7 additions & 0 deletions aries_cloudagent/config/plugin_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ def extend(self, other: Mapping[str, Any]) -> BaseSettings:
vals.update(other)
return PluginSettings(vals)

def to_dict(self) -> dict:
"""Return a dict of the settings instance."""
setting_dict = {}
for k in self:
setting_dict[k] = self[k]
return setting_dict

def get_value(self, *var_names: str, default: Any = None):
"""Fetch a setting.

Expand Down
7 changes: 7 additions & 0 deletions aries_cloudagent/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ def extend(self, other: Mapping[str, Any]) -> BaseSettings:
vals.update(other)
return Settings(vals)

def to_dict(self) -> dict:
"""Return a dict of the settings instance."""
setting_dict = {}
for k in self:
setting_dict[k] = self[k]
return setting_dict

def update(self, other: Mapping[str, Any]):
"""Update the settings in place."""
self._values.update(other)
Expand Down
4 changes: 4 additions & 0 deletions aries_cloudagent/ledger/multiple_ledger/base_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ async def _get_ledger_by_did(
) -> Optional[Tuple[str, BaseLedger, bool]]:
"""Build and submit GET_NYM request and process response."""

@abstractmethod
async def get_ledger_inst_by_id(self, ledger_id: str) -> Optional[BaseLedger]:
"""Return ledger instance by identifier."""

@abstractmethod
async def lookup_did_in_configured_ledgers(
self, did: str, cache_did: bool
Expand Down
6 changes: 6 additions & 0 deletions aries_cloudagent/ledger/multiple_ledger/indy_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ async def get_write_ledger(self) -> Optional[Tuple[str, IndySdkLedger]]:
else:
return None

async def get_ledger_inst_by_id(self, ledger_id: str) -> Optional[BaseLedger]:
"""Return BaseLedger instance."""
return self.production_ledgers.get(
ledger_id
) or self.non_production_ledgers.get(ledger_id)

async def get_prod_ledgers(self) -> Mapping:
"""Return production ledgers mapping."""
return self.production_ledgers
Expand Down
7 changes: 7 additions & 0 deletions aries_cloudagent/ledger/multiple_ledger/indy_vdr_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from ...cache.base import BaseCache
from ...core.profile import Profile
from ...ledger.base import BaseLedger
from ...ledger.error import LedgerError
from ...wallet.crypto import did_is_self_certified

Expand Down Expand Up @@ -63,6 +64,12 @@ async def get_nonprod_ledgers(self) -> Mapping:
"""Return non_production ledgers mapping."""
return self.non_production_ledgers

async def get_ledger_inst_by_id(self, ledger_id: str) -> Optional[BaseLedger]:
"""Return BaseLedger instance."""
return self.production_ledgers.get(
ledger_id
) or self.non_production_ledgers.get(ledger_id)

async def _get_ledger_by_did(
self,
ledger_id: str,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,8 @@ async def get_ledger_for_identifier(
except (MultipleLedgerManagerError, InjectionError):
pass
return (None, self.profile.inject_or(BaseLedger))

async def get_ledger_inst(self, ledger_id: str) -> Optional[BaseLedger]:
"""Return ledger instance from ledger_id set in config."""
multiledger_mgr = self.profile.inject(BaseMultipleLedgerManager)
return await multiledger_mgr.get_ledger_inst_by_id(ledger_id=ledger_id)
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ async def setUp(self):
lookup_did_in_configured_ledgers=async_mock.CoroutineMock(
return_value=("test_prod_1", self.ledger)
),
get_ledger_inst_by_id=async_mock.CoroutineMock(
return_value=self.ledger
),
),
)
self.profile.context.injector.bind_instance(BaseLedger, self.ledger)
Expand All @@ -53,6 +56,10 @@ async def test_get_ledger_for_identifier(self):
assert ledger_id == "test_prod_1"
assert ledger_inst.pool.name == "test_prod_1"

async def test_get_ledger_inst(self):
ledger_inst = await self.indy_ledger_requestor.get_ledger_inst("test_prod_1")
assert ledger_inst

async def test_get_ledger_for_identifier_is_digit(self):
ledger_id, ledger = await self.indy_ledger_requestor.get_ledger_for_identifier(
"123", 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ async def test_get_write_ledger(self):
assert ledger_id == "test_prod_1"
assert ledger_inst.pool.name == "test_prod_1"

async def test_get_ledger_inst_by_id(self):
ledger_inst = await self.manager.get_ledger_inst_by_id("test_prod_2")
assert ledger_inst
ledger_inst = await self.manager.get_ledger_inst_by_id("test_non_prod_2")
assert ledger_inst
ledger_inst = await self.manager.get_ledger_inst_by_id("test_invalid")
assert not ledger_inst

@async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open")
@async_mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close")
@async_mock.patch("indy.ledger.build_get_nym_request")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ async def test_get_write_ledger(self):
assert ledger_id == "test_prod_1"
assert ledger_inst.pool.name == "test_prod_1"

async def test_get_ledger_inst_by_id(self):
ledger_inst = await self.manager.get_ledger_inst_by_id("test_prod_2")
assert ledger_inst
ledger_inst = await self.manager.get_ledger_inst_by_id("test_non_prod_2")
assert ledger_inst
ledger_inst = await self.manager.get_ledger_inst_by_id("test_invalid")
assert not ledger_inst

@async_mock.patch("aries_cloudagent.ledger.indy_vdr.IndyVdrLedgerPool.context_open")
@async_mock.patch(
"aries_cloudagent.ledger.indy_vdr.IndyVdrLedgerPool.context_close"
Expand Down
Loading