Skip to content

Commit 8a8c79c

Browse files
committed
feat(trust-relationships): Add API Access tab with trust relationships UI
The organisation settings API Keys tab becomes API Access and gains a Trust Relationships section behind the trust_relationships feature flag: list, create, edit and delete, with a claim-rules editor and the same admin toggle and RBAC role assignment flow as master API keys, attached via the backing key. Also fixes two latent request types in the master API key role services. beep boop
1 parent 2d8d889 commit 8a8c79c

15 files changed

Lines changed: 1277 additions & 23 deletions

frontend/common/services/useMasterAPIKeyWithMasterAPIKeyRole.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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'],
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+
invalidatesTags: (res) => [
46+
{ id: 'LIST', type: 'TrustRelationship' },
47+
{ id: res?.id, type: 'TrustRelationship' },
48+
],
49+
query: (query: Req['updateTrustRelationship']) => ({
50+
body: query.body,
51+
method: 'PUT',
52+
url: `organisations/${query.organisation_id}/trust-relationships/${query.id}/`,
53+
}),
54+
}),
55+
// END OF ENDPOINTS
56+
}),
57+
})
58+
59+
export async function getTrustRelationships(
60+
store: any,
61+
data: Req['getTrustRelationships'],
62+
options?: Parameters<
63+
typeof trustRelationshipService.endpoints.getTrustRelationships.initiate
64+
>[1],
65+
) {
66+
return store.dispatch(
67+
trustRelationshipService.endpoints.getTrustRelationships.initiate(
68+
data,
69+
options,
70+
),
71+
)
72+
}
73+
// END OF FUNCTION_EXPORTS
74+
75+
export const {
76+
useCreateTrustRelationshipMutation,
77+
useDeleteTrustRelationshipMutation,
78+
useGetTrustRelationshipsQuery,
79+
useUpdateTrustRelationshipMutation,
80+
// END OF EXPORTS
81+
} = trustRelationshipService
82+
83+
/* Usage examples:
84+
const { data, isLoading } = useGetTrustRelationshipsQuery({ organisation_id: 2 }) //get hook
85+
const [createTrustRelationship, { isLoading, data, isSuccess }] = useCreateTrustRelationshipMutation() //create hook
86+
trustRelationshipService.endpoints.getTrustRelationships.select({organisation_id: 2})(store.getState()) //access data from any function
87+
*/

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

@@ -494,14 +495,41 @@ export type Req = {
494495
getRoleMasterApiKey: { org_id: number; role_id: number; id: string }
495496
updateRoleMasterApiKey: { org_id: number; role_id: number; id: string }
496497
deleteRoleMasterApiKey: { org_id: number; role_id: number; id: string }
497-
createRoleMasterApiKey: { org_id: number; role_id: number }
498+
createRoleMasterApiKey: {
499+
org_id: number
500+
role_id: number
501+
body: { master_api_key: string }
502+
}
498503
getMasterAPIKeyWithMasterAPIKeyRoles: { org_id: number; prefix: string }
499504
deleteMasterAPIKeyWithMasterAPIKeyRoles: {
500505
org_id: number
501506
prefix: string
502507
role_id: number
503508
}
504509
getRolesMasterAPIKeyWithMasterAPIKeyRoles: { org_id: number; prefix: string }
510+
getTrustRelationships: { organisation_id: number }
511+
createTrustRelationship: {
512+
organisation_id: number
513+
body: {
514+
name: string
515+
issuer: string
516+
audience: string
517+
claim_rules: TrustRelationshipClaimRule[]
518+
is_admin: boolean
519+
}
520+
}
521+
updateTrustRelationship: {
522+
organisation_id: number
523+
id: number
524+
body: {
525+
name: string
526+
issuer: string
527+
audience: string
528+
claim_rules: TrustRelationshipClaimRule[]
529+
is_admin: boolean
530+
}
531+
}
532+
deleteTrustRelationship: { organisation_id: number; id: number }
505533
createLaunchDarklyProjectImport: {
506534
project_id: number
507535
body: {

frontend/common/types/responses.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,6 +1293,24 @@ export type WarehouseConnection = {
12931293
unique_events_count: number | null
12941294
}
12951295

1296+
export type TrustRelationshipClaimRule = {
1297+
claim: string
1298+
values: string[]
1299+
}
1300+
1301+
export type TrustRelationship = {
1302+
id: number
1303+
name: string
1304+
issuer: string
1305+
audience: string
1306+
claim_rules: TrustRelationshipClaimRule[]
1307+
is_admin: boolean
1308+
master_api_key_id: string
1309+
master_api_key_prefix: string
1310+
created_at: string
1311+
created_by: number | null
1312+
}
1313+
12961314
export type Res = {
12971315
segments: PagedResponse<Segment>
12981316
segment: Segment
@@ -1383,6 +1401,8 @@ export type Res = {
13831401
launchDarklyProjectImport: LaunchDarklyProjectImport
13841402
launchDarklyProjectsImport: LaunchDarklyProjectImport[]
13851403
roleMasterApiKey: { id: number; master_api_key: string; role: number }
1404+
trustRelationship: TrustRelationship
1405+
trustRelationships: PagedResponse<TrustRelationship>
13861406
masterAPIKeyWithMasterAPIKeyRoles: {
13871407
id: string
13881408
prefix: string

frontend/web/components/AdminAPIKeys.js

Lines changed: 25 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'>
@@ -439,12 +447,19 @@ export default class AdminAPIKeys extends PureComponent {
439447
title={'API Keys'}
440448
json={apiKeys}
441449
/>
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'>
450+
<div className='mt-4'>
451+
<PageTitle
452+
title='API keys'
453+
cta={
454+
<Button
455+
onClick={this.createAPIKey}
456+
disabled={this.state.isLoading}
457+
>
458+
{`Create API Key`}
459+
</Button>
460+
}
461+
>
462+
{`API keys are used to authenticate with the Admin API. `}
448463
<Button
449464
theme='text'
450465
href='https://docs.flagsmith.com/integrations/terraform#terraform-api-key'
@@ -453,11 +468,8 @@ export default class AdminAPIKeys extends PureComponent {
453468
>
454469
{`Learn about API Keys.`}
455470
</Button>
456-
</div>
457-
<Button onClick={this.createAPIKey} disabled={this.state.isLoading}>
458-
{`Create API Key`}
459-
</Button>
460-
</Column>
471+
</PageTitle>
472+
</div>
461473
{(this.state.isLoading || !OrganisationStore.model?.users) && (
462474
<div className='text-center'>
463475
<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)