Skip to content

Commit b7676e4

Browse files
authored
feat(OIDC): API Access tab with trust relationships UI (#8044)
1 parent e41cb20 commit b7676e4

27 files changed

Lines changed: 1835 additions & 28 deletions

File tree

frontend/common/services/useMasterAPIKeyWithMasterAPIKeyRole.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ export const masterAPIKeyWithMasterAPIKeyRoleService = service
3030
}),
3131
}),
3232
getRolesMasterAPIKeyWithMasterAPIKeyRoles: builder.query<
33-
Res['masterAPIKeyWithMasterAPIKeyRoles'],
34-
Req['getMasterAPIKeyWithMasterAPIKeyRoles']
33+
Res['rolesMasterAPIKeyWithMasterAPIKeyRoles'],
34+
Req['getRolesMasterAPIKeyWithMasterAPIKeyRoles']
3535
>({
36-
providesTags: (res) => [
37-
{ id: res?.id, type: 'MasterAPIKeyWithMasterAPIKeyRole' },
36+
providesTags: (_res, _error, query) => [
37+
{ id: query.prefix, type: 'MasterAPIKeyWithMasterAPIKeyRole' },
3838
],
39-
query: (query: Req['getMasterAPIKeyWithMasterAPIKeyRoles']) => ({
39+
query: (query: Req['getRolesMasterAPIKeyWithMasterAPIKeyRoles']) => ({
4040
url: `organisations/${query.org_id}/master-api-keys/${query.prefix}/roles/`,
4141
}),
4242
}),
@@ -87,7 +87,7 @@ export async function getRolesMasterAPIKeyWithMasterAPIKeyRoles(
8787

8888
export async function deleteMasterAPIKeyWithMasterAPIKeyRoles(
8989
store: any,
90-
data: Req['getMasterAPIKeyWithMasterAPIKeyRoles'],
90+
data: Req['deleteMasterAPIKeyWithMasterAPIKeyRoles'],
9191
options?: Parameters<
9292
typeof masterAPIKeyWithMasterAPIKeyRoleService.endpoints.deleteMasterAPIKeyWithMasterAPIKeyRoles.initiate
9393
>[1],
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { Res } from 'common/types/responses'
2+
import { Req } from 'common/types/requests'
3+
import { service } from 'common/service'
4+
5+
export const trustRelationshipService = service
6+
.enhanceEndpoints({
7+
addTagTypes: ['TrustRelationship', 'MasterAPIKeyWithMasterAPIKeyRole'],
8+
})
9+
.injectEndpoints({
10+
endpoints: (builder) => ({
11+
createTrustRelationship: builder.mutation<
12+
Res['trustRelationship'],
13+
Req['createTrustRelationship']
14+
>({
15+
invalidatesTags: [{ id: 'LIST', type: 'TrustRelationship' }],
16+
query: (query: Req['createTrustRelationship']) => ({
17+
body: query.body,
18+
method: 'POST',
19+
url: `organisations/${query.organisation_id}/trust-relationships/`,
20+
}),
21+
}),
22+
deleteTrustRelationship: builder.mutation<
23+
void,
24+
Req['deleteTrustRelationship']
25+
>({
26+
invalidatesTags: [{ id: 'LIST', type: 'TrustRelationship' }],
27+
query: (query: Req['deleteTrustRelationship']) => ({
28+
method: 'DELETE',
29+
url: `organisations/${query.organisation_id}/trust-relationships/${query.id}/`,
30+
}),
31+
}),
32+
getTrustRelationships: builder.query<
33+
Res['trustRelationships'],
34+
Req['getTrustRelationships']
35+
>({
36+
providesTags: [{ id: 'LIST', type: 'TrustRelationship' }],
37+
query: (query: Req['getTrustRelationships']) => ({
38+
url: `organisations/${query.organisation_id}/trust-relationships/`,
39+
}),
40+
}),
41+
updateTrustRelationship: builder.mutation<
42+
Res['trustRelationship'],
43+
Req['updateTrustRelationship']
44+
>({
45+
// Turning `is_admin` on detaches the backing key's roles server-side,
46+
// so the cached role list has to go with it.
47+
invalidatesTags: (res) => [
48+
{ id: 'LIST', type: 'TrustRelationship' },
49+
{ id: res?.id, type: 'TrustRelationship' },
50+
'MasterAPIKeyWithMasterAPIKeyRole',
51+
],
52+
query: (query: Req['updateTrustRelationship']) => ({
53+
body: query.body,
54+
method: 'PUT',
55+
url: `organisations/${query.organisation_id}/trust-relationships/${query.id}/`,
56+
}),
57+
}),
58+
// END OF ENDPOINTS
59+
}),
60+
})
61+
62+
export async function getTrustRelationships(
63+
store: any,
64+
data: Req['getTrustRelationships'],
65+
options?: Parameters<
66+
typeof trustRelationshipService.endpoints.getTrustRelationships.initiate
67+
>[1],
68+
) {
69+
return store.dispatch(
70+
trustRelationshipService.endpoints.getTrustRelationships.initiate(
71+
data,
72+
options,
73+
),
74+
)
75+
}
76+
// END OF FUNCTION_EXPORTS
77+
78+
export const {
79+
useCreateTrustRelationshipMutation,
80+
useDeleteTrustRelationshipMutation,
81+
useGetTrustRelationshipsQuery,
82+
useUpdateTrustRelationshipMutation,
83+
// END OF EXPORTS
84+
} = trustRelationshipService
85+
86+
/* Usage examples:
87+
const { data, isLoading } = useGetTrustRelationshipsQuery({ organisation_id: 2 }) //get hook
88+
const [createTrustRelationship, { isLoading, data, isSuccess }] = useCreateTrustRelationshipMutation() //create hook
89+
trustRelationshipService.endpoints.getTrustRelationships.select({organisation_id: 2})(store.getState()) //access data from any function
90+
*/

frontend/common/types/requests.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
TagStrategy,
3434
FeatureType,
3535
LifecycleStage,
36+
TrustRelationshipClaimRule,
3637
} from './responses'
3738
import { UtmsType } from './utms'
3839

@@ -496,14 +497,41 @@ export type Req = {
496497
getRoleMasterApiKey: { org_id: number; role_id: number; id: string }
497498
updateRoleMasterApiKey: { org_id: number; role_id: number; id: string }
498499
deleteRoleMasterApiKey: { org_id: number; role_id: number; id: string }
499-
createRoleMasterApiKey: { org_id: number; role_id: number }
500+
createRoleMasterApiKey: {
501+
org_id: number
502+
role_id: number
503+
body: { master_api_key: string }
504+
}
500505
getMasterAPIKeyWithMasterAPIKeyRoles: { org_id: number; prefix: string }
501506
deleteMasterAPIKeyWithMasterAPIKeyRoles: {
502507
org_id: number
503508
prefix: string
504509
role_id: number
505510
}
506511
getRolesMasterAPIKeyWithMasterAPIKeyRoles: { org_id: number; prefix: string }
512+
getTrustRelationships: { organisation_id: number }
513+
createTrustRelationship: {
514+
organisation_id: number
515+
body: {
516+
name: string
517+
issuer: string
518+
audience: string
519+
claim_rules: TrustRelationshipClaimRule[]
520+
is_admin: boolean
521+
}
522+
}
523+
updateTrustRelationship: {
524+
organisation_id: number
525+
id: number
526+
body: {
527+
name: string
528+
issuer: string
529+
audience: string
530+
claim_rules: TrustRelationshipClaimRule[]
531+
is_admin: boolean
532+
}
533+
}
534+
deleteTrustRelationship: { organisation_id: number; id: number }
507535
createLaunchDarklyProjectImport: {
508536
project_id: number
509537
body: {

frontend/common/types/responses.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,6 +1302,24 @@ export type WarehouseConnection = {
13021302
unique_events_count: number | null
13031303
}
13041304

1305+
export type TrustRelationshipClaimRule = {
1306+
claim: string
1307+
values: string[]
1308+
}
1309+
1310+
export type TrustRelationship = {
1311+
id: number
1312+
name: string
1313+
issuer: string
1314+
audience: string
1315+
claim_rules: TrustRelationshipClaimRule[]
1316+
is_admin: boolean
1317+
master_api_key_id: string
1318+
master_api_key_prefix: string
1319+
created_at: string
1320+
created_by: number | null
1321+
}
1322+
13051323
export type Res = {
13061324
segments: PagedResponse<Segment>
13071325
segment: Segment
@@ -1392,11 +1410,14 @@ export type Res = {
13921410
launchDarklyProjectImport: LaunchDarklyProjectImport
13931411
launchDarklyProjectsImport: LaunchDarklyProjectImport[]
13941412
roleMasterApiKey: { id: number; master_api_key: string; role: number }
1413+
trustRelationship: TrustRelationship
1414+
trustRelationships: PagedResponse<TrustRelationship>
13951415
masterAPIKeyWithMasterAPIKeyRoles: {
13961416
id: string
13971417
prefix: string
13981418
roles: RolePermissionUser[]
13991419
}
1420+
rolesMasterAPIKeyWithMasterAPIKeyRoles: PagedResponse<Role>
14001421
userWithRoles: PagedResponse<Role>
14011422
groupWithRole: PagedResponse<Role>
14021423
changeRequests: PagedResponse<ChangeRequestSummary>

frontend/web/components/AdminAPIKeys.js

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import OrganisationStore from 'common/stores/organisation-store'
77
import getUserDisplayName from 'common/utils/getUserDisplayName'
88
import Token from './Token'
99
import JSONReference from './JSONReference'
10+
import PageTitle from './PageTitle'
11+
import PlanBasedBanner from './PlanBasedAccess'
1012
import Button from './base/forms/Button'
1113
import DateSelect from './DateSelect'
1214
import Icon from './icons/Icon'
@@ -201,8 +203,8 @@ export class CreateAPIKey extends PureComponent {
201203
/>
202204
</Flex>
203205
<>
204-
<Row className='mb-3 mt-4'>
205-
<label className='mr-2'>Is admin</label>
206+
<Row className='mb-3 mt-4 gap-2'>
207+
<label className='mb-0'>Is admin</label>
206208
<Switch
207209
onChange={() => {
208210
this.setState({
@@ -212,7 +214,13 @@ export class CreateAPIKey extends PureComponent {
212214
checked={is_admin}
213215
disabled={!Utils.getPlansPermission('RBAC') && is_admin}
214216
/>
217+
<PlanBasedBanner feature='RBAC' theme='badge' />
215218
</Row>
219+
<PlanBasedBanner
220+
feature='RBAC'
221+
theme='description'
222+
className='mb-4'
223+
/>
216224
{!is_admin && (
217225
<>
218226
<Row className='mb-3 mt-4'>
@@ -405,6 +413,9 @@ export default class AdminAPIKeys extends PureComponent {
405413
isLoading: false,
406414
})
407415
})
416+
.catch(() => {
417+
this.setState({ isLoading: false })
418+
})
408419
}
409420

410421
remove = (v) => {
@@ -439,12 +450,19 @@ export default class AdminAPIKeys extends PureComponent {
439450
title={'API Keys'}
440451
json={apiKeys}
441452
/>
442-
<Column className='my-4 ml-0 col-md-6'>
443-
<h5 className='mb-1'>{`${'Manage'} API Keys`}</h5>
444-
<p className='mb-0 fs-small lh-sm'>
445-
{`API keys are used to authenticate with the Admin API.`}
446-
</p>
447-
<div className='mb-4 fs-small lh-sm'>
453+
<div className='mt-4'>
454+
<PageTitle
455+
title='API keys'
456+
cta={
457+
<Button
458+
onClick={this.createAPIKey}
459+
disabled={this.state.isLoading}
460+
>
461+
{`Create API Key`}
462+
</Button>
463+
}
464+
>
465+
{`API keys are used to authenticate with the Admin API. `}
448466
<Button
449467
theme='text'
450468
href='https://docs.flagsmith.com/integrations/terraform#terraform-api-key'
@@ -453,11 +471,8 @@ export default class AdminAPIKeys extends PureComponent {
453471
>
454472
{`Learn about API Keys.`}
455473
</Button>
456-
</div>
457-
<Button onClick={this.createAPIKey} disabled={this.state.isLoading}>
458-
{`Create API Key`}
459-
</Button>
460-
</Column>
474+
</PageTitle>
475+
</div>
461476
{(this.state.isLoading || !OrganisationStore.model?.users) && (
462477
<div className='text-center'>
463478
<Loader />

frontend/web/components/pages/organisation-settings/OrganisationSettingsPage.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,20 @@ const OrganisationSettingsPage: FC = () => {
4040
API.trackPage(Constants.pages.ORGANISATION_SETTINGS)
4141
}, [])
4242

43-
// Back-compat: the SAML tab was renamed to SSO. Existing bookmarks and
44-
// links pointing at ?tab=saml redirect to ?tab=sso so users land in the
45-
// right place.
43+
// Back-compat: renamed tabs redirect so existing bookmarks and links land
44+
// in the right place (SAML became SSO; the API Keys tab became API Access
45+
// with the `keys` slug).
4646
const history = useHistory()
4747
const location = useLocation()
4848
useEffect(() => {
49+
const legacyTabs: Record<string, string> = {
50+
'api-keys': 'keys',
51+
'saml': 'sso',
52+
}
4953
const params = new URLSearchParams(location.search)
50-
if (params.get('tab') === 'saml') {
51-
params.set('tab', 'sso')
54+
const redirectTo = legacyTabs[params.get('tab') || '']
55+
if (redirectTo) {
56+
params.set('tab', redirectTo)
5257
history.replace(`${location.pathname}?${params.toString()}`)
5358
}
5459
}, [history, location])
@@ -116,7 +121,7 @@ const OrganisationSettingsPage: FC = () => {
116121
component: <APIKeysTab organisationId={organisation.id} />,
117122
isVisible: true,
118123
key: 'keys',
119-
label: 'API Keys',
124+
label: 'API Access',
120125
},
121126
{
122127
component: <WebhooksTab organisationId={organisation.id} />,
@@ -137,7 +142,12 @@ const OrganisationSettingsPage: FC = () => {
137142
<PageTitle title='Organisation Settings' />
138143
<Tabs urlParam='tab' className='mt-0' uncontrolled hideNavOnSingleTab>
139144
{tabs.map(({ component, key, label }) => (
140-
<TabItem key={key} tabLabel={label} data-test={key}>
145+
<TabItem
146+
key={key}
147+
tabLabel={label}
148+
tabLabelString={key}
149+
data-test={key}
150+
>
141151
{component}
142152
</TabItem>
143153
))}
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import React from 'react'
22
import AdminAPIKeys from 'components/AdminAPIKeys'
3+
import Utils from 'common/utils/utils'
4+
import TrustRelationships from './trust-relationships'
35

46
type APIKeysTabProps = {
57
organisationId: number
68
}
79

810
export const APIKeysTab = ({ organisationId }: APIKeysTabProps) => {
9-
return <AdminAPIKeys organisationId={organisationId} />
11+
return (
12+
<>
13+
<AdminAPIKeys organisationId={organisationId} />
14+
{Utils.getFlagsmithHasFeature('trust_relationships') && (
15+
<TrustRelationships organisationId={organisationId} />
16+
)}
17+
</>
18+
)
1019
}

0 commit comments

Comments
 (0)