Skip to content

Commit

Permalink
Merge branch 'develop' into mp/sentry_backend_tunnel
Browse files Browse the repository at this point in the history
  • Loading branch information
puehringer authored Feb 19, 2025
2 parents 2c4e1f5 + cb42cad commit 3edfe7d
Show file tree
Hide file tree
Showing 10 changed files with 48 additions and 9 deletions.
1 change: 1 addition & 0 deletions src/app/header/VisynHeader.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { VisynHeader } from './VisynHeader';
const user: IUser = {
name: 'Jaimy Smith',
roles: [],
properties: {},
};

const customerLogo = (
Expand Down
2 changes: 1 addition & 1 deletion src/security/constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IUser } from './interfaces';

export class UserUtils {
static ANONYMOUS_USER: IUser = { name: 'anonymous', roles: ['anonymous'] };
static ANONYMOUS_USER: IUser = { name: 'anonymous', roles: ['anonymous'], properties: {} };
}

export enum EPermission {
Expand Down
4 changes: 4 additions & 0 deletions src/security/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export interface IUser {
* list of roles the user is associated with
*/
readonly roles: string[];
/**
* arbitrary properties mapped to the user, i.e. from a JWT token payload
*/
properties: Record<string, unknown>;
}

export interface IUserStore<T extends Record<string, any> = Record<string, any>> {
Expand Down
1 change: 1 addition & 0 deletions visyn_core/security/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def user_to_dict(user: User, access_token: str | None = None, payload: dict | No
"payload": payload,
"access_token": access_token,
"token_type": "bearer" if access_token else None,
"properties": user.properties,
}


Expand Down
4 changes: 4 additions & 0 deletions visyn_core/security/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ class User(BaseModel):
OAuth2 access token as many security stores (like ALB or OAuth2) only parse an already existing access token from an IdP.
This token can then be used for downstream tasks like requests to other services.
"""
properties: dict[str, Any] = {}
"""
Arbitary properties that are mapped to the user, i.e. from the JWT token to the user object.
"""

@property
def name(self):
Expand Down
4 changes: 4 additions & 0 deletions visyn_core/security/store/alb_security_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def __init__(
cookie_name: str | None,
signout_url: str | None,
email_token_field: str | list[str],
properties_fields: list[str],
audience: str | list[str] | None,
issuer: str | None,
decode_options: dict[str, Any] | None,
Expand All @@ -37,6 +38,7 @@ def __init__(
self.cookie_name = cookie_name
self.signout_url = signout_url
self.email_token_fields = [email_token_field] if isinstance(email_token_field, str) else email_token_field
self.properties_fields = properties_fields
self.audience = audience
self.issuer = issuer
self.decode_options = decode_options
Expand Down Expand Up @@ -80,6 +82,7 @@ def load_from_request(self, req: Request):
id=id,
roles=user.get("roles", []),
oauth2_access_token=access_token,
properties={key: user.get(key) for key in self.properties_fields},
)
except Exception:
_log.exception("Error in load_from_request")
Expand Down Expand Up @@ -111,6 +114,7 @@ def create():
cookie_name=manager.settings.visyn_core.security.store.alb_security_store.cookie_name,
signout_url=manager.settings.visyn_core.security.store.alb_security_store.signout_url,
email_token_field=manager.settings.visyn_core.security.store.alb_security_store.email_token_field,
properties_fields=manager.settings.visyn_core.security.store.alb_security_store.properties_fields,
audience=manager.settings.visyn_core.security.store.alb_security_store.audience,
decode_options=manager.settings.visyn_core.security.store.alb_security_store.decode_options,
region=manager.settings.visyn_core.security.store.alb_security_store.region,
Expand Down
11 changes: 7 additions & 4 deletions visyn_core/security/store/no_security_store.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Any

from ... import manager
from ..model import User
Expand All @@ -8,12 +9,13 @@


class NoSecurityStore(BaseStore):
def __init__(self, user: str, roles: list[str]):
def __init__(self, user: str, roles: list[str], properties: dict[str, Any]):
self.user = user
self.roles = roles
self.properties = properties

def load_from_request(self, req):
return User(id=self.user, roles=self.roles)
return User(id=self.user, roles=self.roles, properties=self.properties)


def create():
Expand All @@ -24,8 +26,9 @@ def create():
if manager.settings.visyn_core.security.store.no_security_store.enable:
_log.info("Adding NoSecurityStore")
return NoSecurityStore(
manager.settings.visyn_core.security.store.no_security_store.user,
manager.settings.visyn_core.security.store.no_security_store.roles,
user=manager.settings.visyn_core.security.store.no_security_store.user,
roles=manager.settings.visyn_core.security.store.no_security_store.roles,
properties=manager.settings.visyn_core.security.store.no_security_store.properties,
)

return None
11 changes: 7 additions & 4 deletions visyn_core/security/store/oauth2_security_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
class OAuth2SecurityStore(BaseStore):
ui = "AutoLoginForm"

def __init__(self, cookie_name: str | None, signout_url: str | None, email_token_field: str | list[str]):
def __init__(self, cookie_name: str | None, signout_url: str | None, email_token_field: str | list[str], properties_fields: list[str]):
self.cookie_name = cookie_name
self.signout_url: str | None = signout_url
self.email_token_fields = [email_token_field] if isinstance(email_token_field, str) else email_token_field
self.properties_fields = properties_fields

def load_from_request(self, req: Request):
token_field = manager.settings.visyn_core.security.store.oauth2_security_store.access_token_header_name
Expand All @@ -38,6 +39,7 @@ def load_from_request(self, req: Request):
id=id,
roles=[],
oauth2_access_token=access_token,
properties={key: user.get(key) for key in self.properties_fields},
)
except Exception:
_log.exception("Error in load_from_request")
Expand Down Expand Up @@ -65,9 +67,10 @@ def create():
if manager.settings.visyn_core.security.store.oauth2_security_store.enable:
_log.info("Adding OAuth2SecurityStore")
return OAuth2SecurityStore(
manager.settings.visyn_core.security.store.oauth2_security_store.cookie_name,
manager.settings.visyn_core.security.store.oauth2_security_store.signout_url,
manager.settings.visyn_core.security.store.oauth2_security_store.email_token_field,
cookie_name=manager.settings.visyn_core.security.store.oauth2_security_store.cookie_name,
signout_url=manager.settings.visyn_core.security.store.oauth2_security_store.signout_url,
email_token_field=manager.settings.visyn_core.security.store.oauth2_security_store.email_token_field,
properties_fields=manager.settings.visyn_core.security.store.oauth2_security_store.properties_fields,
)

return None
12 changes: 12 additions & 0 deletions visyn_core/settings/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ class AlbSecurityStoreSettings(BaseModel):
"""
Field in the JWT token that contains the email address of the user.
"""
properties_fields: list[str] = []
"""
Fields in the JWT token payload that should be mapped to the properties of the user.
"""
audience: str | list[str] | None = None
"""
Audience of the JWT token.
Expand Down Expand Up @@ -77,12 +81,20 @@ class OAuth2SecurityStoreSettings(BaseModel):
signout_url: str | None = None
access_token_header_name: str = "X-Forwarded-Access-Token"
email_token_field: str | list[str] = ["email"]
"""
Field in the JWT token that contains the email address of the user.
"""
properties_fields: list[str] = []
"""
Fields in the JWT token payload that should be mapped to the properties of the user.
"""


class NoSecurityStoreSettings(BaseModel):
enable: bool = False
user: str = "admin"
roles: list[str] = []
properties: dict[str, Any] = {}


class SecurityStoreSettings(BaseModel):
Expand Down
7 changes: 7 additions & 0 deletions visyn_core/tests/test_security_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def test_alb_security_store(client: TestClient):
# Add some basic configuration
manager.settings.visyn_core.security.store.alb_security_store.enable = True
manager.settings.visyn_core.security.store.alb_security_store.email_token_field = ["field1", "field2", "email"]
manager.settings.visyn_core.security.store.alb_security_store.properties_fields = ["sub", "exp"]
manager.settings.visyn_core.security.store.alb_security_store.decode_options = {"verify_signature": False}
manager.settings.visyn_core.security.store.alb_security_store.cookie_name = "TestCookie"
manager.settings.visyn_core.security.store.alb_security_store.signout_url = "http://localhost/api/logout"
Expand All @@ -143,6 +144,7 @@ def test_alb_security_store(client: TestClient):
assert response.status_code == 200
assert response.json() != '"not_yet_logged_in"'
assert response.json()["name"] == "admin@localhost"
assert response.json()["properties"] == {"sub": "admin", "exp": 1657188138.494586}

# Logout and check if we get the correct redirect url
response = client.post("/api/logout", headers=headers)
Expand All @@ -159,6 +161,8 @@ def test_oauth2_security_store(client: TestClient):
manager.settings.visyn_core.security.store.oauth2_security_store.enable = True
manager.settings.visyn_core.security.store.oauth2_security_store.cookie_name = "TestCookie"
manager.settings.visyn_core.security.store.oauth2_security_store.signout_url = "http://localhost/api/logout"
manager.settings.visyn_core.security.store.oauth2_security_store.email_token_field = ["field1", "field2", "email"]
manager.settings.visyn_core.security.store.oauth2_security_store.properties_fields = ["sub"]

store = create_oauth2_security_store()
assert store is not None
Expand All @@ -178,6 +182,7 @@ def test_oauth2_security_store(client: TestClient):
assert response.status_code == 200
assert response.json() != '"not_yet_logged_in"'
assert response.json()["name"] == "admin@localhost"
assert response.json()["properties"] == {"sub": "admin"}

# Logout and check if we get the correct redirect url
response = client.post("/api/logout", headers=headers)
Expand All @@ -190,6 +195,7 @@ def test_no_security_store(client: TestClient):
manager.settings.visyn_core.security.store.no_security_store.enable = True
manager.settings.visyn_core.security.store.no_security_store.user = "test_name"
manager.settings.visyn_core.security.store.no_security_store.roles = ["test_role"]
manager.settings.visyn_core.security.store.no_security_store.properties = {"id": 123, "name": "test"}

store = create_no_security_store()
assert store is not None
Expand All @@ -200,6 +206,7 @@ def test_no_security_store(client: TestClient):
assert user_info != '"not_yet_logged_in"'
assert user_info["name"] == "test_name"
assert user_info["roles"] == ["test_role"]
assert user_info["properties"] == {"id": 123, "name": "test"}


def test_user_login_hooks(client: TestClient):
Expand Down

0 comments on commit 3edfe7d

Please sign in to comment.