From 755aef1e244724d1322801bb7d46c6bb6e2758a2 Mon Sep 17 00:00:00 2001 From: Richard87 Date: Wed, 25 Sep 2024 10:15:44 +0200 Subject: [PATCH 1/7] Cleanup types --- proxy/server.dev.conf | 4 +- src/components/app-config-ad-groups/index.tsx | 21 +- .../create-application-form/index.tsx | 7 +- src/components/graph/adGroups.tsx | 239 +++++++++++------- src/components/msal-auth-context/config.ts | 2 +- .../page-configuration/change-admin-form.tsx | 37 ++- src/effects/use-debounce.ts | 21 ++ src/store/ms-graph-api.ts | 139 ++++++++-- src/store/radix-api.ts | 8 + 9 files changed, 343 insertions(+), 135 deletions(-) create mode 100644 src/effects/use-debounce.ts diff --git a/proxy/server.dev.conf b/proxy/server.dev.conf index a4de91262..4959a0ecc 100644 --- a/proxy/server.dev.conf +++ b/proxy/server.dev.conf @@ -9,8 +9,8 @@ server { proxy_read_timeout 10; location /api/ { - proxy_pass https://server-radix-api-qa.dev.radix.equinor.com; - # proxy_pass http://172.19.0.1:3002; + # proxy_pass https://server-radix-api-qa.dev.radix.equinor.com; + proxy_pass http://172.19.0.1:3002; proxy_set_header Authorization "Bearer $http_x_forwarded_access_token"; proxy_set_header x-forwarded-access-token ""; } diff --git a/src/components/app-config-ad-groups/index.tsx b/src/components/app-config-ad-groups/index.tsx index a09982358..55a3ad955 100644 --- a/src/components/app-config-ad-groups/index.tsx +++ b/src/components/app-config-ad-groups/index.tsx @@ -1,25 +1,25 @@ import { AuthenticatedTemplate } from '@azure/msal-react'; import { Typography } from '@equinor/eds-core-react'; import * as PropTypes from 'prop-types'; -import type { FunctionComponent } from 'react'; import { ADGroups, type HandleAdGroupsChangeCB } from '../graph/adGroups'; import './style.css'; -export interface AppConfigAdGroupsProps { +interface Props { labeling: string; adGroups?: Array; + adUsers?: Array; isDisabled?: boolean; - handleAdGroupsChange: HandleAdGroupsChangeCB; + onChange: HandleAdGroupsChangeCB; } - -export const AppConfigAdGroups: FunctionComponent = ({ +export const AppConfigAdGroups = ({ labeling, adGroups, + adUsers, isDisabled, - handleAdGroupsChange, -}) => ( + onChange, +}: Props) => (
{labeling} @@ -28,8 +28,9 @@ export const AppConfigAdGroups: FunctionComponent = ({ @@ -40,5 +41,5 @@ AppConfigAdGroups.propTypes = { labeling: PropTypes.string.isRequired, adGroups: PropTypes.arrayOf(PropTypes.string), isDisabled: PropTypes.bool, - handleAdGroupsChange: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, }; diff --git a/src/components/create-application-form/index.tsx b/src/components/create-application-form/index.tsx index 68136cdb3..e5381cb64 100644 --- a/src/components/create-application-form/index.tsx +++ b/src/components/create-application-form/index.tsx @@ -56,6 +56,8 @@ export default function CreateApplicationForm({ onCreated }: Props) { radixConfigFullName: 'radixconfig.yaml', configurationItem: '', readerAdGroups: [], + adUsers: [], + readerAdUsers: [], }); const [refreshApps] = radixApi.endpoints.showApplications.useLazyQuery({}); @@ -68,7 +70,8 @@ export default function CreateApplicationForm({ onCreated }: Props) { const handleAdGroupsChange: HandleAdGroupsChangeCB = (value) => { setAppRegistration((current) => ({ ...current, - adGroups: value.map((x) => x.id), + adGroups: value.filter((x) => x.type === 'Group').map((x) => x.id), + adUsers: value.filter((x) => x.type !== 'Group').map((x) => x.id), })); }; @@ -205,7 +208,7 @@ export default function CreateApplicationForm({ onCreated }: Props) { /> {creationState.isError && ( diff --git a/src/components/graph/adGroups.tsx b/src/components/graph/adGroups.tsx index 33d615118..b81f992cd 100644 --- a/src/components/graph/adGroups.tsx +++ b/src/components/graph/adGroups.tsx @@ -1,130 +1,183 @@ -import { Typography } from '@equinor/eds-core-react'; -import { debounce } from 'lodash'; +import { Icon, Typography } from '@equinor/eds-core-react'; +import { computer, group, person } from '@equinor/eds-icons'; import * as PropTypes from 'prop-types'; -import type { - ActionMeta, - CSSObjectWithLabel, - OnChangeValue, +import { + type CSSObjectWithLabel, + type GroupBase, + type MultiValue, + type MultiValueGenericProps, + type OptionsOrGroups, + components, } from 'react-select'; import AsyncSelect from 'react-select/async'; - +import { useDebounce } from '../../effects/use-debounce'; import { - type AdGroup, - msGraphApi, + type EntraItem, useGetAdGroupsQuery, + useGetAdServicePrincipalQuery, + useLazySearchAdGroupsQuery, + useLazySearchAdServicePrincipalsQuery, } from '../../store/ms-graph-api'; import AsyncResource from '../async-resource/async-resource'; import { UnknownADGroupsAlert } from '../component/unknown-ad-groups-alert'; -type DisplayAdGroups = AdGroup & { deleted?: boolean }; - -type SearchGroupFunctionType = ReturnType< - typeof msGraphApi.endpoints.searchAdGroups.useLazyQuery ->[0]; - -const loadOptions = debounce( - ( - callback: (options: Array) => void, - searchGroup: SearchGroupFunctionType, - value: string - ) => filterOptions(searchGroup, value).then(callback), - 500 -); +export type AdGroupItem = EntraItem & { + deleted?: boolean; + type: 'Group' | 'User' | 'ServicePrincipal' | 'Application'; +}; -async function filterOptions( - searchGroups: SearchGroupFunctionType, - groupName: string -): Promise> { - return (await searchGroups({ groupName, limit: 10 }).unwrap()).value; +export interface GroupedOption { + readonly label: string; + readonly options: readonly AdGroupItem[]; } -function selectValueStyle( - base: CSSObjectWithLabel, - props: { data: DisplayAdGroups } -): CSSObjectWithLabel { - if (props.data.deleted) { - base.backgroundColor = 'var(--eds_interactive_danger__highlight)'; - } - return base; -} - -export type HandleAdGroupsChangeCB = ( - value: OnChangeValue, - actionMeta: ActionMeta +type CallbackType = ( + options: OptionsOrGroups> ) => void; +export type HandleAdGroupsChangeCB = (value: MultiValue) => void; + interface Props { - handleAdGroupsChange: HandleAdGroupsChangeCB; - adGroups?: Array; + onChange: HandleAdGroupsChangeCB; + adGroups: Array; + adUsers: Array; isDisabled?: boolean; } -export function ADGroups({ - handleAdGroupsChange, - adGroups, - isDisabled, -}: Props) { +export function ADGroups({ onChange, adGroups, adUsers, isDisabled }: Props) { const { data: groupsInfo, ...state } = useGetAdGroupsQuery({ ids: adGroups ?? [], }); - const [searchGroups] = msGraphApi.endpoints.searchAdGroups.useLazyQuery(); - const displayGroups = adGroups - ?.map((id) => ({ id, info: groupsInfo?.find((g) => g.id === id) })) - .map((g) => ({ + const { data: spInfo, ...spState } = useGetAdServicePrincipalQuery({ + ids: adUsers ?? [], + }); + const [searchServicePrincipals, spSearchState] = + useLazySearchAdServicePrincipalsQuery(); + const [searchGroups, groupsSearchState] = useLazySearchAdGroupsQuery(); + + const displayGroups: AdGroupItem[] = adGroups + .map((id) => ({ id, info: groupsInfo?.find((g) => g.id === id) })) + .map((g) => ({ id: g.id, displayName: g.info?.displayName ?? g.id, + type: 'Group', deleted: !g.info, })); + const displayUsers: AdGroupItem[] = adUsers + .map((id) => ({ id, info: spInfo?.find((sp) => sp.id === id) })) + .map((sp) => ({ + id: sp.id, + displayName: sp.info?.displayName ?? sp.id, + type: 'ServicePrincipal', + deleted: !sp.info, + })); const unknownADGroups = adGroups?.filter( (adGroupId) => !groupsInfo?.some((adGroup) => adGroup.id === adGroupId) ); + const unknownADUsers = adUsers?.filter( + (adUserId) => !spInfo?.some((adUser) => adUser.id === adUserId) + ); + + const onSearch = useDebounce( + (displayName: string, callback: CallbackType) => { + Promise.all([ + searchGroups({ displayName, limit: 10 }).unwrap(), + searchServicePrincipals({ displayName, limit: 10 }).unwrap(), + ]).then(([groups, servicePrincipals]) => { + callback([ + { + label: 'Groups', + options: groups.value.map((item) => ({ + ...item, + type: 'Group', + })), + }, + { + label: 'Service Principals', + options: servicePrincipals.value.map((item) => ({ + ...item, + type: 'ServicePrincipal', + })), + }, + ]); + }); + }, + 500 + ); return ( - - { - const target = e.target as HTMLInputElement; - return ( - target?.parentElement?.className && - !target.parentElement.className.match(/menu/) - ); - }} - noOptionsMessage={() => null} - loadOptions={(inputValue, callback) => { - inputValue?.length < 3 - ? callback([]) - : loadOptions(callback, searchGroups, inputValue); - }} - onChange={handleAdGroupsChange} - getOptionLabel={({ displayName }) => displayName} - getOptionValue={({ id }) => id} - closeMenuOnSelect={false} - defaultValue={displayGroups} - isDisabled={isDisabled} - styles={{ - multiValueLabel: selectValueStyle, - multiValueRemove: selectValueStyle, - }} - /> - - Azure Active Directory groups (type 3 characters to search) - - {!state.isFetching && unknownADGroups?.length > 0 && ( - - )} + + + + isMulti + name="ADGroups" + menuPosition="fixed" + closeMenuOnScroll={(e: Event) => { + const target = e.target as HTMLInputElement; + return ( + target?.parentElement?.className && + !target.parentElement.className.match(/menu/) + ); + }} + noOptionsMessage={() => null} + loadOptions={(inputValue, callback) => { + if (inputValue.length < 3) return callback([]); + return onSearch(inputValue, callback); + }} + isLoading={groupsSearchState.isLoading || spSearchState.isLoading} + onChange={onChange} + getOptionLabel={({ displayName }) => displayName} + getOptionValue={({ id }) => id} + closeMenuOnSelect={false} + defaultValue={[...displayGroups, ...displayUsers]} + isDisabled={isDisabled} + components={{ MultiValueLabel }} + styles={{ + multiValueLabel: selectValueStyle, + multiValueRemove: selectValueStyle, + }} + /> + + Azure Active Directory (type 3 characters to search) + + {(!state.isFetching && unknownADGroups?.length > 0) || + (!spState.isFetching && unknownADUsers?.length > 0 && ( + + ))} + ); } ADGroups.propTypes = { - handleAdGroupsChange: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, adGroups: PropTypes.arrayOf(PropTypes.string), isDisabled: PropTypes.bool, }; + +function selectValueStyle( + base: CSSObjectWithLabel, + props: { data: AdGroupItem } +) { + if (props.data.deleted) { + base.backgroundColor = 'var(--eds_interactive_danger__highlight)'; + } + return base; +} + +function MultiValueLabel(props: MultiValueGenericProps) { + let icon = computer; + if (props.data.type === 'Group') icon = group; + if (props.data.type === 'User') icon = person; + + return ( +
+ + +
+ ); +} diff --git a/src/components/msal-auth-context/config.ts b/src/components/msal-auth-context/config.ts index 9e128b133..436e250e7 100644 --- a/src/components/msal-auth-context/config.ts +++ b/src/components/msal-auth-context/config.ts @@ -2,7 +2,7 @@ import type { Configuration } from '@azure/msal-browser'; import { configVariables } from '../../utils/config'; export const msGraphConfig = { - scopes: ['User.Read', 'GroupMember.Read.All'], + scopes: ['User.Read', 'GroupMember.Read.All', 'Application.Read.All'], }; export const serviceNowApiConfig = { diff --git a/src/components/page-configuration/change-admin-form.tsx b/src/components/page-configuration/change-admin-form.tsx index 9b3988db6..de8129c18 100644 --- a/src/components/page-configuration/change-admin-form.tsx +++ b/src/components/page-configuration/change-admin-form.tsx @@ -14,17 +14,20 @@ import { } from '../../store/radix-api'; import { AppConfigAdGroups } from '../app-config-ad-groups'; import { handlePromiseWithToast } from '../global-top-nav/styled-toaster'; +import type { AdGroupItem } from '../graph/adGroups'; const isEqual = (a: Array, b: Array) => a.length == b.length && difference(a, b).length === 0; interface Props { registration: ApplicationRegistration; - refetch?: () => unknown; + refetch: () => unknown; } export default function ChangeAdminForm({ registration, refetch }: Props) { const [adminAdGroup, setAdminAdGroup] = useState>(); + const [adminAdUser, setAdminAdUser] = useState>(); const [readerAdGroup, setReaderAdGroup] = useState>(); + const [readerAdUser, setReaderAdUser] = useState>(); const [mutate, { isLoading }] = useModifyRegistrationDetailsMutation(); const handleSubmit = handlePromiseWithToast( @@ -37,25 +40,39 @@ export default function ChangeAdminForm({ registration, refetch }: Props) { acknowledgeWarnings: true, applicationRegistrationPatch: { adGroups: adminAdGroup || registration.adGroups, + adUsers: adminAdUser || registration.adUsers, readerAdGroups: readerAdGroup || registration.readerAdGroups, + readerAdUsers: readerAdUser || registration.readerAdUsers, }, }, }).unwrap(); - await refetch?.(); + await refetch(); setAdminAdGroup(undefined); setReaderAdGroup(undefined); } ); const adminUnchanged = - adminAdGroup == null || isEqual(adminAdGroup, registration.adGroups ?? []); + (adminAdGroup == null || + isEqual(adminAdGroup, registration.adGroups ?? [])) && + (adminAdUser == null || isEqual(adminAdUser, registration.adUsers ?? [])); const readerUnchanged = - readerAdGroup == null || - isEqual(readerAdGroup, registration.readerAdGroups ?? []); + (readerAdGroup == null || + isEqual(readerAdGroup, registration.readerAdGroups ?? [])) && + (readerAdUser == null || + isEqual(readerAdUser, registration.readerAdUsers ?? [])); const isUnchanged = adminUnchanged && readerUnchanged; + const handleReaderAdGroupsChange = (value: AdGroupItem[]) => { + setReaderAdGroup(value.filter((x) => x.type === 'Group').map((v) => v.id)); + setReaderAdUser(value.filter((x) => x.type !== 'Group').map((x) => x.id)); + }; + const handleAdGroupsChange = (value: AdGroupItem[]) => { + setAdminAdGroup(value.filter((x) => x.type === 'Group').map((v) => v.id)); + setAdminAdUser(value.filter((x) => x.type !== 'Group').map((x) => x.id)); + }; return ( @@ -70,16 +87,14 @@ export default function ChangeAdminForm({ registration, refetch }: Props) { - setAdminAdGroup(value.map((v) => v.id)) - } + adUsers={registration.adUsers} + onChange={handleAdGroupsChange} /> - setReaderAdGroup(value.map((v) => v.id)) - } + adUsers={registration.readerAdUsers} + onChange={handleReaderAdGroupsChange} />
{isLoading ? ( diff --git a/src/effects/use-debounce.ts b/src/effects/use-debounce.ts new file mode 100644 index 000000000..75d1eca08 --- /dev/null +++ b/src/effects/use-debounce.ts @@ -0,0 +1,21 @@ +import { debounce } from 'lodash'; +import { useEffect, useMemo, useRef } from 'react'; + +// Picked up from https://www.developerway.com/posts/debouncing-in-react +export function useDebounce, TReturn>( + fn: (...args: TArgs) => TReturn, + delay: number +): (...args: TArgs) => TReturn { + const ref = useRef<(...args: TArgs) => TReturn>(); + + useEffect(() => { + ref.current = fn; + }, [fn]); + + const debouncedCallback = useMemo(() => { + const func = (...args: TArgs): TReturn => ref.current?.(...args); + return debounce(func, delay); + }, [delay]); + + return debouncedCallback; +} diff --git a/src/store/ms-graph-api.ts b/src/store/ms-graph-api.ts index e7c7ab640..718855c8c 100644 --- a/src/store/ms-graph-api.ts +++ b/src/store/ms-graph-api.ts @@ -27,11 +27,11 @@ function parseGraphError(e): FetchBaseQueryError { const injectedRtkApi = api.injectEndpoints({ endpoints: (build) => ({ - getAdGroup: build.query({ + getAdGroup: build.query({ queryFn: async ({ id }, { getState }) => { try { ensureClient(getState() as RootState); - const group: AdGroup = await graphClient + const group: EntraItem = await graphClient .api(`/groups/${id}`) .select('displayName,id') .get(); @@ -42,7 +42,7 @@ const injectedRtkApi = api.injectEndpoints({ } }, }), - getAdGroups: build.query({ + getAdGroups: build.query({ queryFn: async ({ ids }, { getState }) => { try { if (ids.length > 15) { @@ -61,7 +61,7 @@ const injectedRtkApi = api.injectEndpoints({ ensureClient(getState() as RootState); const idFilter = ids.map((s) => `'${s}'`).join(','); - const response: SearchAdGroupsResponse = await graphClient + const response: SearchResponse = await graphClient .api(`/groups`) .select('displayName,id') .filter(`id in (${idFilter})`) @@ -74,15 +74,116 @@ const injectedRtkApi = api.injectEndpoints({ }, }), - searchAdGroups: build.query({ - queryFn: async ({ groupName, limit }, { getState }) => { + getAdServicePrincipal: build.query({ + queryFn: async ({ ids }, { getState }) => { try { + if (ids.length > 15) { + // ref. https://learn.microsoft.com/en-us/graph/filter-query-parameter?tabs=javascript#operators-and-functions-supported-in-filter-expressions + return { + error: { + error: 'We can fetch maximum 15 items at a time', + status: 'CUSTOM_ERROR', + }, + }; + } + + if (ids.length === 0) { + return { data: [] }; + } + ensureClient(getState() as RootState); + const idFilter = ids.map((s) => `'${s}'`).join(','); + const response: SearchResponse = await graphClient + .api(`/servicePrincipals`) + .select('displayName,id') + .filter(`id in (${idFilter})`) + .get(); + + return { data: response.value }; + } catch (e) { + return { error: parseGraphError(e) }; + } + }, + }), + getAdApplication: build.query({ + queryFn: async ({ ids }, { getState }) => { + try { + if (ids.length > 15) { + // ref. https://learn.microsoft.com/en-us/graph/filter-query-parameter?tabs=javascript#operators-and-functions-supported-in-filter-expressions + return { + error: { + error: 'We can fetch maximum 15 items at a time', + status: 'CUSTOM_ERROR', + }, + }; + } - const groups: SearchAdGroupsResponse = await graphClient + if (ids.length === 0) { + return { data: [] }; + } + + ensureClient(getState() as RootState); + const idFilter = ids.map((s) => `'${s}'`).join(','); + const response: SearchResponse = await graphClient + .api(`/applications`) + .select('displayName,id') + .filter(`id in (${idFilter})`) + .get(); + + return { data: response.value }; + } catch (e) { + return { error: parseGraphError(e) }; + } + }, + }), + + searchAdGroups: build.query({ + queryFn: async ({ displayName, limit }, { getState }) => { + try { + ensureClient(getState() as RootState); + + const groups: SearchResponse = await graphClient .api('/groups') .select('displayName,id') - .filter(groupName ? `startswith(displayName,'${groupName}')` : '') + .filter(displayName ? `startswith(displayName,'${displayName}')` : '') + .top(limit) + .get(); + + return { data: groups }; + } catch (e) { + return { error: parseGraphError(e) }; + } + }, + }), + + searchAdServicePrincipals: build.query({ + queryFn: async ({ displayName, limit }, { getState }) => { + try { + ensureClient(getState() as RootState); + + const groups: SearchResponse = await graphClient + .api('/servicePrincipals') + .select('displayName,id') + .filter(displayName ? `startswith(displayName,'${displayName}')` : '') + .top(limit) + .get(); + + return { data: groups }; + } catch (e) { + return { error: parseGraphError(e) }; + } + }, + }), + + searchAdApplications: build.query({ + queryFn: async ({ displayName, limit }, { getState }) => { + try { + ensureClient(getState() as RootState); + + const groups: SearchResponse = await graphClient + .api('/applications') + .select('displayName,id') + .filter(displayName ? `startswith(displayName,'${displayName}')` : '') .top(limit) .get(); @@ -99,28 +200,34 @@ export { injectedRtkApi as msGraphApi }; export const { useGetAdGroupQuery, useGetAdGroupsQuery, + useGetAdApplicationQuery, + useGetAdServicePrincipalQuery, useSearchAdGroupsQuery, + useSearchAdServicePrincipalsQuery, + useSearchAdApplicationsQuery, + useLazySearchAdGroupsQuery, + useLazySearchAdServicePrincipalsQuery, } = injectedRtkApi; type GetAdGroupArg = { id: string; }; -type GetAdGroupResponse = AdGroup; -export type AdGroup = { + +export type EntraItem = { displayName: string; id: string; }; -type GetAdGroupsArg = { +type GetEntraArg = { ids: string[]; }; -type GetAdGroupsResponse = AdGroup[]; +type GetEntraResponse = EntraItem[]; -type SearchAdGroupsArgs = { - groupName: string; +type SearchEntraArgs = { + displayName: string; limit: number; }; -export type SearchAdGroupsResponse = { +export type SearchResponse = { '@odata.context'?: string; '@odata.nextLink'?: string; - value: AdGroup[]; + value: Array; }; diff --git a/src/store/radix-api.ts b/src/store/radix-api.ts index dfd8d3f26..5edbadb67 100644 --- a/src/store/radix-api.ts +++ b/src/store/radix-api.ts @@ -2520,6 +2520,8 @@ export type ApplicationSummary = { export type ApplicationRegistration = { /** AdGroups the groups that should be able to access the application */ adGroups: string[]; + /** AdUsers the users/service-principals that should be able to access the application */ + adUsers: string[]; /** ConfigBranch information */ configBranch: string; /** ConfigurationItem is an identifier for an entity in a configuration management solution such as a CMDB. @@ -2536,6 +2538,8 @@ export type ApplicationRegistration = { radixConfigFullName?: string; /** ReaderAdGroups the groups that should be able to read the application */ readerAdGroups?: string[]; + /** ReaderAdUsers the users/service-principals that should be able to read the application */ + readerAdUsers?: string[]; /** Repository the github repository */ repository: string; /** SharedSecret the shared secret of the webhook */ @@ -2664,6 +2668,8 @@ export type Application = { export type ApplicationRegistrationPatch = { /** AdGroups the groups that should be able to access the application */ adGroups?: string[]; + /** AdUsers the users/service-principals that should be able to access the application */ + adUsers?: string[]; /** ConfigBranch information */ configBranch?: string; /** ConfigurationItem is an identifier for an entity in a configuration management solution such as a CMDB. @@ -2676,6 +2682,8 @@ export type ApplicationRegistrationPatch = { radixConfigFullName?: string; /** ReaderAdGroups the groups that should be able to read the application */ readerAdGroups?: string[]; + /** ReaderAdUsers the users/service-principals that should be able to read the application */ + readerAdUsers?: string[]; /** Repository the github repository */ repository?: string; /** WBS information */ From a6ecf428729c7a44d2bca9d47d31c51f32accd8d Mon Sep 17 00:00:00 2001 From: Richard87 Date: Wed, 25 Sep 2024 10:18:59 +0200 Subject: [PATCH 2/7] Cleanup types --- src/components/graph/adGroups.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/graph/adGroups.tsx b/src/components/graph/adGroups.tsx index b81f992cd..2ef932a03 100644 --- a/src/components/graph/adGroups.tsx +++ b/src/components/graph/adGroups.tsx @@ -21,20 +21,20 @@ import { import AsyncResource from '../async-resource/async-resource'; import { UnknownADGroupsAlert } from '../component/unknown-ad-groups-alert'; +export type HandleAdGroupsChangeCB = (value: MultiValue) => void; export type AdGroupItem = EntraItem & { deleted?: boolean; type: 'Group' | 'User' | 'ServicePrincipal' | 'Application'; }; -export interface GroupedOption { +type GroupedOption = { readonly label: string; readonly options: readonly AdGroupItem[]; -} +}; type CallbackType = ( options: OptionsOrGroups> ) => void; -export type HandleAdGroupsChangeCB = (value: MultiValue) => void; interface Props { onChange: HandleAdGroupsChangeCB; From 6f3cd6de48e7f9cabf20404cc11f78a7af77e199 Mon Sep 17 00:00:00 2001 From: Richard87 Date: Wed, 25 Sep 2024 10:24:36 +0200 Subject: [PATCH 3/7] Cleanup types --- src/store/radix-api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/store/radix-api.ts b/src/store/radix-api.ts index 5edbadb67..e487f32b5 100644 --- a/src/store/radix-api.ts +++ b/src/store/radix-api.ts @@ -2537,9 +2537,9 @@ export type ApplicationRegistration = { /** radixconfig.yaml file name and path, starting from the GitHub repository root (without leading slash) */ radixConfigFullName?: string; /** ReaderAdGroups the groups that should be able to read the application */ - readerAdGroups?: string[]; + readerAdGroups: string[]; /** ReaderAdUsers the users/service-principals that should be able to read the application */ - readerAdUsers?: string[]; + readerAdUsers: string[]; /** Repository the github repository */ repository: string; /** SharedSecret the shared secret of the webhook */ From 926470ea7dd0c0fb3893403169bc7c9fdeb8996d Mon Sep 17 00:00:00 2001 From: Richard87 Date: Wed, 25 Sep 2024 10:30:01 +0200 Subject: [PATCH 4/7] fix test --- src/components/configure-application-github/dev.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/configure-application-github/dev.tsx b/src/components/configure-application-github/dev.tsx index a2d3d8c13..60e744fe7 100644 --- a/src/components/configure-application-github/dev.tsx +++ b/src/components/configure-application-github/dev.tsx @@ -5,6 +5,9 @@ export default ( Date: Wed, 25 Sep 2024 12:53:49 +0200 Subject: [PATCH 5/7] cleanup proxy --- proxy/server.dev.conf | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/proxy/server.dev.conf b/proxy/server.dev.conf index 4959a0ecc..4e4507fdd 100644 --- a/proxy/server.dev.conf +++ b/proxy/server.dev.conf @@ -9,8 +9,7 @@ server { proxy_read_timeout 10; location /api/ { - # proxy_pass https://server-radix-api-qa.dev.radix.equinor.com; - proxy_pass http://172.19.0.1:3002; + proxy_pass https://server-radix-api-qa.dev.radix.equinor.com; proxy_set_header Authorization "Bearer $http_x_forwarded_access_token"; proxy_set_header x-forwarded-access-token ""; } From b9a2430064bd8a019150d18d5889c8ba5c32c553 Mon Sep 17 00:00:00 2001 From: Richard87 Date: Wed, 25 Sep 2024 13:40:44 +0200 Subject: [PATCH 6/7] Added color and unknown users --- proxy/server.dev.conf | 3 +- src/components/app-config-ad-groups/index.tsx | 2 +- .../component/unknown-ad-groups-alert.tsx | 36 ++++--- src/components/graph/adGroups.tsx | 16 +++- src/components/page-configuration/index.tsx | 6 +- .../page-configuration/overview.tsx | 95 ++++++++++++------- 6 files changed, 107 insertions(+), 51 deletions(-) diff --git a/proxy/server.dev.conf b/proxy/server.dev.conf index 4e4507fdd..4959a0ecc 100644 --- a/proxy/server.dev.conf +++ b/proxy/server.dev.conf @@ -9,7 +9,8 @@ server { proxy_read_timeout 10; location /api/ { - proxy_pass https://server-radix-api-qa.dev.radix.equinor.com; + # proxy_pass https://server-radix-api-qa.dev.radix.equinor.com; + proxy_pass http://172.19.0.1:3002; proxy_set_header Authorization "Bearer $http_x_forwarded_access_token"; proxy_set_header x-forwarded-access-token ""; } diff --git a/src/components/app-config-ad-groups/index.tsx b/src/components/app-config-ad-groups/index.tsx index 55a3ad955..6fd9253ce 100644 --- a/src/components/app-config-ad-groups/index.tsx +++ b/src/components/app-config-ad-groups/index.tsx @@ -24,7 +24,7 @@ export const AppConfigAdGroups = ({ {labeling} User authentication is your application's responsibility; it is not - related to these groups + related to these Entra objects ; + unknownADUsers?: Array; } -export function UnknownADGroupsAlert({ unknownADGroups }: Props) { +export function UnknownADGroupsAlert({ + unknownADGroups, + unknownADUsers, +}: Props) { return ( - <> - {unknownADGroups?.length > 0 && ( - - Unknown or deleted AD group(s) - - {unknownADGroups.map((adGroup) => ( - {adGroup} - ))} - - - )} - + + Unknown or deleted Entra object(s) + + {unknownADGroups.map((adGroup) => ( + + {adGroup} + + ))} + {unknownADUsers.map((adSp) => ( + + {adSp} + + ))} + + ); } diff --git a/src/components/graph/adGroups.tsx b/src/components/graph/adGroups.tsx index 2ef932a03..8e04f2a4c 100644 --- a/src/components/graph/adGroups.tsx +++ b/src/components/graph/adGroups.tsx @@ -146,7 +146,10 @@ export function ADGroups({ onChange, adGroups, adUsers, isDisabled }: Props) { {(!state.isFetching && unknownADGroups?.length > 0) || (!spState.isFetching && unknownADUsers?.length > 0 && ( - + ))} @@ -175,7 +178,16 @@ function MultiValueLabel(props: MultiValueGenericProps) { if (props.data.type === 'User') icon = person; return ( -
+
diff --git a/src/components/page-configuration/index.tsx b/src/components/page-configuration/index.tsx index b8ec5834e..a10149391 100644 --- a/src/components/page-configuration/index.tsx +++ b/src/components/page-configuration/index.tsx @@ -67,7 +67,11 @@ export function PageConfiguration({ appName }: { appName: string }) { {registration?.name && ( <> - +
GitHub diff --git a/src/components/page-configuration/overview.tsx b/src/components/page-configuration/overview.tsx index 1a7d18962..ed1de7cd6 100644 --- a/src/components/page-configuration/overview.tsx +++ b/src/components/page-configuration/overview.tsx @@ -1,25 +1,38 @@ import { CircularProgress, + Icon, List, - Tooltip, Typography, } from '@equinor/eds-core-react'; import * as PropTypes from 'prop-types'; -import { useGetAdGroupsQuery } from '../../store/ms-graph-api'; +import { computer, group } from '@equinor/eds-icons'; +import { + useGetAdGroupsQuery, + useGetAdServicePrincipalQuery, +} from '../../store/ms-graph-api'; import { Alert } from '../alert'; import AsyncResource from '../async-resource/async-resource'; import { UnknownADGroupsAlert } from '../component/unknown-ad-groups-alert'; interface Props { - adGroups?: Array; + adGroups: Array; + adUsers: Array; appName: string; } -export function Overview({ adGroups, appName }: Props) { - const { data, ...state } = useGetAdGroupsQuery({ ids: adGroups }); - const unknownADGroups = adGroups?.filter( - (adGroupId) => !data?.some((adGroup) => adGroup.id === adGroupId) +export function Overview({ adGroups, adUsers, appName }: Props) { + const { data: groups, ...groupState } = useGetAdGroupsQuery({ + ids: adGroups, + }); + const { data: SPs, ...spState } = useGetAdServicePrincipalQuery({ + ids: adUsers, + }); + const unknownADGroups = adGroups.filter( + (adGroupId) => !groups?.some((x) => x.id === adGroupId) + ); + const unknownADUsers = adUsers.filter( + (adUserId) => !SPs?.some((x) => x.id === adUserId) ); return ( @@ -34,36 +47,54 @@ export function Overview({ adGroups, appName }: Props) {
{adGroups?.length > 0 ? ( <> - - Radix administrators ( - - AD - {' '} - groups): - - {state.isLoading ? ( + Radix administrators: + {groupState.isLoading || spState.isLoading ? ( <> Updating… ) : ( - - - {data?.map(({ id, displayName }) => ( - - - {displayName} - - + + + + {groups?.map(({ id, displayName }) => ( + + + {displayName} + + + ))} + {SPs?.map(({ id, displayName }) => ( + + + {displayName} + + + ))} + + + {(!groupState.isFetching && unknownADGroups.length > 0) || + (!spState.isFetching && unknownADUsers.length > 0 && ( + ))} - - {!state.isFetching && unknownADGroups?.length > 0 && ( - - )} )} From c89ad36b98d1a978f84661d2e79f36ab6b9280fc Mon Sep 17 00:00:00 2001 From: Richard87 Date: Wed, 25 Sep 2024 14:37:02 +0200 Subject: [PATCH 7/7] revert proxy --- proxy/server.dev.conf | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/proxy/server.dev.conf b/proxy/server.dev.conf index 4959a0ecc..4e4507fdd 100644 --- a/proxy/server.dev.conf +++ b/proxy/server.dev.conf @@ -9,8 +9,7 @@ server { proxy_read_timeout 10; location /api/ { - # proxy_pass https://server-radix-api-qa.dev.radix.equinor.com; - proxy_pass http://172.19.0.1:3002; + proxy_pass https://server-radix-api-qa.dev.radix.equinor.com; proxy_set_header Authorization "Bearer $http_x_forwarded_access_token"; proxy_set_header x-forwarded-access-token ""; }