Skip to content

Commit

Permalink
Merge pull request #675 from bcgov/variable-substitution
Browse files Browse the repository at this point in the history
Add User Definable Variable Substitution
  • Loading branch information
esune authored Nov 8, 2024
2 parents d7cd916 + ec1ee82 commit 078e47e
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 13 deletions.
11 changes: 11 additions & 0 deletions charts/vc-authn-oidc/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ spec:
- name: auth-session-ttl
configMap:
name: {{ include "global.fullname" . }}-session-timeout
- name: custom-variable-substitution
configMap:
name: {{ include "global.fullname" . }}-variable-substitution-config
items:
- key: user_variable_substitution.py
path: user_variable_substitution.py
containers:
- name: {{ .Chart.Name }}
securityContext:
Expand Down Expand Up @@ -72,6 +78,8 @@ spec:
value: {{ .Values.controller.presentationExpireTime | quote }}
# - name: CONTROLLER_SESSION_TIMEOUT_CONFIG_FILE
# value: /home/aries/sessiontimeout.json
- name: CONTROLLER_VARIABLE_SUBSTITUTION_OVERRIDE
value: /home/aries/user_variable_substitution.py
- name: CONTROLLER_PRESENTATION_CLEANUP_TIME
value: {{ .Values.controller.sessionTimeout.duration | quote }}
- name: ACAPY_AGENT_URL
Expand Down Expand Up @@ -133,6 +141,9 @@ spec:
- name: auth-session-ttl
mountPath: /home/aries/sessiontimeout.json
subPath: sessiontimeout.json
- name: custom-variable-substitution
mountPath: /home/aries/user_variable_substitution.py
subPath: user_variable_substitution.py
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
Expand Down
2 changes: 2 additions & 0 deletions docker/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ services:
- CONTROLLER_PRESENTATION_EXPIRE_TIME=${CONTROLLER_PRESENTATION_EXPIRE_TIME}
- CONTROLLER_PRESENTATION_CLEANUP_TIME=${CONTROLLER_PRESENTATION_CLEANUP_TIME}
- CONTROLLER_SESSION_TIMEOUT_CONFIG_FILE=${CONTROLLER_SESSION_TIMEOUT_CONFIG_FILE}
- CONTROLLER_VARIABLE_SUBSTITUTION_OVERRIDE=${CONTROLLER_VARIABLE_SUBSTITUTION_OVERRIDE}
- ACAPY_TENANCY=${AGENT_TENANT_MODE}
- ACAPY_AGENT_URL=${AGENT_ENDPOINT}
- ACAPY_ADMIN_URL=${AGENT_ADMIN_URL}
Expand All @@ -45,6 +46,7 @@ services:
volumes:
- ../oidc-controller:/app:rw
- ./oidc-controller/config/sessiontimeout.json:/home/aries/sessiontimeout.json
- ./oidc-controller/config/user_variable_substitution.py:/home/aries/user_variable_substitution.py
networks:
- vc_auth

Expand Down
3 changes: 3 additions & 0 deletions docker/manage
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ configureEnvironment() {
# The path to the auth_session timeouts config file
export CONTROLLER_SESSION_TIMEOUT_CONFIG_FILE="/home/aries/sessiontimeout.json"

# Extend Variable Substitutions
export CONTROLLER_VARIABLE_SUBSTITUTION_OVERRIDE="/home/aries/user_variable_substitution.py"

#controller app settings
export INVITATION_LABEL=${INVITATION_LABEL:-"VC-AuthN"}
export SET_NON_REVOKED="True"
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
def sub_days_plus_one(days: str) -> int:
"""Strings like '$sub_days_plus_one_4' will be replaced with the
final number incremented by one. In this case 5.
$sub_days_plus_one_4 -> 5
$sub_days_plus_one_10 -> 11"""
return int(days) + 1


variable_substitution_map.add_variable_substitution(
r"\$sub_days_plus_one_(\d+)", sub_days_plus_one
)


def sub_string_for_sure(_: str) -> str:
"""Turns strings like $sub_string_for_sure_something into the string 'sure'
$sub_string_for_sure_something -> sure
$sub_string_for_sure_i -> sure
"""
return "sure"


variable_substitution_map.add_variable_substitution(
r"\$sub_string_for_sure_(.+)", sub_string_for_sure
)
42 changes: 36 additions & 6 deletions docs/ConfigurationGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,42 @@ and at runtime when the user navigates to the QR code page, the proof would incl
See the `oidc-controller\api\verificationConfigs\variableSubstitutions.py` file for implementations.

#### Customizing variables
For user defined variable substitutions users can set the environment
variable to point at a python file defining new substitutions.

In `oidc-controller\api\verificationConfigs\variableSubstitutions.py` there are the built-in variables above.
For an advanced use case, if you require further customization, it could be possible to just replace that `variableSubstitutions.py` file
in a VC-AuthN implementation and the newly introduced variables would be run if they are included in a proof request configuration.
##### User Defined Variable API
In `oidc-controller\api\verificationConfigs\variableSubstitutions.py`
you will find the method `add_variable_substitution` which can be used
to modify the existing instance of `VariableSubstitutionMap` named
`variable_substitution_map`.

For regular variables they can be added to the `static_map`, mapping your variable name to a function doing the operation.
For "dynamic" ones alter `__contains__` and `__getitem__` to use a regex to parse and extract what is needed.
Takes a valid regular expression `pattern` and a function who's
arguments correspond with each regex group `substitution_function`. Each
captured regex group will be passed to the function as a `str`.

The file `oidc-controller\api\verificationConfigs\helpers.py` contains the function that recurses through the config doing any substitutions, so it would pick up whatever is available in `variableSubstitutions.py`
Here is an example python file that would define a new variable
substitution `$today_plus_x_times_y` which will add X days multiplied
by Y days to today's date

```python
from datetime import datetime, timedelta

def today_plus_times(added_days: str, multiplied_days: str) -> int:
return int(
((datetime.today() + timedelta(days=int(added_days))) * timedelta(days=int(multiplied_days)))
).strftime("%Y%m%d"))

# variable_substitution_map will already be defined in variableSubstitutions.py
variable_substitution_map.add_variable_substitution(r"\$today_plus_(\d+)_times_(\d+)", today_plus_times)
```

For an example of this python file see `docker/oidc-controller/config/user_variable_substitution_example.py`

All that is necessary is the adding of substitution variables.These
changes will be applied by vc-authn during startup. The variable
`variable_substitution_map` **will already be defined**.

After loading the python file during the service startup each new user
defined variable is logged for confirmation. Any failures to load
these changes will be logged. If no new definitions are found
indication of this will also be logged
4 changes: 4 additions & 0 deletions oidc-controller/api/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pathlib import Path
from pydantic_settings import BaseSettings
from pydantic import ConfigDict
from typing import Any

import structlog

Expand Down Expand Up @@ -227,6 +228,9 @@ class GlobalConfig(BaseSettings):
)
SET_NON_REVOKED: bool = strtobool(os.environ.get("SET_NON_REVOKED", True))

CONTROLLER_VARIABLE_SUBSTITUTION_OVERRIDE: str | None = os.environ.get(
"CONTROLLER_VARIABLE_SUBSTITUTION_OVERRIDE"
)
model_config = ConfigDict(case_sensitive=True)


Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import time
from typing_extensions import Callable
import pytest
from datetime import datetime, timedelta
from api.verificationConfigs.variableSubstitutions import VariableSubstitutionMap
Expand Down Expand Up @@ -27,7 +28,17 @@ def test_get_threshold_years_date():
expected_date = (
datetime.today().replace(year=datetime.today().year - years).strftime("%Y%m%d")
)
assert vsm.get_threshold_years_date(years) == int(expected_date)
assert vsm.get_threshold_years_date(str(years)) == int(expected_date)


def test_user_defined_func():
vsm = VariableSubstitutionMap()
func: Callable[[int], int] = lambda x, y: int(x) + int(y)
vsm.add_variable_substitution(r"\$years since (\d+) (\d+)", func)
days = 10
years = 22
assert f"$years since {years} {days}" in vsm
assert vsm[f"$years since {years} {days}"]() == years + days


def test_contains_static_variable():
Expand Down
72 changes: 66 additions & 6 deletions oidc-controller/api/verificationConfigs/variableSubstitutions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@
from datetime import datetime, timedelta
import time
import re
import copy

import structlog
from typing_extensions import Callable
from ..core.config import settings

logger: structlog.typing.FilteringBoundLogger = structlog.getLogger(__name__)

SubstitutionFunction = Callable[..., int | str]


class VariableSubstitutionMap:
Expand All @@ -20,7 +29,25 @@ def __init__(self):
"$tomorrow_int": self.get_tomorrow_date,
}

def get_threshold_years_date(self, years: int) -> int:
self.user_static_map: dict[re.Pattern[str], SubstitutionFunction] = {
re.compile(r"\$threshold_years_(\d+)"): self.get_threshold_years_date
}

def add_variable_substitution(
self, pattern: str, substitution_function: SubstitutionFunction
):
"""Takes a valid regular expression PATTERN and a function
who's arguments correspond with each regex group
SUBSTITUTION_FUNCTION. Each captured regex group will be
passed to the function as a str.
Examples:
vsm.add_variable_substitution(r\"\\$years since (\\d+) (\\d+)\", lambda x, y: int(x) + int(y))
vsm[f"$years since {10} {12}"] => 22
"""
self.user_static_map[re.compile(pattern)] = substitution_function

def get_threshold_years_date(self, years: str) -> int:
"""
Calculate the threshold date for a given number of years.
Expand All @@ -30,7 +57,9 @@ def get_threshold_years_date(self, years: int) -> int:
Returns:
int: The current date minux X years in YYYYMMDD format.
"""
threshold_date = datetime.today().replace(year=datetime.today().year - years)
threshold_date = datetime.today().replace(
year=datetime.today().year - int(years)
)
return int(threshold_date.strftime("%Y%m%d"))

def get_now(self) -> int:
Expand Down Expand Up @@ -63,16 +92,47 @@ def get_tomorrow_date(self) -> int:
# For "dynamic" variables, use a regex to match the key and return a lambda function
# So a proof request can use $threshold_years_X to get the years back for X years
def __contains__(self, key: str) -> bool:
return key in self.static_map or re.match(r"\$threshold_years_(\d+)", key)
res = key in self.static_map
if not res:
for i in self.user_static_map.keys():
if re.match(i, key):
return True
return res

def __getitem__(self, key: str):
if key in self.static_map:
return self.static_map[key]
match = re.match(r"\$threshold_years_(\d+)", key)
if match:
return lambda: self.get_threshold_years_date(int(match.group(1)))
for i, j in self.user_static_map.items():
if nmatch := re.match(i, key):
return lambda: j(*nmatch.groups())
raise KeyError(f"Key {key} not found in format_args_function_map")


# Create an instance of the custom mapping class
variable_substitution_map = VariableSubstitutionMap()


def apply_user_variables():
try:
with open(settings.CONTROLLER_VARIABLE_SUBSTITUTION_OVERRIDE) as user_file:
code = user_file.read()
except TypeError:
logger.warning("No user defined variable substitutions file provided")
return None
except FileNotFoundError:
logger.warning("User defined variable substitutions file could not be found")
return None
else:
og_substitution_map = copy.copy(variable_substitution_map.user_static_map)
exec(code)
if len(variable_substitution_map.user_static_map) <= 1:
logger.info("No new user created variable substitution where found")

for pattern, func in variable_substitution_map.user_static_map.items():
if pattern not in og_substitution_map:
logger.info(
f'New user created variable substitution: The pattern "{pattern.pattern}" is now mapped to the function {func.__name__}'
)


apply_user_variables()

0 comments on commit 078e47e

Please sign in to comment.