-
Notifications
You must be signed in to change notification settings - Fork 32
[uss_qualifier] Add auth adapter expectations to authorization verification #1654
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
Changes from all commits
46bcfef
208fc48
1e9d675
07e8b5f
8f04277
2379500
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,9 @@ | ||||||||||||||||||||||
| import importlib | ||||||||||||||||||||||
| import inspect | ||||||||||||||||||||||
| import pkgutil | ||||||||||||||||||||||
| from typing import Any, Optional | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| from implicitdict import ImplicitDict | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| _modules_imported = set() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -47,3 +50,77 @@ def fullname(class_type: type) -> str: | |||||||||||||||||||||
|
|
||||||||||||||||||||||
| def calling_function_name(levels: int = 0) -> str: | ||||||||||||||||||||||
| return inspect.stack()[levels + 1].function | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| class AttributeValuePair(ImplicitDict): | ||||||||||||||||||||||
| name: str | ||||||||||||||||||||||
| """The attribute that is expected to have a particular value. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Nested attributes are accepted (e.g., `"foo.bar"`).""" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| equals_string_value: Optional[str] | ||||||||||||||||||||||
| """The attribute value is this string.""" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| equals_number_value: Optional[float] | ||||||||||||||||||||||
| """The attribute value is exactly this number. Note that this may not be the desirable behavior when comparing float values.""" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def _has_attr(obj: Any, attr_name: str) -> bool: | ||||||||||||||||||||||
| if "." in attr_name: | ||||||||||||||||||||||
| levels = attr_name.split(".") | ||||||||||||||||||||||
| if not hasattr(obj, levels[0]): | ||||||||||||||||||||||
| return False | ||||||||||||||||||||||
| return _has_attr(getattr(obj, levels[0]), ".".join(levels[1:])) | ||||||||||||||||||||||
| else: | ||||||||||||||||||||||
| return hasattr(obj, attr_name) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def _get_attr_value(obj: Any, attr_name: str) -> Any: | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This intends to do the same as: monitoring/monitoring/uss_qualifier/scenarios/astm/netrid/common_dictionary_evaluator.py Lines 1572 to 1581 in dfa29f7
Consider deduplicating
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function does have similar behavior to the content added to inspection.py in this PR, but it's not identical (e.g., _dotted_get returns None when keys can't be found whereas _get_attr_value errors). I'm not sure deduplicating this particular function would be worth it after the changes necessary to expose _get_attr_value publicly and change its functionality to be polymorphic (raise an AttributeError or return None), especially since it's only a few lines long in both places. If we did want to harmonize the two functions, I think that would probably be in a follow-up PR since both functions are currently private, so the harmonization would be a non-trivial addition to this PR. If there was already something available in the codebase to perform the dotted-get task added in this PR, I agree we wouldn't want to merge this PR until we avoided introducing unnecessary duplication. However, I don't think that's the case here with this _dotted_get, especially since it is not currently in a position to be reused (in common_dictionary_evaluator.py rather than somewhere in monitorlib or similar). |
||||||||||||||||||||||
| if "." in attr_name: | ||||||||||||||||||||||
| base, remaining = attr_name.split(".", 1) | ||||||||||||||||||||||
| return _get_attr_value(getattr(obj, base), remaining) | ||||||||||||||||||||||
| else: | ||||||||||||||||||||||
| return getattr(obj, attr_name) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def evaluate_attributes( | ||||||||||||||||||||||
| obj: Any, | ||||||||||||||||||||||
| expectations: list[AttributeValuePair], | ||||||||||||||||||||||
| ) -> list[str]: | ||||||||||||||||||||||
| """Evaluates an object against a set of AttributeValuePair expectations. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||
| A list of string descriptions detailing any failed expectations. An empty list signifies success. | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
| failures: list[str] = [] | ||||||||||||||||||||||
| for pair in expectations: | ||||||||||||||||||||||
| attr_name = pair.name | ||||||||||||||||||||||
| if not _has_attr(obj, attr_name): | ||||||||||||||||||||||
| failures.append( | ||||||||||||||||||||||
| f"Required attribute '{attr_name}' is entirely absent from the object." | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| continue | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| actual_val = _get_attr_value(obj, attr_name) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if "equals_string_value" in pair and pair.equals_string_value is not None: | ||||||||||||||||||||||
| if not isinstance(actual_val, str): | ||||||||||||||||||||||
| failures.append( | ||||||||||||||||||||||
| f"Attribute '{attr_name}' expected to be of type 'str', but observed type '{type(actual_val).__name__}'." | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| elif actual_val != pair.equals_string_value: | ||||||||||||||||||||||
| failures.append( | ||||||||||||||||||||||
| f"Attribute '{attr_name}': Expected string value '{pair.equals_string_value}', but observed '{actual_val}'." | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if "equals_number_value" in pair and pair.equals_number_value is not None: | ||||||||||||||||||||||
| if not isinstance(actual_val, (int, float)): | ||||||||||||||||||||||
| failures.append( | ||||||||||||||||||||||
| f"Attribute '{attr_name}' expected to be numeric, but observed type '{type(actual_val).__name__}'." | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| elif actual_val != pair.equals_number_value: | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider comparison with delta to avoid float precision issues
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree this is a valid concern as exact comparison of float numbers is often not best accomplished with exact equality. I considered switching to |
||||||||||||||||||||||
| failures.append( | ||||||||||||||||||||||
| f"Attribute '{attr_name}': Expected numeric value {pair.equals_number_value}, but observed {actual_val}." | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return failures | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,10 @@ | ||
| from .access_token_expectations import ( | ||
| AccessTokensExpectationsResource as AccessTokensExpectationsResource, | ||
| ) | ||
| from .auth_adapter import AuthAdapterResource as AuthAdapterResource | ||
| from .auth_adapter import ( | ||
| AuthAdapterExpectationsResource as AuthAdapterExpectationsResource, | ||
| ) | ||
| from .auth_adapter import ( | ||
| AuthAdapterResource as AuthAdapterResource, | ||
| ) | ||
| from .client_identity import ClientIdentityResource as ClientIdentityResource |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| { | ||
| "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/monitorlib/inspection/AttributeValuePair.json", | ||
| "$schema": "https://json-schema.org/draft/2020-12/schema", | ||
| "description": "monitoring.monitorlib.inspection.AttributeValuePair, as defined in monitoring/monitorlib/inspection.py", | ||
| "properties": { | ||
| "$ref": { | ||
| "description": "Path to content that replaces the $ref", | ||
| "type": "string" | ||
| }, | ||
| "equals_number_value": { | ||
| "description": "The attribute value is exactly this number. Note that this may not be the desirable behavior when comparing float values.", | ||
| "type": [ | ||
| "number", | ||
| "null" | ||
| ] | ||
| }, | ||
| "equals_string_value": { | ||
| "description": "The attribute value is this string.", | ||
| "type": [ | ||
| "string", | ||
| "null" | ||
| ] | ||
| }, | ||
| "name": { | ||
| "description": "The attribute that is expected to have a particular value.\n\nNested attributes are accepted (e.g., `\"foo.bar\"`).", | ||
| "type": "string" | ||
| } | ||
| }, | ||
| "required": [ | ||
| "name" | ||
| ], | ||
| "type": "object" | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: you can
base, rest = attr_name.split(".", 2)and avoid the array addressing and join.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like it needs to be
.split(".", 1), but done.