diff --git a/api/api_keys/models.py b/api/api_keys/models.py index ec6311ce83f4..2db37f58b598 100644 --- a/api/api_keys/models.py +++ b/api/api_keys/models.py @@ -1,4 +1,6 @@ +from django.conf import settings from django.db import models +from django_lifecycle import BEFORE_UPDATE, LifecycleModelMixin, hook from rest_framework_api_key.models import AbstractAPIKey, APIKeyManager from softdelete.models import SoftDeleteManager, SoftDeleteObject @@ -9,7 +11,7 @@ class MasterAPIKeyManager(APIKeyManager, SoftDeleteManager): pass -class MasterAPIKey(AbstractAPIKey, SoftDeleteObject): +class MasterAPIKey(AbstractAPIKey, LifecycleModelMixin, SoftDeleteObject): organisation = models.ForeignKey( Organisation, on_delete=models.CASCADE, @@ -18,3 +20,12 @@ class MasterAPIKey(AbstractAPIKey, SoftDeleteObject): objects = MasterAPIKeyManager() is_admin = models.BooleanField(default=True) + + @hook(BEFORE_UPDATE, when="is_admin", was=False, is_now=True) + def delete_role_api_keys( + self, + ): + if settings.IS_RBAC_INSTALLED: + from rbac.models import MasterAPIKeyRole + + MasterAPIKeyRole.objects.filter(master_api_key=self.id).delete() diff --git a/frontend/common/services/useMasterAPIKeyWithMasterAPIKeyRole.ts b/frontend/common/services/useMasterAPIKeyWithMasterAPIKeyRole.ts new file mode 100644 index 000000000000..0f948f2c1f4b --- /dev/null +++ b/frontend/common/services/useMasterAPIKeyWithMasterAPIKeyRole.ts @@ -0,0 +1,130 @@ +import { Res } from 'common/types/responses' +import { Req } from 'common/types/requests' +import { service } from 'common/service' + +export const masterAPIKeyWithMasterAPIKeyRoleService = service + .enhanceEndpoints({ + addTagTypes: ['MasterAPIKeyWithMasterAPIKeyRole', 'RoleMasterApiKey'], + }) + .injectEndpoints({ + endpoints: (builder) => ({ + deleteMasterAPIKeyWithMasterAPIKeyRoles: builder.mutation< + Res['masterAPIKeyWithMasterAPIKeyRoles'], + Req['deleteMasterAPIKeyWithMasterAPIKeyRoles'] + >({ + invalidatesTags: ['MasterAPIKeyWithMasterAPIKeyRole'], + query: (query: Req['deleteMasterAPIKeyWithMasterAPIKeyRoles']) => ({ + method: 'DELETE', + url: `organisations/${query.org_id}/master-api-keys/${query.prefix}/roles/${query.role_id}/detach-roles-from-master-api-key/`, + }), + }), + getMasterAPIKeyWithMasterAPIKeyRoles: builder.query< + Res['masterAPIKeyWithMasterAPIKeyRoles'], + Req['getMasterAPIKeyWithMasterAPIKeyRoles'] + >({ + providesTags: (res) => [ + { id: res?.id, type: 'MasterAPIKeyWithMasterAPIKeyRole' }, + ], + query: (query: Req['getMasterAPIKeyWithMasterAPIKeyRoles']) => ({ + url: `organisations/${query.org_id}/master-api-keys/${query.prefix}/`, + }), + }), + getRolesMasterAPIKeyWithMasterAPIKeyRoles: builder.query< + Res['masterAPIKeyWithMasterAPIKeyRoles'], + Req['getMasterAPIKeyWithMasterAPIKeyRoles'] + >({ + providesTags: (res) => [ + { id: res?.id, type: 'MasterAPIKeyWithMasterAPIKeyRole' }, + ], + query: (query: Req['getMasterAPIKeyWithMasterAPIKeyRoles']) => ({ + url: `organisations/${query.org_id}/master-api-keys/${query.prefix}/roles/`, + }), + }), + updateMasterAPIKeyWithMasterAPIKeyRoles: builder.mutation< + Res['masterAPIKeyWithMasterAPIKeyRoles'], + Req['updateMasterAPIKeyWithMasterAPIKeyRoles'] + >({ + invalidatesTags: ['MasterAPIKeyWithMasterAPIKeyRole'], + query: (query: Req['updateMasterAPIKeyWithMasterAPIKeyRoles']) => ({ + body: query.body, + method: 'PUT', + url: `organisations/${query.org_id}/master-api-keys/${query.prefix}/`, + }), + }), + // END OF ENDPOINTS + }), + }) + +export async function getMasterAPIKeyWithMasterAPIKeyRoles( + store: any, + data: Req['getMasterAPIKeyWithMasterAPIKeyRoles'], + options?: Parameters< + typeof masterAPIKeyWithMasterAPIKeyRoleService.endpoints.getMasterAPIKeyWithMasterAPIKeyRoles.initiate + >[1], +) { + return store.dispatch( + masterAPIKeyWithMasterAPIKeyRoleService.endpoints.getMasterAPIKeyWithMasterAPIKeyRoles.initiate( + data, + options, + ), + ) +} + +export async function getRolesMasterAPIKeyWithMasterAPIKeyRoles( + store: any, + data: Req['getMasterAPIKeyWithMasterAPIKeyRoles'], + options?: Parameters< + typeof masterAPIKeyWithMasterAPIKeyRoleService.endpoints.getRolesMasterAPIKeyWithMasterAPIKeyRoles.initiate + >[1], +) { + return store.dispatch( + masterAPIKeyWithMasterAPIKeyRoleService.endpoints.getRolesMasterAPIKeyWithMasterAPIKeyRoles.initiate( + data, + options, + ), + ) +} + +export async function deleteMasterAPIKeyWithMasterAPIKeyRoles( + store: any, + data: Req['getMasterAPIKeyWithMasterAPIKeyRoles'], + options?: Parameters< + typeof masterAPIKeyWithMasterAPIKeyRoleService.endpoints.deleteMasterAPIKeyWithMasterAPIKeyRoles.initiate + >[1], +) { + return store.dispatch( + masterAPIKeyWithMasterAPIKeyRoleService.endpoints.deleteMasterAPIKeyWithMasterAPIKeyRoles.initiate( + data, + options, + ), + ) +} +export async function updateMasterAPIKeyWithMasterAPIKeyRoles( + store: any, + data: Req['updateMasterAPIKeyWithMasterAPIKeyRoles'], + options?: Parameters< + typeof masterAPIKeyWithMasterAPIKeyRoleService.endpoints.updateMasterAPIKeyWithMasterAPIKeyRoles.initiate + >[1], +) { + return store.dispatch( + masterAPIKeyWithMasterAPIKeyRoleService.endpoints.updateMasterAPIKeyWithMasterAPIKeyRoles.initiate( + data, + options, + ), + ) +} +// END OF FUNCTION_EXPORTS + +export const { + useDeleteMasterAPIKeyWithMasterAPIKeyRolesMutation, + useGetMasterAPIKeyWithMasterAPIKeyRolesQuery, + useGetRolesMasterAPIKeyWithMasterAPIKeyRolesQuery, + useUpdateMasterAPIKeyWithMasterAPIKeyRolesMutation, + // END OF EXPORTS +} = masterAPIKeyWithMasterAPIKeyRoleService + +/* Usage examples: +const { data, isLoading } = useGetMasterAPIKeyWithMasterAPIKeyRolesQuery({ id: 2 }, {}) //get hook +const [createMasterAPIKeyWithMasterAPIKeyRoles, { isLoading, data, isSuccess }] = useCreateMasterAPIKeyWithMasterAPIKeyRolesMutation() //create hook +masterAPIKeyWithMasterAPIKeyRoleService.endpoints.getMasterAPIKeyWithMasterAPIKeyRoles.select({id: 2})(store.getState()) //access data from any function +*/ diff --git a/frontend/common/services/useRoleMasterApiKey.ts b/frontend/common/services/useRoleMasterApiKey.ts new file mode 100644 index 000000000000..f0b22f083eed --- /dev/null +++ b/frontend/common/services/useRoleMasterApiKey.ts @@ -0,0 +1,135 @@ +import { Res } from 'common/types/responses' +import { Req } from 'common/types/requests' +import { service } from 'common/service' + +export const roleMasterApiKeyService = service + .enhanceEndpoints({ + addTagTypes: ['RoleMasterApiKey', 'MasterAPIKeyWithMasterAPIKeyRole'], + }) + .injectEndpoints({ + endpoints: (builder) => ({ + createRoleMasterApiKey: builder.mutation< + Res['roleMasterApiKey'], + Req['createRoleMasterApiKey'] + >({ + invalidatesTags: [ + { id: 'LIST', type: 'RoleMasterApiKey' }, + 'MasterAPIKeyWithMasterAPIKeyRole', + ], + query: (query: Req['createRoleMasterApiKey']) => ({ + body: query.body, + method: 'POST', + url: `organisations/${query.org_id}/roles/${query.role_id}/master-api-keys/`, + }), + }), + deleteRoleMasterApiKey: builder.mutation< + Res['roleMasterApiKey'], + Req['deleteRoleMasterApiKey'] + >({ + invalidatesTags: [ + { id: 'LIST', type: 'RoleMasterApiKey' }, + 'MasterAPIKeyWithMasterAPIKeyRole', + ], + query: (query: Req['deleteRoleMasterApiKey']) => ({ + method: 'DELETE', + url: `organisations/${query.org_id}/roles/${query.role_id}/master-api-keys/${query.id}/`, + }), + }), + getRoleMasterApiKey: builder.query< + Res['roleMasterApiKey'], + Req['getRoleMasterApiKey'] + >({ + providesTags: (res) => [{ id: res?.id, type: 'RoleMasterApiKey' }], + query: (query: Req['getRoleMasterApiKey']) => ({ + url: `organisations/${query.org_id}/roles/${query.role_id}/master-api-keys/${query.prefix}/`, + }), + }), + updateRoleMasterApiKey: builder.mutation< + Res['roleMasterApiKey'], + Req['updateRoleMasterApiKey'] + >({ + invalidatesTags: (res) => [ + { id: 'LIST', type: 'RoleMasterApiKey' }, + { id: res?.id, type: 'RoleMasterApiKey' }, + ], + query: (query: Req['updateRoleMasterApiKey']) => ({ + body: query, + method: 'PUT', + url: `organisations/${query.org_id}/roles/${query.role_id}/master-api-keys/${id}/`, + }), + }), + // END OF ENDPOINTS + }), + }) + +export async function createRoleMasterApiKey( + store: any, + data: Req['createRoleMasterApiKey'], + options?: Parameters< + typeof roleMasterApiKeyService.endpoints.createRoleMasterApiKey.initiate + >[1], +) { + return store.dispatch( + roleMasterApiKeyService.endpoints.createRoleMasterApiKey.initiate( + data, + options, + ), + ) +} +export async function deleteRoleMasterApiKey( + store: any, + data: Req['deleteRoleMasterApiKey'], + options?: Parameters< + typeof roleMasterApiKeyService.endpoints.deleteRoleMasterApiKey.initiate + >[1], +) { + return store.dispatch( + roleMasterApiKeyService.endpoints.deleteRoleMasterApiKey.initiate( + data, + options, + ), + ) +} +export async function getRoleMasterApiKey( + store: any, + data: Req['getRoleMasterApiKey'], + options?: Parameters< + typeof roleMasterApiKeyService.endpoints.getRoleMasterApiKey.initiate + >[1], +) { + return store.dispatch( + roleMasterApiKeyService.endpoints.getRoleMasterApiKey.initiate( + data, + options, + ), + ) +} +export async function updateRoleMasterApiKey( + store: any, + data: Req['updateRoleMasterApiKey'], + options?: Parameters< + typeof roleMasterApiKeyService.endpoints.updateRoleMasterApiKey.initiate + >[1], +) { + return store.dispatch( + roleMasterApiKeyService.endpoints.updateRoleMasterApiKey.initiate( + data, + options, + ), + ) +} +// END OF FUNCTION_EXPORTS + +export const { + useCreateRoleMasterApiKeyMutation, + useDeleteRoleMasterApiKeyMutation, + useGetRoleMasterApiKeyQuery, + useUpdateRoleMasterApiKeyMutation, + // END OF EXPORTS +} = roleMasterApiKeyService + +/* Usage examples: +const { data, isLoading } = useGetRoleMasterApiKeyQuery({ id: 2 }, {}) //get hook +const [createRoleMasterApiKey, { isLoading, data, isSuccess }] = useCreateRoleMasterApiKeyMutation() //create hook +roleMasterApiKeyService.endpoints.getRoleMasterApiKey.select({id: 2})(store.getState()) //access data from any function +*/ diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index 85bbaa43f168..4e8bc57758bf 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -148,6 +148,17 @@ export type Req = { getGetSubscriptionMetadata: { id: string } getEnvironment: { id: string } getSubscriptionMetadata: { id: string } + getRoleMasterApiKey: { org_id: number; role_id: number; id: string } + updateRoleMasterApiKey: { org_id: number; role_id: number; id: string } + deleteRoleMasterApiKey: { org_id: number; role_id: number; id: string } + createRoleMasterApiKey: { org_id: number; role_id: number } + getMasterAPIKeyWithMasterAPIKeyRoles: { org_id: number; prefix: string } + deleteMasterAPIKeyWithMasterAPIKeyRoles: { + org_id: number + prefix: string + role_id: number + } + getRolesMasterAPIKeyWithMasterAPIKeyRoles: { org_id: number; prefix: string } createLaunchDarklyProjectImport: { project_id: string body: { diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index c73fb4a979d1..512b3333fca1 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -380,6 +380,7 @@ export type RolePermissionUser = { user: number role: number id: number + role_name: string } export type FeatureVersion = { created_at: string @@ -464,6 +465,12 @@ export type Res = { environment: Environment launchDarklyProjectImport: LaunchDarklyProjectImport launchDarklyProjectsImport: LaunchDarklyProjectImport[] + roleMasterApiKey: { id: number; master_api_key: string; role: number } + masterAPIKeyWithMasterAPIKeyRoles: { + id: string + prefix: string + roles: RolePermissionUser[] + } userWithRoles: PagedResponse groupWithRole: PagedResponse changeRequests: PagedResponse diff --git a/frontend/web/components/AdminAPIKeys.js b/frontend/web/components/AdminAPIKeys.js index 2cc52ac63971..4116c0defe2b 100644 --- a/frontend/web/components/AdminAPIKeys.js +++ b/frontend/web/components/AdminAPIKeys.js @@ -1,4 +1,6 @@ import React, { PureComponent } from 'react' +import { close as closeIcon } from 'ionicons/icons' +import { IonIcon } from '@ionic/react' import data from 'common/data/base/_data' import InfoMessage from './InfoMessage' import Token from './Token' @@ -6,12 +8,31 @@ import JSONReference from './JSONReference' import Button from './base/forms/Button' import DateSelect from './DateSelect' import Icon from './Icon' +import Switch from './Switch' +import MyRoleSelect from './MyRoleSelect' +import { getStore } from 'common/store' +import { createRoleMasterApiKey } from 'common/services/useRoleMasterApiKey' +import { + deleteMasterAPIKeyWithMasterAPIKeyRoles, + getMasterAPIKeyWithMasterAPIKeyRoles, + getRolesMasterAPIKeyWithMasterAPIKeyRoles, + updateMasterAPIKeyWithMasterAPIKeyRoles, +} from 'common/services/useMasterAPIKeyWithMasterAPIKeyRole' export class CreateAPIKey extends PureComponent { state = { expiry_date: null, + is_admin: true, key: '', name: '', + roles: [], + showRoles: false, + } + + componentDidMount() { + Utils.getFlagsmithHasFeature('show_role_management') && + this.props.isEdit && + this.getApiKeyByPrefix(this.props.prefix) } submit = () => { @@ -21,6 +42,7 @@ export class CreateAPIKey extends PureComponent { `${Project.api}organisations/${this.props.organisationId}/master-api-keys/`, { expiry_date: this.state.expiry_date, + is_admin: this.state.is_admin, name: this.state.name, organisation: this.props.organisationId, }, @@ -30,11 +52,111 @@ export class CreateAPIKey extends PureComponent { isSaving: false, key: res.key, }) + Utils.getFlagsmithHasFeature('show_role_management') && + Promise.all( + this.state.roles.map((role) => + createRoleMasterApiKey(getStore(), { + body: { master_api_key: res.id }, + org_id: AccountStore.getOrganisation().id, + role_id: role.id, + }).then(() => { + toast('Role API Key was Created') + }), + ), + ) + this.props.onSuccess() }) } + updateApiKey = (prefix) => { + updateMasterAPIKeyWithMasterAPIKeyRoles(getStore(), { + body: { + expiry_date: this.state.expiry_date, + is_admin: this.state.is_admin, + name: this.state.name, + revoked: false, + }, + org_id: AccountStore.getOrganisation().id, + prefix: prefix, + }).then(() => { + this.props.onSuccess() + }) + } + + getApiKeyByPrefix = (prefix) => { + getMasterAPIKeyWithMasterAPIKeyRoles(getStore(), { + org_id: AccountStore.getOrganisation().id, + prefix: prefix, + }).then((res) => { + getRolesMasterAPIKeyWithMasterAPIKeyRoles(getStore(), { + org_id: AccountStore.getOrganisation().id, + prefix: prefix, + }).then((rolesData) => { + this.setState({ + expiry_date: res.data.expiry_date, + is_admin: res.data.is_admin, + name: res.data.name, + roles: rolesData.data.results, + }) + }) + }) + } + + removeRoleApiKey = (roleId, isEdit) => { + const roleSelected = this.state.roles.find((item) => item.id === roleId) + if (isEdit) { + deleteMasterAPIKeyWithMasterAPIKeyRoles(getStore(), { + org_id: AccountStore.getOrganisation().id, + prefix: this.props.prefix, + role_id: roleSelected.id, + }).then(() => { + toast('Role API Key was removed') + }) + } + this.setState({ + roles: (this.state.roles || []).filter((v) => v.id !== roleId), + }) + } + + addRole = (role, isEdit) => { + if (isEdit) { + createRoleMasterApiKey(getStore(), { + body: { master_api_key: this.props.masterAPIKey }, + org_id: AccountStore.getOrganisation().id, + role_id: role.id, + }).then((res) => { + toast('Role API Key was added') + this.setState({ + roles: [ + ...(this.state.roles || []), + { + id: role.id, + name: role.name, + }, + ], + }) + }) + } else { + this.setState({ + roles: [ + ...(this.state.roles || []), + { + id: role.id, + name: role.name, + }, + ], + }) + } + } + render() { + const { expiry_date, is_admin, roles, showRoles } = this.state + const buttonText = this.props.isEdit ? 'Update' : 'Create' + const buttonSavingText = this.props.isEdit ? 'Updating' : 'Creating' + const showRoleManagementEnabled = Utils.getFlagsmithHasFeature( + 'show_role_management', + ) return ( <>
@@ -55,6 +177,76 @@ export class CreateAPIKey extends PureComponent { placeholder='e.g. Admin API Key' /> + {showRoleManagementEnabled && ( + <> + + + { + this.setState({ + is_admin: !is_admin, + }) + }} + checked={is_admin} + /> + + {!is_admin && ( + <> + + + {roles?.map((r) => ( + + this.removeRoleApiKey(r.id, this.props.isEdit) + } + className='chip' + > + {r.name} + + + + + ))} + + + + {Utils.getFlagsmithHasFeature( + 'show_role_management', + ) && ( +
+ v.id)} + onAdd={(role) => + this.addRole(role, this.props.isEdit) + } + onRemove={(roleId) => + this.removeRoleApiKey(roleId, this.props.isEdit) + } + isOpen={showRoles} + onToggle={() => + this.setState({ showRoles: !showRoles }) + } + /> +
+ )} +
+ + )} + + )}
@@ -65,16 +257,10 @@ export class CreateAPIKey extends PureComponent { expiry_date: e.toISOString(), }) }} - selected={ - this.state.expiry_date - ? moment(this.state.expiry_date)._d - : null - } + selected={expiry_date ? moment(expiry_date)._d : null} value={ - this.state.expiry_date - ? `${moment(this.state.expiry_date).format( - 'Do MMM YYYY hh:mma', - )}` + expiry_date + ? `${moment(expiry_date).format('Do MMM YYYY hh:mma')}` : 'Never' } /> @@ -98,7 +284,7 @@ export class CreateAPIKey extends PureComponent { <>
@@ -154,6 +348,24 @@ export default class AdminAPIKeys extends PureComponent { ) } + editAPIKey = (name, masterAPIKey, prefix) => { + openModal( + `${name} API Key`, + { + this.setState({ isLoading: true }) + this.fetch() + closeModal() + toast('API key Updated') + }} + />, + 'p-0 side-modal', + ) + } + fetch = () => { this.setState({ isLoading: true }) data @@ -189,18 +401,25 @@ export default class AdminAPIKeys extends PureComponent { render() { const apiKeys = this.state.apiKeys && this.state.apiKeys.results + const showRoleManagementEnabled = Utils.getFlagsmithHasFeature( + 'show_role_management', + ) return (
-
Terraform API Keys
+
{`${ + showRoleManagementEnabled ? 'Manage' : 'Terraform' + } API Keys`}

- Terraform API keys are used to authenticate with the Admin API. + {`${ + showRoleManagementEnabled ? '' : 'Terraform' + } API keys are used to authenticate with the Admin API.`}

{this.state.isLoading && ( @@ -227,8 +448,9 @@ export default class AdminAPIKeys extends PureComponent { items={apiKeys} header={ - Terraform API Keys + API Keys Created + Is Admin
!v.revoked && ( - + + showRoleManagementEnabled && + this.editAPIKey(v.name, v.id, v.prefix) + } + >
{v.name}
@@ -249,12 +478,18 @@ export default class AdminAPIKeys extends PureComponent { {moment(v.created).format('Do MMM YYYY HH:mma')} + + +