Skip to content

Commit ee3bdfd

Browse files
authored
Restrict non-staff users accessing compliance data (#621)
* updated_date timestamp issue * Restrict non-staff users accesss to data
1 parent 8836438 commit ee3bdfd

File tree

7 files changed

+135
-4
lines changed

7 files changed

+135
-4
lines changed

compliance-api/src/compliance_api/models/staff_user.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def update_staff(cls, user_id, user_dict, session=None) -> Optional[StaffUser]:
9292
def get_by_auth_guid(cls, auth_guid: str) -> StaffUser:
9393
"""Retrieve the staff user by auth_guid."""
9494
staff_user = StaffUser.query.filter_by(
95-
auth_user_guid=auth_guid, is_deleted=False
95+
auth_user_guid=auth_guid, is_deleted=False, is_active=True
9696
).first()
9797
return staff_user
9898

compliance-api/src/compliance_api/resources/staff_user.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,25 @@ def delete(user_id):
121121
return StaffUserSchema().dump(deleted_user), HTTPStatus.OK
122122

123123

124+
@cors_preflight("GET")
125+
@API.route("/by-auth-user-guid/<string:auth_user_guid>", methods=["GET"])
126+
@API.doc(params={"auth_user_guid": "The auth user GUID"})
127+
class StaffUserByAuthGuid(Resource):
128+
"""Resource for getting staff user by auth_user_guid."""
129+
130+
@staticmethod
131+
@auth.require
132+
@ApiHelper.swagger_decorators(API, endpoint_description="Fetch a user by auth_user_guid")
133+
@API.response(code=200, model=user_list_model, description="Success")
134+
@API.response(404, "Not Found")
135+
def get(auth_user_guid):
136+
"""Fetch a user by auth_user_guid."""
137+
user = StaffUserService.get_user_by_auth_guid(auth_user_guid)
138+
if not user:
139+
raise ResourceNotFoundError(f"User with auth_user_guid {auth_user_guid} not found")
140+
return StaffUserSchema().dump(user), HTTPStatus.OK
141+
142+
124143
@cors_preflight("GET")
125144
@API.route("/permissions", methods=["GET"])
126145
class StaffUserPermissions(Resource):

compliance-api/src/compliance_api/services/staff_user.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ def get_user_by_id(cls, user_id):
2323
)
2424
return staff_user
2525

26+
@classmethod
27+
def get_user_by_auth_guid(cls, auth_user_guid: str):
28+
"""Get user by auth_user_guid."""
29+
staff_user = StaffUserModel.get_by_auth_guid(auth_user_guid)
30+
if staff_user:
31+
auth_user = AuthService.get_epic_user_by_guid(staff_user.auth_user_guid)
32+
staff_user = _set_permission_level_in_compliance_user_obj(
33+
staff_user, auth_user
34+
)
35+
return staff_user
36+
2637
@classmethod
2738
def get_all_staff_users(cls):
2839
"""Get all users."""

compliance-web/src/hooks/useAuthorization.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { StaffUser } from "@/models/Staff";
22
import { jwtDecode, JwtPayload } from "jwt-decode";
33
import { useAuth } from "react-oidc-context";
4+
import { useQuery } from "@tanstack/react-query";
5+
import { request } from "@/utils/axiosUtils";
46
import { OidcConfig } from "@/utils/config";
57

68
interface CustomJwtPayload extends JwtPayload {
@@ -51,3 +53,53 @@ export const useCurrentLoggedInUser = () => {
5153
const { user } = useAuth();
5254
return user?.profile;
5355
};
56+
57+
export const useStaffUserValidation = () => {
58+
const { user, isAuthenticated } = useAuth();
59+
const preferredUsername = user?.profile?.preferred_username;
60+
61+
const {
62+
data: staffUser,
63+
isLoading,
64+
error,
65+
isError,
66+
} = useQuery({
67+
queryKey: ["staff-user-validation", preferredUsername],
68+
queryFn: async (): Promise<StaffUser | null> => {
69+
if (!preferredUsername) {
70+
throw new Error("No preferred_username found in token");
71+
}
72+
73+
try {
74+
const response = await request({
75+
url: `/staff-users/by-auth-user-guid/${preferredUsername}`,
76+
method: "get"
77+
});
78+
return response;
79+
} catch (error: unknown) {
80+
// If user not found (404), return null instead of throwing
81+
if (error && typeof error === 'object' && 'response' in error &&
82+
error.response && typeof error.response === 'object' && 'status' in error.response &&
83+
error.response.status === 404) {
84+
return null;
85+
}
86+
throw error;
87+
}
88+
},
89+
enabled: isAuthenticated && !!preferredUsername,
90+
retry: false, // Don't retry if staff user doesn't exist
91+
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
92+
});
93+
94+
const isValidStaffUser = !!staffUser;
95+
const isAccessDenied = !isLoading && !isError && !isValidStaffUser;
96+
97+
return {
98+
staffUser,
99+
isLoading,
100+
error,
101+
isValidStaffUser,
102+
isAccessDenied,
103+
preferredUsername,
104+
};
105+
};

compliance-web/src/router/RouterProviderWithAuthContext.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { RouterProvider } from "@tanstack/react-router";
22
import { useAuth } from "react-oidc-context";
33
import router from "./router";
44
import { useEffect } from "react";
5+
import { useStaffUserValidation } from "@/hooks/useAuthorization";
56

67
// Register the router instance for type safety
78
declare module "@tanstack/react-router" {
@@ -12,6 +13,7 @@ declare module "@tanstack/react-router" {
1213

1314
export default function RouterProviderWithAuthContext() {
1415
const authentication = useAuth();
16+
const { isAccessDenied, preferredUsername } = useStaffUserValidation();
1517

1618
useEffect(() => {
1719
// the `return` is important - addAccessTokenExpiring() returns a cleanup function
@@ -22,5 +24,31 @@ export default function RouterProviderWithAuthContext() {
2224
});
2325
}, [authentication, authentication.events, authentication.signinSilent]);
2426

27+
// Show access denied if user is authenticated but not a valid staff user
28+
if (authentication.isAuthenticated && isAccessDenied) {
29+
return (
30+
<div style={{ padding: "2rem", textAlign: "center" }}>
31+
<h1>Access Denied</h1>
32+
<p>You are not authorized to access this application.</p>
33+
<p>User ID: {preferredUsername}</p>
34+
<p>Please contact your administrator to get access.</p>
35+
<button
36+
onClick={() => authentication.signoutRedirect()}
37+
style={{
38+
padding: "0.5rem 1rem",
39+
marginTop: "1rem",
40+
backgroundColor: "#dc3545",
41+
color: "white",
42+
border: "none",
43+
borderRadius: "4px",
44+
cursor: "pointer"
45+
}}
46+
>
47+
Sign Out
48+
</button>
49+
</div>
50+
);
51+
}
52+
2553
return <RouterProvider router={router} context={{ authentication }} />;
2654
}

compliance-web/src/routes/_authenticated/admin/staff.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export function Staff() {
100100

101101
const onDeleteSuccess = () => {
102102
queryClient.invalidateQueries({ queryKey: ["staff-users"] });
103+
queryClient.invalidateQueries({ queryKey: ["staff-user-validation"] });
103104
setClose();
104105
notify.success("Staff deleted successfully!");
105106
};
Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,42 @@
11
import { createFileRoute, Navigate } from "@tanstack/react-router";
22
import { useAuth } from "react-oidc-context";
3+
import { useStaffUserValidation } from "@/hooks/useAuthorization";
4+
import { notify } from "@/store/snackbarStore";
35

46
export const Route = createFileRoute("/oidc-callback")({
57
component: OidcCallback,
68
});
79

810
function OidcCallback() {
911
const { isAuthenticated, isLoading, error } = useAuth();
12+
const {
13+
isLoading: isValidatingStaff,
14+
isValidStaffUser,
15+
isAccessDenied,
16+
preferredUsername,
17+
error: staffValidationError
18+
} = useStaffUserValidation();
1019

11-
if (isLoading) {
20+
if (isLoading || isValidatingStaff) {
1221
return <h1>Redirecting, Please wait...</h1>;
1322
}
1423

1524
if (error?.message) {
1625
return <h1>Error: {error.message}</h1>;
1726
}
1827

19-
if(!isLoading && isAuthenticated) {
20-
return <Navigate to="/ce-database/case-files"></Navigate>
28+
if (staffValidationError && staffValidationError.message !== "No preferred_username found in token") {
29+
return <h1>Error validating user: {staffValidationError.message}</h1>;
2130
}
31+
32+
if (isAccessDenied) {
33+
notify.error(`Access Denied: User ${preferredUsername} is not authorized to access this application. Please contact your administrator.`);
34+
return <h1>Access denied. Redirecting...</h1>;
35+
}
36+
37+
if (!isLoading && isAuthenticated && isValidStaffUser) {
38+
return <Navigate to="/ce-database/case-files"></Navigate>;
39+
}
40+
41+
return <h1>Authentication in progress...</h1>;
2242
}

0 commit comments

Comments
 (0)