diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c50a6d38a..d8b85d0fb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,11 @@ dcicutils Change Log ---------- +8.3.0 +========= + +* Updates for RAS to Redis API + 8.2.0 ===== * 2023-11-02 diff --git a/dcicutils/deployment_utils.py b/dcicutils/deployment_utils.py index 69716a990..c17781a4e 100644 --- a/dcicutils/deployment_utils.py +++ b/dcicutils/deployment_utils.py @@ -426,7 +426,7 @@ def build_ini_file_from_template(cls, template_file_name, init_file_name, *, indexer=None, index_server=None, sentry_dsn=None, tibanna_cwls_bucket=None, tibanna_output_bucket=None, application_bucket_prefix=None, foursight_bucket_prefix=None, - auth0_domain=DEFAULT_AUTH0_DOMAIN, auth0_client=None, auth0_secret=None, + auth0_domain=None, auth0_client=None, auth0_secret=None, auth0_allowed_connections=None, re_captcha_key=None, re_captcha_secret=None, redis_server=None, @@ -680,7 +680,7 @@ def build_ini_stream_from_template(cls, template_file_name, init_file_stream, *, sentry_dsn = sentry_dsn or os.environ.get("ENCODED_SENTRY_DSN", "") # Auth0 Configuration - auth0_domain = auth0_domain or os.environ.get("ENCODED_AUTH0_DOMAIN", "") + auth0_domain = auth0_domain or os.environ.get("ENCODED_AUTH0_DOMAIN", cls.DEFAULT_AUTH0_DOMAIN) auth0_client = auth0_client or os.environ.get("ENCODED_AUTH0_CLIENT", "") auth0_secret = auth0_secret or os.environ.get("ENCODED_AUTH0_SECRET", "") auth0_allowed_connections = auth0_allowed_connections or os.environ.get("ENCODED_AUTH0_ALLOWED_CONNECTIONS", "") diff --git a/dcicutils/redis_tools.py b/dcicutils/redis_tools.py index fb6418d61..ba132fb67 100644 --- a/dcicutils/redis_tools.py +++ b/dcicutils/redis_tools.py @@ -44,11 +44,12 @@ def _build_redis_key(namespace: str, token: str) -> str: """ return f'{namespace}:session:{token}' - def __init__(self, *, namespace: str, jwt: str, token=None, expiration=None): + def __init__(self, *, namespace: str, jwt: str, email: str, token=None, expiration=None): """ Creates a Redis Session object, storing a hash of the JWT into Redis and returning this value as the session token. :param namespace: namespace to build key under, for example the env name :param jwt: jwt generated for this user + :param email: email verified for this user :param token: value of token if passed, if not one will be generated :param expiration: expiration of token if passed, if not new expiration will be generated """ @@ -59,11 +60,12 @@ def __init__(self, *, namespace: str, jwt: str, token=None, expiration=None): self.namespace = namespace self.redis_key = self._build_redis_key(self.namespace, self.session_token) self.jwt = jwt + self.email = email self.expiration = expiration or self._build_session_expiration() def __eq__(self, other): """ Evaluates equality of two session objects based on the value of the session hset """ - return (self.redis_key == other.redis_key) and (self.jwt == other.jwt) + return (self.redis_key == other.redis_key) and (self.jwt == other.jwt) and (self.email == other.email) def get_session_token(self) -> str: """ Extracts the session token stored on this object """ @@ -81,6 +83,10 @@ def get_jwt(self) -> str: """ Returns the JWT set on this session token object """ return self.jwt + def get_email(self) -> str: + """ Returns the email set on this session token object """ + return self.email + @classmethod def from_redis(cls, *, redis_handler: RedisBase, namespace: str, token: str): """ Builds a RedisSessionToken from an existing record - allows extracting JWT @@ -93,11 +99,14 @@ def from_redis(cls, *, redis_handler: RedisBase, namespace: str, token: str): redis_key = f'{namespace}:session:{token}' redis_entry = redis_handler.get(redis_key) if redis_entry: + jwt_and_email = redis_entry.split(':') + jwt = jwt_and_email[0] + email = jwt_and_email[1] if len(jwt_and_email) > 1 else None expiration = redis_handler.ttl(redis_key) - return cls(namespace=namespace, jwt=redis_entry, + return cls(namespace=namespace, jwt=jwt, email=email, token=token, expiration=expiration) - def decode_jwt(self, audience: str, secret: str, leeway: int = 30) -> dict: + def decode_jwt(self, audience: str, secret: str, leeway: int = 30, algorithms: list = ['HS256']) -> dict: """ Decodes JWT to grab info such as the email :param audience: audience under which to decode, typically Auth0Client :param secret: secret to decrypt using, typically Auth0Secret @@ -105,7 +114,7 @@ def decode_jwt(self, audience: str, secret: str, leeway: int = 30) -> dict: :return: a decoded JWT in dictionary format """ return jwt.decode(self.jwt, secret, audience=audience, leeway=leeway, - options={'verify_signature': True}, algorithms=['HS256']) + options={'verify_signature': True}, algorithms=algorithms) def store_session_token(self, *, redis_handler: RedisBase) -> bool: """ Stores the created session token object as an hset in Redis @@ -113,7 +122,7 @@ def store_session_token(self, *, redis_handler: RedisBase) -> bool: :return: True if successful, raise Exception otherwise """ try: - redis_handler.set(self.redis_key, self.jwt, exp=self.expiration) + redis_handler.set(self.redis_key, f'{self.jwt}:{self.email or ""}', exp=self.expiration) except Exception as e: log.error(str(e)) raise RedisException() @@ -129,10 +138,11 @@ def validate_session_token(self, *, redis_handler: RedisBase) -> bool: return False # if it doesn't exist it's not valid return True # if it does exist it must be valid since we always send with TTL - def update_session_token(self, *, redis_handler: RedisBase, jwt: str) -> bool: + def update_session_token(self, *, redis_handler: RedisBase, jwt: str, email: str) -> bool: """ Refreshes the session token, jwt (if different) and expiration stored in Redis :param redis_handler: handle to Redis API :param jwt: jwt of user + :param email: email of user :return: True if successful, raise Exception otherwise """ # remove old token @@ -142,6 +152,7 @@ def update_session_token(self, *, redis_handler: RedisBase, jwt: str) -> bool: self.redis_key = self._build_redis_key(self.namespace, self.session_token) self.expiration = self._build_session_expiration() self.jwt = jwt + self.email = email return self.store_session_token(redis_handler=redis_handler) def delete_session_token(self, *, redis_handler: RedisBase) -> bool: diff --git a/pyproject.toml b/pyproject.toml index 5c3863c3a..6d6847fd5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "dcicutils" -version = "8.2.0" +version = "8.3.0" description = "Utility package for interacting with the 4DN Data Portal and other 4DN resources" authors = ["4DN-DCIC Team "] license = "MIT" diff --git a/test/test_redis_tools.py b/test/test_redis_tools.py index 4e63bbdf4..b533ca8b4 100644 --- a/test/test_redis_tools.py +++ b/test/test_redis_tools.py @@ -23,6 +23,7 @@ def test_redis_session_basic(self, redisdb): rd = RedisBase(redisdb) session_token = RedisSessionToken( namespace=self.NAMESPACE, + email=self.DUMMY_EMAIL, jwt=self.DUMMY_JWT ) session_token.store_session_token(redis_handler=rd) @@ -34,7 +35,7 @@ def test_redis_session_basic(self, redisdb): assert not session_token.validate_session_token(redis_handler=rd) # update with a new token and expiration session_token.redis_key = working_token - session_token.update_session_token(redis_handler=rd, jwt=self.DUMMY_JWT) + session_token.update_session_token(redis_handler=rd, email=self.DUMMY_EMAIL, jwt=self.DUMMY_JWT) assert session_token.validate_session_token(redis_handler=rd) session_token.redis_key = working_token assert not session_token.validate_session_token(redis_handler=rd) @@ -47,13 +48,14 @@ def test_redis_session_expired_token(self, redisdb): with mock.patch.object(RedisSessionToken, '_build_session_expiration', self.mock_build_session_expiration): session_token = RedisSessionToken( namespace=self.NAMESPACE, + email=self.DUMMY_EMAIL, jwt=self.DUMMY_JWT ) session_token.store_session_token(redis_handler=rd) time.sleep(2) assert not session_token.validate_session_token(redis_handler=rd) # update then should validate - session_token.update_session_token(redis_handler=rd, jwt=self.DUMMY_JWT) + session_token.update_session_token(redis_handler=rd, email=self.DUMMY_EMAIL, jwt=self.DUMMY_JWT) assert session_token.validate_session_token(redis_handler=rd) def test_redis_session_many_sessions(self, redisdb): @@ -65,6 +67,7 @@ def test_redis_session_many_sessions(self, redisdb): for _ in range(5): session_token = RedisSessionToken( namespace=self.NAMESPACE, + email=self.DUMMY_EMAIL, jwt=self.DUMMY_JWT ) session_token.store_session_token(redis_handler=rd) @@ -92,6 +95,7 @@ def test_redis_session_from_redis_equality(self, redisdb): rd = RedisBase(redisdb) session_token_local = RedisSessionToken( namespace=self.NAMESPACE, + email=self.DUMMY_EMAIL, jwt=self.DUMMY_JWT ) session_token_local.store_session_token(redis_handler=rd)