-
Notifications
You must be signed in to change notification settings - Fork 2
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
Feature/25 add jwt config #35
Conversation
Coverage reportThe coverage rate went from
Diff Coverage details (click to unfold)src/config.py
src/entities/engine/engine.py
src/entities/models/dto.py
src/main.py
src/oauth2/oauth2_admin.py
|
src/.env.local
Outdated
INST_CONN=postgresql+asyncpg://${INST_DB_USER}:${INST_DB_PWD}@${INST_DB_HOST}/${INST_DB_NAME} | ||
JWT_OPTS=verify_at_hash:False,verify_aud:False,verify_iss:False |
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.
I feel like these should be parameters we can tune individually, like:
- JWT_OPTS_VERIFY_AT_HASH
- JWT_OPTS_VERIFY_AUD
- JWT_OPTS_ISS
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.
agreed, just thought of getting env vars by prefix, so will be implementing that approach.
src/oauth2/oauth2_admin.py
Outdated
pairs = opts_string.split(",") | ||
for pair in pairs: | ||
[key, value] = pair.split(":", 1) | ||
jwt_opts[key] = ast.literal_eval(value) |
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.
Since we already have Pydantic in place, we might as well use their much friendlier boolean parsing.
jwt_opts[key] = ast.literal_eval(value) | |
pydantic.parse_obj_as(bool, envvar_name) |
Also, whether we use pydantic or the ast route, we should wrap this in a try/except and raise a friendly error.
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.
will try/except, I'm thinking app should fail to start in this instance considering the configuration is vital to everything else.
also, there is one option that's not bool; I'm thinking sticking with ast, or do we want to make a distinction on that?
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.
will try/except, I'm thinking app should fail to start in this instance considering the configuration is vital to everything else.
Agreed. It should be a hard fail.
also, there is one option that's not bool; I'm thinking sticking with ast, or do we want to make a distinction on that?
I like Pydantic as it parses all the values that you'd assume are legit for a boolean: True,true,Yes,no,ON,Off,0,1...etc. The AST is for parsing the Python language itself. You basically have to make it exactly True
or False
, otherwise 💥. Also, you have a chance of parsing values into real Python values. For example, if you passed 0
or 1
, you'd get an int
, and then the app would likely fail down the line, perhaps in an unexpected way.
Also, I'm guessing there'll be more configs added to this project. FastAPI has a Pydantic-based Settings feature that seems like it'd handle our configs pretty cleanly.
- https://fastapi.tiangolo.com/advanced/settings/#pydantic-settings
- https://docs.pydantic.dev/latest/concepts/pydantic_settings/
I didn't want to add that to this PR since it's a bigger change than we'd originally discussed, but it seems like a nice baby step in that direction.
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.
Oh yeah, and what's the option that's not a bool
?
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.
leeway
is the option name, I remember vaguely with it being about timing.
src/oauth2/oauth2_admin.py
Outdated
def get_jwt_opts(opts_string: str) -> Dict[str, bool | int]: | ||
""" | ||
Parses out the opts_string into JWT options dictionary. | ||
|
||
Args: | ||
opts_string (str): comma separated key value pairs in the form of "key1:True,key2:False" | ||
all options are boolean, with exception of 'leeway' being int | ||
valid options can be found here: | ||
https://github.com/mpdavis/python-jose/blob/4b0701b46a8d00988afcc5168c2b3a1fd60d15d8/jose/jwt.py#L81 | ||
|
||
Returns: | ||
dict: dictionary of options supported by jwt, mentioned in link above | ||
""" | ||
jwt_opts = {} | ||
if opts_string: | ||
pairs = opts_string.split(",") | ||
for pair in pairs: | ||
[key, value] = pair.split(":", 1) | ||
jwt_opts[key] = ast.literal_eval(value) | ||
return jwt_opts | ||
|
||
|
||
JWT_OPTS = get_jwt_opts(os.getenv("JWT_OPTS", "")) |
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.
...and if we made them separate envvars, we could make this something like:
from pydantic import parse_obj_as
from pydantic.error_wrappers import ValidationError
import os
# Enable all jwt_opts settings by default
JWT_OPTS = {
"verify_at_hash": True,
"verify_aud": True,
"verify_iss": True,
}
# Override the defaults with envvars prefixed with JWT_OPTS_
for jwt_opt_key in JWT_OPTS:
env_key = f"JWT_OPTS_{jwt_opt_key.upper()}"
env_val = os.getenv(env_key, None)
if env_val:
try:
JWT_OPTS[jwt_opt_key] = parse_obj_as(bool, env_val)
except ValidationError:
raise ValueError(f'jwt_opts setting "{env_key}" value of "{env_val}" could not be parsed into a boolean')
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.
I was thinking of using JWT_
prefix parsing to get all possible configs from env vars, so we can dynamically add additional configs as needed.
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.
I started off that way, filtering the list of envvars, but that didn't handle default values if an envvar wasn't set, or what'd happen if a envvar was added that had that prefix, but the suffix was invalid. Will it get ignored? Will it blow up? Having and explicit dict
for the supported values, each with a default, and then looking up just those from the env seemed safer.
I'm not opposed to the other option. I just didn't know how jws_opts
would handle those situations, and I didn't feel like doing the testing, so I went with safe.
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.
gotcha, invalid suffices gets ignored; I think adding a docstring should serve the purpose of what the valid options are; also gives the flexibility that if any additional configs arise, no code changes are needed.
will also take this opportunity to refactor how we deal with env vars by creating constants in |
I think before we go too far down the road of creating our own thing, it's worth looking into FastAPI's Pydantic |
oh nice, yeap, Settings is exactly what I had in mind. |
Cool. If you want to go that route for this PR, 👍. I think it's the right way to go long term. I was initially hesitant to add it here since we hadn't planned for that work, but these type of settings seem like the right fit for it, so why go do a one-off custom thing we'll probably drop in the future anyway!? If it starts to look like it's going to stretch on, though, let's circle back to something simpler for now. |
Yeah, I think I'll change the ticket description, just adding jwt options is a really small change anyway, think it makes sense for this scope creep, I've been meaning to refactor the env var handling for a while now, might as well do it now. |
@@ -15,7 +15,7 @@ class FinancialInsitutionDomainDto(FinancialInsitutionDomainBase): | |||
lei: str | |||
|
|||
class Config: | |||
orm_mode = True | |||
from_attributes = True |
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.
orm_mode
was deprecated and renamed to from_attributes
: https://docs.pydantic.dev/2.4/migration/#changes-to-config
|
||
def __init__(self, **data): | ||
super().__init__(**data) | ||
self.set_jwt_opts() |
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.
using pydantic settings with prefixes still required defining the keys, and the extra
param for the config wouldn't pull in the undefined configs if a prefix is specified; so jwt opts still needed some custom transformation.
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.
Overall, looks good. The way jwt_opts
gets initialized is a little more complicated than I thought it'd be, but I think that's OK. Thanks for adding the docstring on that one.
I added a few suggestions, mostly around using more of Pydantic's built-in types. Some of those seem pretty handy.
Co-authored-by: Hans Keeler <[email protected]>
closes #25