Skip to content

Commit ddff143

Browse files
authored
Merge pull request #172 from aws-samples/wip/tmscarla/configurable-oidc-roles
feat: make user roles claim in the jwt configurable, using 'cognito:groups' as default
2 parents ff04e64 + ff7a25f commit ddff143

File tree

10 files changed

+55
-35
lines changed

10 files changed

+55
-35
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ npm run dev
116116

117117
Then navigate to [http://localhost:3000](http://localhost:3000)
118118

119+
## Testing
120+
121+
Launch tests of the API backend by running:
122+
123+
```bash
124+
pytest
125+
```
126+
For detailed information on how to invoke `pytest`, see this [resource](https://docs.pytest.org/en/7.1.x/how-to/usage.html).
127+
119128
## Security
120129

121130
See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.

api/PclusterApiHandler.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
SECRET_ID = os.getenv("SECRET_ID")
3434
ENABLE_MFA = os.getenv("ENABLE_MFA")
3535
SITE_URL = os.getenv("SITE_URL", API_BASE_URL)
36+
USER_ROLES_CLAIM = os.getenv("USER_ROLES_CLAIM", "cognito:groups")
3637

3738
try:
3839
if (not USER_POOL_ID or USER_POOL_ID == "") and SECRET_ID:
@@ -137,7 +138,7 @@ def authenticate(group):
137138
return auth_redirect()
138139
except jose.exceptions.JWSSignatureError:
139140
return logout()
140-
if not disable_auth() and (group != "guest") and (group not in set(decoded.get("cognito:groups", []))):
141+
if not disable_auth() and (group != "guest") and (group not in set(decoded.get(USER_ROLES_CLAIM, []))):
141142
return auth_redirect()
142143

143144

@@ -540,15 +541,24 @@ def get_instance_types():
540541
return {"instance_types": sorted(instance_types, key=lambda x: x["InstanceType"])}
541542

542543

544+
def _get_user_roles(decoded):
545+
print(os.environ.get("USER_ROLES_CLAIM"))
546+
return decoded[USER_ROLES_CLAIM] if USER_ROLES_CLAIM in decoded else ["user"]
547+
548+
549+
543550
def get_identity():
544551
if running_local():
545-
return {"cognito:groups": ["user", "admin"], "username": "username", "attributes": {"email": "[email protected]"}}
552+
return {"user_roles": ["user", "admin"], "username": "username", "attributes": {"email": "[email protected]"}}
546553

547554
access_token = request.cookies.get("accessToken")
548555
if not access_token:
549556
return {"message": "No access token."}, 401
550557
try:
551558
decoded = jwt_decode(access_token, USER_POOL_ID)
559+
decoded["user_roles"] = _get_user_roles(decoded)
560+
decoded.pop(USER_ROLES_CLAIM)
561+
552562
username = decoded.get("username")
553563
if username:
554564
cognito = boto3.client("cognito-idp")
@@ -559,7 +569,7 @@ def get_identity():
559569
return {"message": "Signature expired."}, 401
560570

561571
if disable_auth():
562-
decoded["cognito:groups"] = ["user", "admin"]
572+
decoded["user_roles"] = ["user", "admin"]
563573

564574
return decoded
565575

api/tests/__init__.py

Whitespace-only changes.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import pytest
2+
from unittest import mock
3+
from api.PclusterApiHandler import _get_user_roles
4+
5+
6+
@mock.patch("api.PclusterApiHandler.USER_ROLES_CLAIM", "user_roles")
7+
def test_user_roles():
8+
user_roles = ["user", "admin"]
9+
10+
_test_decoded_with_user_roles_claim(decoded={"user_roles": user_roles}, user_roles=user_roles)
11+
_test_decoded_without_user_roles_claim(decoded={})
12+
13+
14+
15+
def _test_decoded_with_user_roles_claim(decoded, user_roles):
16+
assert _get_user_roles(decoded) == user_roles
17+
18+
19+
def _test_decoded_without_user_roles_claim(decoded):
20+
assert _get_user_roles(decoded) == ["user"]

frontend/src/auth/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const USER_ROLES_CLAIM = "user_roles"

frontend/src/components/SideBar.js

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
// limitations under the License.
1111
import * as React from 'react';
1212
import { Link, useLocation } from "react-router-dom"
13-
import { setState, useState, isAdmin} from '../store'
13+
import { setState, useState, isGuest, isUser, isAdmin } from '../store'
1414

1515
// UI Elements
1616
import Divider from '@mui/material/Divider';
@@ -24,18 +24,8 @@ import HomeIcon from '@mui/icons-material/Home';
2424
import GroupIcon from '@mui/icons-material/Group';
2525

2626
export function SideBarIcons(props) {
27-
let identity = useState(['identity']);
28-
let groups = useState(['identity', 'cognito:groups']) || [];
2927
const drawerOpen = useState(['app', 'sidebar', 'drawerOpen']);
30-
31-
const isGuest = () => {
32-
return identity && (!groups || ((!groups.includes("admin")) && (!groups.includes("user"))));
33-
}
34-
35-
const isUser = () => {
36-
return groups && ((groups.includes("admin")) || (groups.includes("user")));
37-
}
38-
28+
3929
const location = useLocation();
4030
let defaultPage = isGuest() ? "home" : "clusters";
4131
let section = location && location.pathname && location.pathname.substring(1);
@@ -72,18 +62,7 @@ export function SideBarIcons(props) {
7262
}
7363

7464
export default function SideBar(props) {
75-
let identity = useState(['identity']);
76-
let groups = useState(['identity', 'cognito:groups']);
7765
const drawerOpen = useState(['app', 'sidebar', 'drawerOpen']);
78-
79-
const isGuest = () => {
80-
return identity && (!groups || ((!groups.includes("admin")) && (!groups.includes("user"))));
81-
}
82-
83-
const isUser = () => {
84-
return groups && ((groups.includes("admin")) || (groups.includes("user")));
85-
}
86-
8766
useNotifier();
8867

8968
const location = useLocation();

frontend/src/model.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import axios from 'axios'
1212
import { store, setState, getState, clearState, updateState, clearAllState } from './store'
1313
import { enqueueSnackbar as enqueueSnackbarAction } from './redux/snackbar_actions';
1414
import { closeSnackbar as closeSnackbarAction } from './redux/snackbar_actions';
15+
import { USER_ROLES_CLAIM } from './auth/constants';
1516

1617
// UI Elements
1718
import Button from '@mui/material/Button';
@@ -768,7 +769,7 @@ function LoadInitialState() {
768769
clearAllState();
769770
GetVersion();
770771
GetIdentity((identity) => {
771-
let groups = identity['cognito:groups'];
772+
let groups = identity[USER_ROLES_CLAIM];
772773
if(groups && (groups.includes("admin") || groups.includes("user")))
773774
{
774775
ListUsers();

frontend/src/pages/index.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,12 @@ import CssBaseline from '@mui/material/CssBaseline';
3030
// Components
3131
import Loading from '../components/Loading'
3232

33+
import { isGuest } from '../store';
34+
35+
3336
export default function App() {
3437
const identity = useState(['identity']);
35-
const groups = useState(['identity', 'cognito:groups']);
36-
37-
const isGuest = () => {
38-
return identity && (!groups || ((!groups.includes("admin")) && (!groups.includes("user"))));
39-
}
40-
38+
4139
React.useEffect(() => {
4240
LoadInitialState();
4341
}, [])

frontend/src/store.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { createStore, combineReducers } from '@reduxjs/toolkit'
1212
import { useSelector } from 'react-redux'
1313
import notifications from './redux/snackbar_reducer'
1414
import { getIn, swapIn } from './util'
15+
import { USER_ROLES_CLAIM } from './auth/constants'
1516

1617
// These are identity reducers that allow the names to be at the top level for combining
1718
function clusters(state = {}, action){ return state }
@@ -143,12 +144,12 @@ function useState(path) {
143144
}
144145

145146
function isAdmin() {
146-
let groups = getState(['identity', 'cognito:groups']) || [];
147+
let groups = getState(['identity', USER_ROLES_CLAIM]) || [];
147148
return groups && groups.includes("admin");
148149
}
149150

150151
function isUser() {
151-
let groups = getState(['identity', 'cognito:groups']) || [];
152+
let groups = getState(['identity', USER_ROLES_CLAIM]) || [];
152153
return groups && ((groups.includes("admin")) || (groups.includes("user")));
153154
}
154155

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ boto3
44
requests
55
python-jose
66
pyyaml
7+
pytest

0 commit comments

Comments
 (0)