diff --git a/api/auth/auth_jwt.py b/api/auth/auth_jwt.py index 4eff80789..e8824b6b9 100644 --- a/api/auth/auth_jwt.py +++ b/api/auth/auth_jwt.py @@ -6,14 +6,14 @@ from starlette import status from starlette.exceptions import HTTPException from chalicelib.core import authorizers, users -from schemas import CurrentContext +import schemas class JWTAuth(HTTPBearer): def __init__(self, auto_error: bool = True): super(JWTAuth, self).__init__(auto_error=auto_error) - async def __call__(self, request: Request) -> Optional[CurrentContext]: + async def __call__(self, request: Request) -> Optional[schemas.CurrentContext]: credentials: HTTPAuthorizationCredentials = await super(JWTAuth, self).__call__(request) if credentials: if not credentials.scheme == "Bearer": @@ -49,9 +49,9 @@ class JWTAuth(HTTPBearer): jwt_payload["authorizer_identity"] = "jwt" print(jwt_payload) request.state.authorizer_identity = "jwt" - request.state.currentContext = CurrentContext(tenant_id=jwt_payload.get("tenantId", -1), - user_id=jwt_payload.get("userId", -1), - email=user["email"]) + request.state.currentContext = schemas.CurrentContext(tenant_id=jwt_payload.get("tenantId", -1), + user_id=jwt_payload.get("userId", -1), + email=user["email"]) return request.state.currentContext else: diff --git a/api/chalicelib/core/errors.py b/api/chalicelib/core/errors.py index 1c7ed94c9..b20853646 100644 --- a/api/chalicelib/core/errors.py +++ b/api/chalicelib/core/errors.py @@ -709,36 +709,6 @@ def __status_rank(status): }.get(status) -def merge(error_ids): - error_ids = list(set(error_ids)) - errors = get_batch(error_ids) - if len(error_ids) <= 1 or len(error_ids) > len(errors): - return {"errors": ["invalid list of ids"]} - error_ids = [e["errorId"] for e in errors] - parent_error_id = error_ids[0] - status = "unresolved" - for e in errors: - if __status_rank(status) < __status_rank(e["status"]): - status = e["status"] - if __status_rank(status) == MAX_RANK: - break - params = { - "error_ids": tuple(error_ids), - "parent_error_id": parent_error_id, - "status": status - } - with pg_client.PostgresClient() as cur: - query = cur.mogrify( - """UPDATE public.errors - SET parent_error_id = %(parent_error_id)s, status = %(status)s - WHERE error_id IN %(error_ids)s OR parent_error_id IN %(error_ids)s;""", - params) - cur.execute(query=query) - # row = cur.fetchone() - - return {"data": "success"} - - def format_first_stack_frame(error): error["stack"] = sourcemaps.format_payload(error.pop("payload"), truncate_to_first=True) for s in error["stack"]: diff --git a/api/routers/core.py b/api/routers/core.py index 5c4b311f5..3741fdec6 100644 --- a/api/routers/core.py +++ b/api/routers/core.py @@ -59,87 +59,6 @@ def sessions_search(projectId: int, data: schemas.FlatSessionsSearchPayloadSchem return {'data': data} -@app.get('/{projectId}/sessions/{sessionId}', tags=["sessions"]) -@app.get('/{projectId}/sessions2/{sessionId}', tags=["sessions"]) -def get_session2(projectId: int, sessionId: Union[int, str], background_tasks: BackgroundTasks, - context: schemas.CurrentContext = Depends(OR_context)): - if isinstance(sessionId, str): - return {"errors": ["session not found"]} - data = sessions.get_by_id2_pg(project_id=projectId, session_id=sessionId, full_data=True, user_id=context.user_id, - include_fav_viewed=True, group_metadata=True) - if data is None: - return {"errors": ["session not found"]} - if data.get("inDB"): - background_tasks.add_task(sessions_viewed.view_session, project_id=projectId, user_id=context.user_id, - session_id=sessionId) - return { - 'data': data - } - - -@app.get('/{projectId}/sessions/{sessionId}/favorite', tags=["sessions"]) -@app.get('/{projectId}/sessions2/{sessionId}/favorite', tags=["sessions"]) -def add_remove_favorite_session2(projectId: int, sessionId: int, - context: schemas.CurrentContext = Depends(OR_context)): - return { - "data": sessions_favorite.favorite_session(project_id=projectId, user_id=context.user_id, - session_id=sessionId)} - - -@app.get('/{projectId}/sessions/{sessionId}/assign', tags=["sessions"]) -@app.get('/{projectId}/sessions2/{sessionId}/assign', tags=["sessions"]) -def assign_session(projectId: int, sessionId, context: schemas.CurrentContext = Depends(OR_context)): - data = sessions_assignments.get_by_session(project_id=projectId, session_id=sessionId, - tenant_id=context.tenant_id, - user_id=context.user_id) - if "errors" in data: - return data - return { - 'data': data - } - - -@app.get('/{projectId}/sessions/{sessionId}/errors/{errorId}/sourcemaps', tags=["sessions", "sourcemaps"]) -@app.get('/{projectId}/sessions2/{sessionId}/errors/{errorId}/sourcemaps', tags=["sessions", "sourcemaps"]) -def get_error_trace(projectId: int, sessionId: int, errorId: str, - context: schemas.CurrentContext = Depends(OR_context)): - data = errors.get_trace(project_id=projectId, error_id=errorId) - if "errors" in data: - return data - return { - 'data': data - } - - -@app.get('/{projectId}/sessions/{sessionId}/assign/{issueId}', tags=["sessions", "issueTracking"]) -@app.get('/{projectId}/sessions2/{sessionId}/assign/{issueId}', tags=["sessions", "issueTracking"]) -def assign_session(projectId: int, sessionId: int, issueId: str, - context: schemas.CurrentContext = Depends(OR_context)): - data = sessions_assignments.get(project_id=projectId, session_id=sessionId, assignment_id=issueId, - tenant_id=context.tenant_id, user_id=context.user_id) - if "errors" in data: - return data - return { - 'data': data - } - - -@app.post('/{projectId}/sessions/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"]) -@app.put('/{projectId}/sessions/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"]) -@app.post('/{projectId}/sessions2/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"]) -@app.put('/{projectId}/sessions2/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"]) -def comment_assignment(projectId: int, sessionId: int, issueId: str, data: schemas.CommentAssignmentSchema = Body(...), - context: schemas.CurrentContext = Depends(OR_context)): - data = sessions_assignments.comment(tenant_id=context.tenant_id, project_id=projectId, - session_id=sessionId, assignment_id=issueId, - user_id=context.user_id, message=data.message) - if "errors" in data.keys(): - return data - return { - 'data': data - } - - @app.get('/{projectId}/events/search', tags=["events"]) def events_search(projectId: int, q: str, type: Union[schemas.FilterType, schemas.EventType, @@ -664,13 +583,6 @@ def get_all_announcements(context: schemas.CurrentContext = Depends(OR_context)) return {"data": announcements.view(user_id=context.user_id)} -@app.post('/{projectId}/errors/merge', tags=["errors"]) -def errors_merge(projectId: int, data: schemas.ErrorIdsPayloadSchema = Body(...), - context: schemas.CurrentContext = Depends(OR_context)): - data = errors.merge(error_ids=data.errors) - return data - - @app.get('/show_banner', tags=["banner"]) def errors_merge(context: schemas.CurrentContext = Depends(OR_context)): return {"data": False} @@ -894,45 +806,6 @@ def sessions_live(projectId: int, data: schemas.LiveSessionsSearchPayloadSchema return {'data': data} -@app.get('/{projectId}/assist/sessions/{sessionId}', tags=["assist"]) -def get_live_session(projectId: int, sessionId: str, background_tasks: BackgroundTasks, - context: schemas.CurrentContext = Depends(OR_context)): - data = assist.get_live_session_by_id(project_id=projectId, session_id=sessionId) - if data is None: - data = sessions.get_by_id2_pg(project_id=projectId, session_id=sessionId, full_data=True, - user_id=context.user_id, include_fav_viewed=True, group_metadata=True, live=False) - if data is None: - return {"errors": ["session not found"]} - if data.get("inDB"): - background_tasks.add_task(sessions_viewed.view_session, project_id=projectId, - user_id=context.user_id, session_id=sessionId) - return {'data': data} - - -@app.get('/{projectId}/unprocessed/{sessionId}', tags=["assist"]) -@app.get('/{projectId}/assist/sessions/{sessionId}/replay', tags=["assist"]) -def get_live_session_replay_file(projectId: int, sessionId: Union[int, str], - context: schemas.CurrentContext = Depends(OR_context)): - if isinstance(sessionId, str) or not sessions.session_exists(project_id=projectId, session_id=sessionId): - if isinstance(sessionId, str): - print(f"{sessionId} not a valid number.") - else: - print(f"{projectId}/{sessionId} not found in DB.") - - return {"errors": ["Replay file not found"]} - path = assist.get_raw_mob_by_id(project_id=projectId, session_id=sessionId) - if path is None: - return {"errors": ["Replay file not found"]} - - return FileResponse(path=path, media_type="application/octet-stream") - - -@app.post('/{projectId}/heatmaps/url', tags=["heatmaps"]) -def get_heatmaps_by_url(projectId: int, data: schemas.GetHeatmapPayloadSchema = Body(...), - context: schemas.CurrentContext = Depends(OR_context)): - return {"data": heatmaps.get_by_url(project_id=projectId, data=data.dict())} - - @app.post('/{projectId}/mobile/{sessionId}/urls', tags=['mobile']) def mobile_signe(projectId: int, sessionId: int, data: schemas.MobileSignPayloadSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): @@ -978,67 +851,6 @@ def edit_client(data: schemas.UpdateTenantSchema = Body(...), return tenants.update(tenant_id=context.tenant_id, user_id=context.user_id, data=data) -@app.post('/{projectId}/errors/search', tags=['errors']) -def errors_search(projectId: int, data: schemas.SearchErrorsSchema = Body(...), - context: schemas.CurrentContext = Depends(OR_context)): - return {"data": errors.search(data, projectId, user_id=context.user_id)} - - -@app.get('/{projectId}/errors/stats', tags=['errors']) -def errors_stats(projectId: int, startTimestamp: int, endTimestamp: int, - context: schemas.CurrentContext = Depends(OR_context)): - return errors.stats(projectId, user_id=context.user_id, startTimestamp=startTimestamp, endTimestamp=endTimestamp) - - -@app.get('/{projectId}/errors/{errorId}', tags=['errors']) -def errors_get_details(projectId: int, errorId: str, background_tasks: BackgroundTasks, density24: int = 24, - density30: int = 30, - context: schemas.CurrentContext = Depends(OR_context)): - data = errors.get_details(project_id=projectId, user_id=context.user_id, error_id=errorId, - **{"density24": density24, "density30": density30}) - if data.get("data") is not None: - background_tasks.add_task(errors_viewed.viewed_error, project_id=projectId, user_id=context.user_id, - error_id=errorId) - return data - - -@app.get('/{projectId}/errors/{errorId}/stats', tags=['errors']) -def errors_get_details_right_column(projectId: int, errorId: str, startDate: int = TimeUTC.now(-7), - endDate: int = TimeUTC.now(), density: int = 7, - context: schemas.CurrentContext = Depends(OR_context)): - data = errors.get_details_chart(project_id=projectId, user_id=context.user_id, error_id=errorId, - **{"startDate": startDate, "endDate": endDate, "density": density}) - return data - - -@app.get('/{projectId}/errors/{errorId}/sourcemaps', tags=['errors']) -def errors_get_details_sourcemaps(projectId: int, errorId: str, - context: schemas.CurrentContext = Depends(OR_context)): - data = errors.get_trace(project_id=projectId, error_id=errorId) - if "errors" in data: - return data - return { - 'data': data - } - - -@app.get('/{projectId}/errors/{errorId}/{action}', tags=["errors"]) -def add_remove_favorite_error(projectId: int, errorId: str, action: str, startDate: int = TimeUTC.now(-7), - endDate: int = TimeUTC.now(), context: schemas.CurrentContext = Depends(OR_context)): - if action == "favorite": - return errors_favorite.favorite_error(project_id=projectId, user_id=context.user_id, error_id=errorId) - elif action == "sessions": - start_date = startDate - end_date = endDate - return { - "data": errors.get_sessions(project_id=projectId, user_id=context.user_id, error_id=errorId, - start_date=start_date, end_date=end_date)} - elif action in list(errors.ACTION_STATE.keys()): - return errors.change_state(project_id=projectId, user_id=context.user_id, error_id=errorId, action=action) - else: - return {"errors": ["undefined action"]} - - @app.get('/notifications', tags=['notifications']) def get_notifications(context: schemas.CurrentContext = Depends(OR_context)): return {"data": notifications.get_all(tenant_id=context.tenant_id, user_id=context.user_id)} diff --git a/api/routers/core_dynamic.py b/api/routers/core_dynamic.py index d37a56728..0b3952dd2 100644 --- a/api/routers/core_dynamic.py +++ b/api/routers/core_dynamic.py @@ -11,6 +11,7 @@ from chalicelib.core import tenants, users, projects, license from chalicelib.core import webhook from chalicelib.core.collaboration_slack import Slack from chalicelib.utils import helper +from chalicelib.utils.TimeUTC import TimeUTC from or_dependencies import OR_context from routers.base import get_routers @@ -165,3 +166,184 @@ def get_general_stats(): def get_projects(context: schemas.CurrentContext = Depends(OR_context)): return {"data": projects.get_projects(tenant_id=context.tenant_id, recording_state=True, gdpr=True, recorded=True, stack_integrations=True)} + + +@app.get('/{projectId}/sessions/{sessionId}', tags=["sessions"]) +@app.get('/{projectId}/sessions2/{sessionId}', tags=["sessions"]) +def get_session(projectId: int, sessionId: Union[int, str], background_tasks: BackgroundTasks, + context: schemas.CurrentContext = Depends(OR_context)): + if isinstance(sessionId, str): + return {"errors": ["session not found"]} + data = sessions.get_by_id2_pg(project_id=projectId, session_id=sessionId, full_data=True, user_id=context.user_id, + include_fav_viewed=True, group_metadata=True) + if data is None: + return {"errors": ["session not found"]} + if data.get("inDB"): + background_tasks.add_task(sessions_viewed.view_session, project_id=projectId, user_id=context.user_id, + session_id=sessionId) + return { + 'data': data + } + + +@app.get('/{projectId}/sessions/{sessionId}/errors/{errorId}/sourcemaps', tags=["sessions", "sourcemaps"]) +@app.get('/{projectId}/sessions2/{sessionId}/errors/{errorId}/sourcemaps', tags=["sessions", "sourcemaps"]) +def get_error_trace(projectId: int, sessionId: int, errorId: str, + context: schemas.CurrentContext = Depends(OR_context)): + data = errors.get_trace(project_id=projectId, error_id=errorId) + if "errors" in data: + return data + return { + 'data': data + } + + +@app.post('/{projectId}/errors/search', tags=['errors']) +def errors_search(projectId: int, data: schemas.SearchErrorsSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + return {"data": errors.search(data, projectId, user_id=context.user_id)} + + +@app.get('/{projectId}/errors/stats', tags=['errors']) +def errors_stats(projectId: int, startTimestamp: int, endTimestamp: int, + context: schemas.CurrentContext = Depends(OR_context)): + return errors.stats(projectId, user_id=context.user_id, startTimestamp=startTimestamp, endTimestamp=endTimestamp) + + +@app.get('/{projectId}/errors/{errorId}', tags=['errors']) +def errors_get_details(projectId: int, errorId: str, background_tasks: BackgroundTasks, density24: int = 24, + density30: int = 30, + context: schemas.CurrentContext = Depends(OR_context)): + data = errors.get_details(project_id=projectId, user_id=context.user_id, error_id=errorId, + **{"density24": density24, "density30": density30}) + if data.get("data") is not None: + background_tasks.add_task(errors_viewed.viewed_error, project_id=projectId, user_id=context.user_id, + error_id=errorId) + return data + + +@app.get('/{projectId}/errors/{errorId}/stats', tags=['errors']) +def errors_get_details_right_column(projectId: int, errorId: str, startDate: int = TimeUTC.now(-7), + endDate: int = TimeUTC.now(), density: int = 7, + context: schemas.CurrentContext = Depends(OR_context)): + data = errors.get_details_chart(project_id=projectId, user_id=context.user_id, error_id=errorId, + **{"startDate": startDate, "endDate": endDate, "density": density}) + return data + + +@app.get('/{projectId}/errors/{errorId}/sourcemaps', tags=['errors']) +def errors_get_details_sourcemaps(projectId: int, errorId: str, + context: schemas.CurrentContext = Depends(OR_context)): + data = errors.get_trace(project_id=projectId, error_id=errorId) + if "errors" in data: + return data + return { + 'data': data + } + + +@app.get('/{projectId}/errors/{errorId}/{action}', tags=["errors"]) +def add_remove_favorite_error(projectId: int, errorId: str, action: str, startDate: int = TimeUTC.now(-7), + endDate: int = TimeUTC.now(), context: schemas.CurrentContext = Depends(OR_context)): + if action == "favorite": + return errors_favorite.favorite_error(project_id=projectId, user_id=context.user_id, error_id=errorId) + elif action == "sessions": + start_date = startDate + end_date = endDate + return { + "data": errors.get_sessions(project_id=projectId, user_id=context.user_id, error_id=errorId, + start_date=start_date, end_date=end_date)} + elif action in list(errors.ACTION_STATE.keys()): + return errors.change_state(project_id=projectId, user_id=context.user_id, error_id=errorId, action=action) + else: + return {"errors": ["undefined action"]} + + +@app.get('/{projectId}/assist/sessions/{sessionId}', tags=["assist"]) +def get_live_session(projectId: int, sessionId: str, background_tasks: BackgroundTasks, + context: schemas.CurrentContext = Depends(OR_context)): + data = assist.get_live_session_by_id(project_id=projectId, session_id=sessionId) + if data is None: + data = sessions.get_by_id2_pg(project_id=projectId, session_id=sessionId, full_data=True, + user_id=context.user_id, include_fav_viewed=True, group_metadata=True, live=False) + if data is None: + return {"errors": ["session not found"]} + if data.get("inDB"): + background_tasks.add_task(sessions_viewed.view_session, project_id=projectId, + user_id=context.user_id, session_id=sessionId) + return {'data': data} + + +@app.get('/{projectId}/unprocessed/{sessionId}', tags=["assist"]) +@app.get('/{projectId}/assist/sessions/{sessionId}/replay', tags=["assist"]) +def get_live_session_replay_file(projectId: int, sessionId: Union[int, str], + context: schemas.CurrentContext = Depends(OR_context)): + if isinstance(sessionId, str) or not sessions.session_exists(project_id=projectId, session_id=sessionId): + if isinstance(sessionId, str): + print(f"{sessionId} not a valid number.") + else: + print(f"{projectId}/{sessionId} not found in DB.") + + return {"errors": ["Replay file not found"]} + path = assist.get_raw_mob_by_id(project_id=projectId, session_id=sessionId) + if path is None: + return {"errors": ["Replay file not found"]} + + return FileResponse(path=path, media_type="application/octet-stream") + + +@app.post('/{projectId}/heatmaps/url', tags=["heatmaps"]) +def get_heatmaps_by_url(projectId: int, data: schemas.GetHeatmapPayloadSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + return {"data": heatmaps.get_by_url(project_id=projectId, data=data.dict())} + + +@app.get('/{projectId}/sessions/{sessionId}/favorite', tags=["sessions"]) +@app.get('/{projectId}/sessions2/{sessionId}/favorite', tags=["sessions"]) +def add_remove_favorite_session2(projectId: int, sessionId: int, + context: schemas.CurrentContext = Depends(OR_context)): + return { + "data": sessions_favorite.favorite_session(project_id=projectId, user_id=context.user_id, + session_id=sessionId)} + + +@app.get('/{projectId}/sessions/{sessionId}/assign', tags=["sessions"]) +@app.get('/{projectId}/sessions2/{sessionId}/assign', tags=["sessions"]) +def assign_session(projectId: int, sessionId, context: schemas.CurrentContext = Depends(OR_context)): + data = sessions_assignments.get_by_session(project_id=projectId, session_id=sessionId, + tenant_id=context.tenant_id, + user_id=context.user_id) + if "errors" in data: + return data + return { + 'data': data + } + + +@app.get('/{projectId}/sessions/{sessionId}/assign/{issueId}', tags=["sessions", "issueTracking"]) +@app.get('/{projectId}/sessions2/{sessionId}/assign/{issueId}', tags=["sessions", "issueTracking"]) +def assign_session(projectId: int, sessionId: int, issueId: str, + context: schemas.CurrentContext = Depends(OR_context)): + data = sessions_assignments.get(project_id=projectId, session_id=sessionId, assignment_id=issueId, + tenant_id=context.tenant_id, user_id=context.user_id) + if "errors" in data: + return data + return { + 'data': data + } + + +@app.post('/{projectId}/sessions/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"]) +@app.put('/{projectId}/sessions/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"]) +@app.post('/{projectId}/sessions2/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"]) +@app.put('/{projectId}/sessions2/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"]) +def comment_assignment(projectId: int, sessionId: int, issueId: str, data: schemas.CommentAssignmentSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + data = sessions_assignments.comment(tenant_id=context.tenant_id, project_id=projectId, + session_id=sessionId, assignment_id=issueId, + user_id=context.user_id, message=data.message) + if "errors" in data.keys(): + return data + return { + 'data': data + } diff --git a/ee/api/.gitignore b/ee/api/.gitignore index d25a4474d..e0bc9b436 100644 --- a/ee/api/.gitignore +++ b/ee/api/.gitignore @@ -243,7 +243,6 @@ Pipfile /routers/__init__.py /chalicelib/core/assist.py /auth/auth_apikey.py -/auth/auth_jwt.py /build.sh /routers/base.py /routers/core.py diff --git a/ee/api/auth/auth_jwt.py b/ee/api/auth/auth_jwt.py new file mode 100644 index 000000000..477beba3d --- /dev/null +++ b/ee/api/auth/auth_jwt.py @@ -0,0 +1,60 @@ +from typing import Optional + +from fastapi import Request +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from starlette import status +from starlette.exceptions import HTTPException + +from chalicelib.core import authorizers, users +import schemas_ee + + +class JWTAuth(HTTPBearer): + def __init__(self, auto_error: bool = True): + super(JWTAuth, self).__init__(auto_error=auto_error) + + async def __call__(self, request: Request) -> Optional[schemas_ee.CurrentContext]: + credentials: HTTPAuthorizationCredentials = await super(JWTAuth, self).__call__(request) + if credentials: + if not credentials.scheme == "Bearer": + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid authentication scheme.") + jwt_payload = authorizers.jwt_authorizer(credentials.scheme + " " + credentials.credentials) + auth_exists = jwt_payload is not None \ + and users.auth_exists(user_id=jwt_payload.get("userId", -1), + tenant_id=jwt_payload.get("tenantId", -1), + jwt_iat=jwt_payload.get("iat", 100), + jwt_aud=jwt_payload.get("aud", "")) + if jwt_payload is None \ + or jwt_payload.get("iat") is None or jwt_payload.get("aud") is None \ + or not auth_exists: + print("JWTAuth: Token issue") + if jwt_payload is not None: + print(jwt_payload) + print(f"JWTAuth: user_id={jwt_payload.get('userId')} tenant_id={jwt_payload.get('tenantId')}") + if jwt_payload is None: + print("JWTAuth: jwt_payload is None") + print(credentials.scheme + " " + credentials.credentials) + if jwt_payload is not None and jwt_payload.get("iat") is None: + print("JWTAuth: iat is None") + if jwt_payload is not None and jwt_payload.get("aud") is None: + print("JWTAuth: aud is None") + if jwt_payload is not None and not auth_exists: + print("JWTAuth: not users.auth_exists") + + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid token or expired token.") + user = users.get(user_id=jwt_payload.get("userId", -1), tenant_id=jwt_payload.get("tenantId", -1)) + if user is None: + print("JWTAuth: User not found.") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User not found.") + jwt_payload["authorizer_identity"] = "jwt" + print(jwt_payload) + request.state.authorizer_identity = "jwt" + request.state.currentContext = schemas_ee.CurrentContext(tenant_id=jwt_payload.get("tenantId", -1), + user_id=jwt_payload.get("userId", -1), + email=user["email"], + permissions=user["permissions"]) + return request.state.currentContext + + else: + print("JWTAuth: Invalid authorization code.") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid authorization code.") diff --git a/ee/api/auth/router_security.py b/ee/api/auth/router_security.py new file mode 100644 index 000000000..1b0c98980 --- /dev/null +++ b/ee/api/auth/router_security.py @@ -0,0 +1,15 @@ +from fastapi import HTTPException, Depends +from fastapi.security import SecurityScopes + +import schemas_ee +from or_dependencies import OR_context + + +def check(security_scopes: SecurityScopes, context: schemas_ee.CurrentContext = Depends(OR_context)): + for scope in security_scopes.scopes: + if scope not in context.permissions: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + ) + \ No newline at end of file diff --git a/ee/api/chalicelib/core/errors.py b/ee/api/chalicelib/core/errors.py index d50c6b54a..1db619b15 100644 --- a/ee/api/chalicelib/core/errors.py +++ b/ee/api/chalicelib/core/errors.py @@ -716,36 +716,6 @@ def __status_rank(status): }.get(status) -def merge(error_ids): - error_ids = list(set(error_ids)) - errors = get_batch(error_ids) - if len(error_ids) <= 1 or len(error_ids) > len(errors): - return {"errors": ["invalid list of ids"]} - error_ids = [e["errorId"] for e in errors] - parent_error_id = error_ids[0] - status = "unresolved" - for e in errors: - if __status_rank(status) < __status_rank(e["status"]): - status = e["status"] - if __status_rank(status) == MAX_RANK: - break - params = { - "error_ids": tuple(error_ids), - "parent_error_id": parent_error_id, - "status": status - } - with pg_client.PostgresClient() as cur: - query = cur.mogrify( - """UPDATE public.errors - SET parent_error_id = %(parent_error_id)s, status = %(status)s - WHERE error_id IN %(error_ids)s OR parent_error_id IN %(error_ids)s;""", - params) - cur.execute(query=query) - # row = cur.fetchone() - - return {"data": "success"} - - def format_first_stack_frame(error): error["stack"] = sourcemaps.format_payload(error.pop("payload"), truncate_to_first=True) for s in error["stack"]: diff --git a/ee/api/clean.sh b/ee/api/clean.sh index 9aa916080..395bd21af 100755 --- a/ee/api/clean.sh +++ b/ee/api/clean.sh @@ -65,7 +65,6 @@ rm -rf ./routers/subs/__init__.py rm -rf ./routers/__init__.py rm -rf ./chalicelib/core/assist.py rm -rf ./auth/auth_apikey.py -rm -rf ./auth/auth_jwt.py rm -rf ./build.sh rm -rf ./routers/core.py rm -rf ./routers/crons/core_crons.py diff --git a/ee/api/or_dependencies.py b/ee/api/or_dependencies.py index ec0eb5d51..4ca35476d 100644 --- a/ee/api/or_dependencies.py +++ b/ee/api/or_dependencies.py @@ -8,10 +8,11 @@ from starlette.requests import Request from starlette.responses import Response, JSONResponse import schemas +import schemas_ee from chalicelib.core import traces -async def OR_context(request: Request) -> schemas.CurrentContext: +async def OR_context(request: Request) -> schemas_ee.CurrentContext: if hasattr(request.state, "currentContext"): return request.state.currentContext else: diff --git a/ee/api/routers/core_dynamic.py b/ee/api/routers/core_dynamic.py index a414aed05..75806aeca 100644 --- a/ee/api/routers/core_dynamic.py +++ b/ee/api/routers/core_dynamic.py @@ -1,17 +1,23 @@ -from typing import Optional +from typing import Optional, Union from decouple import config -from fastapi import Body, Depends, BackgroundTasks +from fastapi import Body, Depends, BackgroundTasks, Security, HTTPException +from fastapi.security import SecurityScopes +from starlette import status from starlette.responses import RedirectResponse import schemas import schemas_ee +from schemas_ee import Permissions +from auth import router_security from chalicelib.core import sessions from chalicelib.core import tenants, users, projects, license from chalicelib.core import webhook +from chalicelib.core import sessions_viewed from chalicelib.core.collaboration_slack import Slack from chalicelib.utils import SAML2_helper from chalicelib.utils import helper +from chalicelib.utils.TimeUTC import TimeUTC from or_dependencies import OR_context from routers.base import get_routers @@ -171,3 +177,211 @@ def get_general_stats(): def get_projects(context: schemas.CurrentContext = Depends(OR_context)): return {"data": projects.get_projects(tenant_id=context.tenant_id, recording_state=True, gdpr=True, recorded=True, stack_integrations=True, user_id=context.user_id)} + + +@app.get('/{projectId}/sessions/{sessionId}', tags=["sessions"], + dependencies=[Security(router_security.check, scopes=[Permissions.session_replay])]) +@app.get('/{projectId}/sessions2/{sessionId}', tags=["sessions"], + dependencies=[Security(router_security.check, scopes=[Permissions.session_replay])]) +def get_session(projectId: int, sessionId: Union[int, str], background_tasks: BackgroundTasks, + context: schemas.CurrentContext = Depends(OR_context)): + if isinstance(sessionId, str): + return {"errors": ["session not found"]} + data = sessions.get_by_id2_pg(project_id=projectId, session_id=sessionId, full_data=True, user_id=context.user_id, + include_fav_viewed=True, group_metadata=True) + if data is None: + return {"errors": ["session not found"]} + if data.get("inDB"): + background_tasks.add_task(sessions_viewed.view_session, project_id=projectId, user_id=context.user_id, + session_id=sessionId) + return { + 'data': data + } + + +@app.get('/{projectId}/sessions/{sessionId}/errors/{errorId}/sourcemaps', tags=["sessions", "sourcemaps"], + dependencies=[Security(router_security.check, + scopes=[Permissions.session_replay, Permissions.errors])]) +@app.get('/{projectId}/sessions2/{sessionId}/errors/{errorId}/sourcemaps', tags=["sessions", "sourcemaps"], + dependencies=[Security(router_security.check, + scopes=[Permissions.session_replay, Permissions.errors])]) +def get_error_trace(projectId: int, sessionId: int, errorId: str, + context: schemas.CurrentContext = Depends(OR_context)): + data = errors.get_trace(project_id=projectId, error_id=errorId) + if "errors" in data: + return data + return { + 'data': data + } + + +@app.post('/{projectId}/errors/search', tags=['errors'], + dependencies=[Security(router_security.check, scopes=[Permissions.errors])]) +def errors_search(projectId: int, data: schemas.SearchErrorsSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + return {"data": errors.search(data, projectId, user_id=context.user_id)} + + +@app.get('/{projectId}/errors/stats', tags=['errors'], + dependencies=[Security(router_security.check, scopes=[Permissions.errors])]) +def errors_stats(projectId: int, startTimestamp: int, endTimestamp: int, + context: schemas.CurrentContext = Depends(OR_context)): + return errors.stats(projectId, user_id=context.user_id, startTimestamp=startTimestamp, endTimestamp=endTimestamp) + + +@app.get('/{projectId}/errors/{errorId}', tags=['errors'], + dependencies=[Security(router_security.check, scopes=[Permissions.errors])]) +def errors_get_details(projectId: int, errorId: str, background_tasks: BackgroundTasks, density24: int = 24, + density30: int = 30, + context: schemas.CurrentContext = Depends(OR_context)): + data = errors.get_details(project_id=projectId, user_id=context.user_id, error_id=errorId, + **{"density24": density24, "density30": density30}) + if data.get("data") is not None: + background_tasks.add_task(errors_viewed.viewed_error, project_id=projectId, user_id=context.user_id, + error_id=errorId) + return data + + +@app.get('/{projectId}/errors/{errorId}/stats', tags=['errors'], + dependencies=[Security(router_security.check, scopes=[Permissions.errors])]) +def errors_get_details_right_column(projectId: int, errorId: str, startDate: int = TimeUTC.now(-7), + endDate: int = TimeUTC.now(), density: int = 7, + context: schemas.CurrentContext = Depends(OR_context)): + data = errors.get_details_chart(project_id=projectId, user_id=context.user_id, error_id=errorId, + **{"startDate": startDate, "endDate": endDate, "density": density}) + return data + + +@app.get('/{projectId}/errors/{errorId}/sourcemaps', tags=['errors'], + dependencies=[Security(router_security.check, scopes=[Permissions.errors])]) +def errors_get_details_sourcemaps(projectId: int, errorId: str, + context: schemas.CurrentContext = Depends(OR_context)): + data = errors.get_trace(project_id=projectId, error_id=errorId) + if "errors" in data: + return data + return { + 'data': data + } + + +@app.get('/{projectId}/errors/{errorId}/{action}', tags=["errors"], + dependencies=[Security(router_security.check, scopes=[Permissions.errors])]) +def add_remove_favorite_error(projectId: int, errorId: str, action: str, startDate: int = TimeUTC.now(-7), + endDate: int = TimeUTC.now(), context: schemas.CurrentContext = Depends(OR_context)): + if action == "favorite": + return errors_favorite.favorite_error(project_id=projectId, user_id=context.user_id, error_id=errorId) + elif action == "sessions": + start_date = startDate + end_date = endDate + return { + "data": errors.get_sessions(project_id=projectId, user_id=context.user_id, error_id=errorId, + start_date=start_date, end_date=end_date)} + elif action in list(errors.ACTION_STATE.keys()): + return errors.change_state(project_id=projectId, user_id=context.user_id, error_id=errorId, action=action) + else: + return {"errors": ["undefined action"]} + + +@app.get('/{projectId}/assist/sessions/{sessionId}', tags=["assist"], + dependencies=[Security(router_security.check, scopes=[Permissions.assist_live])]) +def get_live_session(projectId: int, sessionId: str, background_tasks: BackgroundTasks, + context: schemas.CurrentContext = Depends(OR_context)): + data = assist.get_live_session_by_id(project_id=projectId, session_id=sessionId) + if data is None: + data = sessions.get_by_id2_pg(project_id=projectId, session_id=sessionId, full_data=True, + user_id=context.user_id, include_fav_viewed=True, group_metadata=True, live=False) + if data is None: + return {"errors": ["session not found"]} + if data.get("inDB"): + background_tasks.add_task(sessions_viewed.view_session, project_id=projectId, + user_id=context.user_id, session_id=sessionId) + return {'data': data} + + +@app.get('/{projectId}/unprocessed/{sessionId}', tags=["assist"], dependencies=[Security(router_security.check, scopes=[ + Permissions.assist_live, Permissions.session_replay])]) +@app.get('/{projectId}/assist/sessions/{sessionId}/replay', tags=["assist"], dependencies=[ + Security(router_security.check, + scopes=[Permissions.assist_live, Permissions.session_replay])]) +def get_live_session_replay_file(projectId: int, sessionId: Union[int, str], + context: schemas.CurrentContext = Depends(OR_context)): + if isinstance(sessionId, str) or not sessions.session_exists(project_id=projectId, session_id=sessionId): + if isinstance(sessionId, str): + print(f"{sessionId} not a valid number.") + else: + print(f"{projectId}/{sessionId} not found in DB.") + + return {"errors": ["Replay file not found"]} + path = assist.get_raw_mob_by_id(project_id=projectId, session_id=sessionId) + if path is None: + return {"errors": ["Replay file not found"]} + + return FileResponse(path=path, media_type="application/octet-stream") + + +@app.post('/{projectId}/heatmaps/url', tags=["heatmaps"], + dependencies=[Security(router_security.check, scopes=[Permissions.session_replay])]) +def get_heatmaps_by_url(projectId: int, data: schemas.GetHeatmapPayloadSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + return {"data": heatmaps.get_by_url(project_id=projectId, data=data.dict())} + + +@app.get('/{projectId}/sessions/{sessionId}/favorite', tags=["sessions"], + dependencies=[Security(router_security.check, scopes=[Permissions.session_replay])]) +@app.get('/{projectId}/sessions2/{sessionId}/favorite', tags=["sessions"], + dependencies=[Security(router_security.check, scopes=[Permissions.session_replay])]) +def add_remove_favorite_session2(projectId: int, sessionId: int, + context: schemas.CurrentContext = Depends(OR_context)): + return { + "data": sessions_favorite.favorite_session(project_id=projectId, user_id=context.user_id, + session_id=sessionId)} + + +@app.get('/{projectId}/sessions/{sessionId}/assign', tags=["sessions"], + dependencies=[Security(router_security.check, scopes=[Permissions.session_replay])]) +@app.get('/{projectId}/sessions2/{sessionId}/assign', tags=["sessions"], + dependencies=[Security(router_security.check, scopes=[Permissions.session_replay])]) +def assign_session(projectId: int, sessionId, context: schemas.CurrentContext = Depends(OR_context)): + data = sessions_assignments.get_by_session(project_id=projectId, session_id=sessionId, + tenant_id=context.tenant_id, + user_id=context.user_id) + if "errors" in data: + return data + return { + 'data': data + } + + +@app.get('/{projectId}/sessions/{sessionId}/assign/{issueId}', tags=["sessions", "issueTracking"], + dependencies=[Security(router_security.check, scopes=[Permissions.session_replay])]) +@app.get('/{projectId}/sessions2/{sessionId}/assign/{issueId}', tags=["sessions", "issueTracking"], + dependencies=[Security(router_security.check, scopes=[Permissions.session_replay])]) +def assign_session(projectId: int, sessionId: int, issueId: str, + context: schemas.CurrentContext = Depends(OR_context)): + data = sessions_assignments.get(project_id=projectId, session_id=sessionId, assignment_id=issueId, + tenant_id=context.tenant_id, user_id=context.user_id) + if "errors" in data: + return data + return { + 'data': data + } + + +@app.post('/{projectId}/sessions/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"], + dependencies=[Security(router_security.check, scopes=[Permissions.session_replay])]) +@app.put('/{projectId}/sessions/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"], + dependencies=[Security(router_security.check, scopes=[Permissions.session_replay])]) +@app.post('/{projectId}/sessions2/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"], + dependencies=[Security(router_security.check, scopes=[Permissions.session_replay])]) +@app.put('/{projectId}/sessions2/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"], + dependencies=[Security(router_security.check, scopes=[Permissions.session_replay])]) +def comment_assignment(projectId: int, sessionId: int, issueId: str, data: schemas.CommentAssignmentSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + data = sessions_assignments.comment(tenant_id=context.tenant_id, project_id=projectId, + session_id=sessionId, assignment_id=issueId, + user_id=context.user_id, message=data.message) + if "errors" in data.keys(): + return data + return { + 'data': data + } diff --git a/ee/api/schemas_ee.py b/ee/api/schemas_ee.py index 458bdc052..9a91cb944 100644 --- a/ee/api/schemas_ee.py +++ b/ee/api/schemas_ee.py @@ -4,12 +4,26 @@ from pydantic import BaseModel, Field, EmailStr import schemas from chalicelib.utils.TimeUTC import TimeUTC +from enum import Enum + + +class Permissions(str, Enum): + session_replay = "SESSION_REPLAY" + dev_tools = "DEV_TOOLS" + errors = "ERRORS" + metrics = "METRICS" + assist_live = "ASSIST_LIVE" + assist_call = "ASSIST_CALL" + + +class CurrentContext(schemas.CurrentContext): + permissions: List[Optional[Permissions]] = Field(...) class RolePayloadSchema(BaseModel): name: str = Field(...) description: Optional[str] = Field(None) - permissions: List[str] = Field(...) + permissions: List[Permissions] = Field(...) all_projects: bool = Field(True) projects: List[int] = Field([])