Skip to content

Commit a1fe928

Browse files
authored
Merge pull request #5 from tswistak/feature/support-resource-roles
feat(): add support for resource roles
2 parents 6839c8c + 7908cf0 commit a1fe928

File tree

5 files changed

+49
-14
lines changed

5 files changed

+49
-14
lines changed

README.md

+17-13
Original file line numberDiff line numberDiff line change
@@ -151,29 +151,33 @@ const {
151151
decodedToken,
152152
username,
153153
roles,
154+
resourceRoles,
154155
keycloak,
155156

156157
// Functions
157158
hasRoles,
159+
hasResourceRoles,
158160
} = useKeycloak()
159161
```
160162

161-
| State | Type | Description |
162-
| --------------- | --------------------------- | ------------------------------------------------------ |
163-
| isAuthenticated | `Ref<boolean>` | If `true` the user is authenticated. |
164-
| isPending | `Ref<boolean>` | If `true` the authentication request is still pending. |
165-
| hasFailed | `Ref<boolean>` | If `true` authentication request has failed. |
166-
| token | `Ref<string>` | `token` is the raw value of the JWT token. |
167-
| decodedToken | `Ref<T>` | `decodedToken` is the decoded value of the JWT token. |
168-
| username | `Ref<string>` | `username` the name of our user. |
169-
| roles | `Ref<string[]>` | `roles` is a list of the users roles. |
170-
| keycloak | `Keycloak.KeycloakInstance` | `keycloak` is the instance of the keycloak-js adapter. |
163+
| State | Type | Description |
164+
| --------------- | ------------------------------ | ------------------------------------------------------------------- |
165+
| isAuthenticated | `Ref<boolean>` | If `true` the user is authenticated. |
166+
| isPending | `Ref<boolean>` | If `true` the authentication request is still pending. |
167+
| hasFailed | `Ref<boolean>` | If `true` authentication request has failed. |
168+
| token | `Ref<string>` | `token` is the raw value of the JWT token. |
169+
| decodedToken | `Ref<T>` | `decodedToken` is the decoded value of the JWT token. |
170+
| username | `Ref<string>` | `username` the name of our user. |
171+
| roles | `Ref<string[]>` | `roles` is a list of the users roles. |
172+
| resourceRoles | `Ref<Record<string, string[]>` | `resourceRoles` is a list of the users roles in specific resources. |
173+
| keycloak | `Keycloak.KeycloakInstance` | `keycloak` is the instance of the keycloak-js adapter. |
171174

172175
#### Functions
173176

174-
| Function | Type | Description |
175-
| -------- | ------------------------------ | ------------------------------------------------------------ |
176-
| hasRoles | `(roles: string[]) => boolean` | `hasRoles` returns true if the user has all the given roles. |
177+
| Function | Type | Description |
178+
| ---------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------- |
179+
| hasRoles | `(roles: string[]) => boolean` | `hasRoles` returns true if the user has all the given roles. |
180+
| hasResourceRoles | `(roles: string[], resource: string) => boolean` | `hasResourceRoles` returns true if the user has all the given roles in a resource. |
177181

178182
# License
179183

src/composable.test.ts

+16
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,20 @@ describe('useKeycloak', () => {
2626
expect(hasRoles(null)).toBe(false)
2727
})
2828
})
29+
describe('hasResourceRoles', () => {
30+
test('should tell if the user has the role in a resource or not and is authenticated', () => {
31+
state.isAuthenticated = true
32+
state.resourceRoles = { myApp: ['my-role', 'my-other-role'] }
33+
const { hasResourceRoles } = useKeycloak()
34+
35+
expect(hasResourceRoles(['my-role', 'my-other-role'], 'myApp')).toBe(true)
36+
expect(hasResourceRoles(['my-role', 'not-my-role'], 'myApp')).toBe(false)
37+
expect(hasResourceRoles(['my-role', 'my-other-role'], undefined)).toBe(false)
38+
expect(hasResourceRoles(['my-role', 'my-other-role'], null)).toBe(false)
39+
expect(hasResourceRoles(undefined, undefined)).toBe(false)
40+
expect(hasResourceRoles(undefined, 'myApp')).toBe(false)
41+
expect(hasResourceRoles(null, null)).toBe(false)
42+
expect(hasResourceRoles(null, 'myApp')).toBe(false)
43+
})
44+
})
2945
})

src/composable.ts

+7
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ export interface KeycloakComposable {
1111
token: Ref<string>
1212
username: Ref<string>
1313
roles: Ref<string[]>
14+
resourceRoles: Ref<Record<string, string[]>>
1415
keycloak: KeycloakInstance
1516
hasRoles: (roles: string[]) => boolean
17+
hasResourceRoles: (roles: string[], resource: string) => boolean
1618
}
1719

1820
export const useKeycloak = (): KeycloakComposable => {
@@ -21,5 +23,10 @@ export const useKeycloak = (): KeycloakComposable => {
2123
keycloak: getKeycloak(),
2224
hasRoles: (roles: string[]) =>
2325
!isNil(roles) && state.isAuthenticated && roles.every(role => state.roles.includes(role)),
26+
hasResourceRoles: (roles: string[], resource: string) =>
27+
!isNil(roles) &&
28+
!isNil(resource) &&
29+
state.isAuthenticated &&
30+
roles.every(role => state.resourceRoles[resource].includes(role)),
2431
}
2532
}

src/state.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { state, setToken } from './state'
22

33
describe('state', () => {
44
const token =
5-
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJteS1uYW1lIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm15LXJvbGUiXX19.caCXjtPtGJ84gjyWZRZwOPJE0hfMYpbD34KJhA_aASU'
5+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJteS1uYW1lIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm15LXJvbGUiXX0sInJlc291cmNlX2FjY2VzcyI6eyJteS1hcHAiOnsicm9sZXMiOlsibXktcm9sZSJdfX19.oAnF7H8DndIWOb2KeHntbzwf6h7VjZlxt5AR2KPZTBU'
66

77
test('should have the correct inital values', () => {
88
expect(state.isAuthenticated).toBe(false)
@@ -11,6 +11,7 @@ describe('state', () => {
1111
expect(state.token).toBe('')
1212
expect(state.username).toBe('')
1313
expect(state.roles).toStrictEqual([])
14+
expect(state.resourceRoles).toStrictEqual({});
1415
})
1516

1617
test('should update the state', () => {
@@ -19,5 +20,6 @@ describe('state', () => {
1920
expect(state.token).toBe(token)
2021
expect(state.username).toBe('my-name')
2122
expect(state.roles).toStrictEqual(['my-role'])
23+
expect(state.resourceRoles).toStrictEqual({ 'my-app': ['my-role'] })
2224
})
2325
})

src/state.ts

+6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface KeycloakState<T = unknown> {
99
decodedToken: T
1010
username: string
1111
roles: string[]
12+
resourceRoles: Record<string, string[]>
1213
}
1314

1415
export const state = reactive<KeycloakState>({
@@ -19,13 +20,15 @@ export const state = reactive<KeycloakState>({
1920
decodedToken: {},
2021
username: '',
2122
roles: [] as string[],
23+
resourceRoles: {},
2224
})
2325

2426
interface TokenContent {
2527
preferred_username: string
2628
realm_access: {
2729
roles: string[]
2830
}
31+
resource_access: Record<string, { roles: string[] }>
2932
}
3033

3134
export const setToken = (token: string): void => {
@@ -34,6 +37,9 @@ export const setToken = (token: string): void => {
3437
state.decodedToken = content
3538
state.roles = content.realm_access.roles
3639
state.username = content.preferred_username
40+
state.resourceRoles = content.resource_access ? Object.fromEntries(
41+
Object.entries(content.resource_access).map(([key, value]) => [key, value.roles]),
42+
) : {};
3743
}
3844

3945
export const hasFailed = (value: boolean): void => {

0 commit comments

Comments
 (0)