From acf50e2552619348353d8cb421d241e657551c4d Mon Sep 17 00:00:00 2001 From: Lucas ONeil Date: Sun, 8 Sep 2024 23:11:38 -0700 Subject: [PATCH] Add variable substitution for proof configs Signed-off-by: Lucas ONeil --- .../api/verificationConfigs/helpers.py | 32 ++++++++ .../api/verificationConfigs/models.py | 14 +++- .../variableSubstitutions.py | 77 +++++++++++++++++++ 3 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 oidc-controller/api/verificationConfigs/helpers.py create mode 100644 oidc-controller/api/verificationConfigs/variableSubstitutions.py diff --git a/oidc-controller/api/verificationConfigs/helpers.py b/oidc-controller/api/verificationConfigs/helpers.py new file mode 100644 index 00000000..c0e9f2ba --- /dev/null +++ b/oidc-controller/api/verificationConfigs/helpers.py @@ -0,0 +1,32 @@ +from .variableSubstitutions import variable_substitution_map + + +def replace_proof_variables(proof_req_dict: dict) -> dict: + """ + Recursively replaces variables in the proof request with actual values. + The map is provided by imported variable_substitution_map. + Additional variables can be added to the map in the variableSubstitutions.py file, + or other dynamic functionality. + + Args: + proof_req_dict (dict): The proof request dictionary from the resolved config. + + Returns: + dict: The updated proof request dictionary with placeholder variables replaced. + """ + + for k, v in proof_req_dict.items(): + # If the value is a dictionary, recurse + if isinstance(v, dict): + replace_proof_variables(v) + # If the value is a list, iterate trhough list items and recurse + elif isinstance(v, list): + for i in v: + if isinstance(i, dict): + replace_proof_variables(i) + # If the value is a string and matches a key in the map, replace it + elif isinstance(v, str): + if v in variable_substitution_map: + proof_req_dict[k] = variable_substitution_map[v]() + # Base case: If the value is not a dict, list, or matching string, do nothing + return proof_req_dict diff --git a/oidc-controller/api/verificationConfigs/models.py b/oidc-controller/api/verificationConfigs/models.py index fd5e92ed..11de8958 100644 --- a/oidc-controller/api/verificationConfigs/models.py +++ b/oidc-controller/api/verificationConfigs/models.py @@ -1,9 +1,11 @@ +from datetime import datetime import time from typing import Optional, List -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, PrivateAttr from .examples import ex_ver_config from ..core.config import settings +from .helpers import replace_proof_variables # Slightly modified from ACAPY models. @@ -44,6 +46,9 @@ class VerificationConfigBase(BaseModel): generate_consistent_identifier: Optional[bool] = Field(default=False) include_v1_attributes: Optional[bool] = Field(default=False) + def get_now(self) -> int: + return int(time.time()) + def generate_proof_request(self): result = { "name": "proof_requested", @@ -61,15 +66,16 @@ def generate_proof_request(self): "from": int(time.time()), "to": int(time.time()), } - # TODO add I indexing - for req_pred in self.proof_request.requested_predicates: + for i, req_pred in enumerate(self.proof_request.requested_predicates): label = req_pred.label or "req_pred_" + str(i) result["requested_predicates"][label] = req_pred.dict(exclude_none=True) if settings.SET_NON_REVOKED: - result["requested_attributes"][label]["non_revoked"] = { + result["requested_predicates"][label]["non_revoked"] = { "from": int(time.time()), "to": int(time.time()), } + # Recursively check for subistitution variables and invoke the apporpriate replacement function + result = replace_proof_variables(result) return result model_config = ConfigDict(json_schema_extra={"example": ex_ver_config}) diff --git a/oidc-controller/api/verificationConfigs/variableSubstitutions.py b/oidc-controller/api/verificationConfigs/variableSubstitutions.py new file mode 100644 index 00000000..17e9c09a --- /dev/null +++ b/oidc-controller/api/verificationConfigs/variableSubstitutions.py @@ -0,0 +1,77 @@ +""" +This file contains the VariableSubstitutionMap class, which provides a mapping of static variables +that can be used in a proof. Other users of this project can add their own variable substitutions +or override the entire file to suit their own needs. +""" + +from datetime import datetime, timedelta +import time +import re + + +class VariableSubstitutionMap: + def __init__(self): + # Map of static variables that can be used in a proof + # This class defines threshold_years_X as a dynamic one + self.static_map = { + "$now": self.get_now, + "$today_str": self.get_today_date, + "$tomorrow_str": self.get_tomorrow_date, + } + + def get_threshold_years_date(self, years: int) -> int: + """ + Calculate the threshold date for a given number of years. + + Args: + years (int): The number of years to subtract from the current year. + + Returns: + int: The current date minux X years in YYYYMMDD format. + """ + threshold_date = datetime.today().replace(year=datetime.today().year - years) + return int(threshold_date.strftime("%Y%m%d")) + + def get_now(self) -> int: + """ + Get the current timestamp. + + Returns: + int: The current timestamp in seconds since the epoch. + """ + return int(time.time()) + + def get_today_date(self) -> str: + """ + Get today's date in YYYYMMDD format. + + Returns: + str: Today's date in YYYYMMDD format. + """ + return datetime.today().strftime("%Y%m%d") + + def get_tomorrow_date(self) -> str: + """ + Get tomorrow's date in YYYYMMDD format. + + Returns: + str: Tomorrow's date in YYYYMMDD format. + """ + return (datetime.today() + timedelta(days=1)).strftime("%Y%m%d") + + # For "dynamic" variables, we use a regex to match the key and return a lambda function + # So a proof request can use $threshold_years_X to get the threshold birthdate for X years + def __contains__(self, key: str) -> bool: + return key in self.static_map or re.match(r"\$threshold_years_(\d+)", key) + + 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))) + raise KeyError(f"Key {key} not found in format_args_function_map") + + +# Create an instance of the custom mapping class +variable_substitution_map = VariableSubstitutionMap()