Skip to content

Commit

Permalink
feat: [M3-7412] - Add new Parent/Child api-v4 endpoints (#9944)
Browse files Browse the repository at this point in the history
Co-authored-by: Jaalah Ramos <jaalah.ramos@gmail.com>
Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com>
Co-authored-by: Dajahi Wiley <114682940+dwiley-akamai@users.noreply.github.com>
Co-authored-by: Dajahi Wiley <dwiley@linode.com>
Co-authored-by: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com>
  • Loading branch information
6 people authored Dec 3, 2023
1 parent 9eee3aa commit 0989de5
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Upcoming Features
---

Add new endpoints for Parent/Child account switching ([#9944](https://github.com/linode/manager/pull/9944))
59 changes: 57 additions & 2 deletions packages/api-v4/src/account/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import { API_ROOT, BETA_API_ROOT } from '../constants';
import Request, {
setData,
setHeaders,
setMethod,
setURL,
setParams,
Expand All @@ -18,8 +19,10 @@ import {
CancelAccountPayload,
Agreements,
RegionalNetworkUtilization,
ChildAccountPayload,
} from './types';
import { Filter, ResourcePage as Page, Params } from '../types';
import type { Filter, ResourcePage, Params, RequestOptions } from '../types';
import type { Token } from '../profile';

/**
* getAccountInfo
Expand Down Expand Up @@ -115,7 +118,7 @@ export const getAccountAgreements = () =>
*
*/
export const getAccountAvailabilities = (params?: Params, filter?: Filter) =>
Request<Page<AccountAvailability>>(
Request<ResourcePage<AccountAvailability>>(
setURL(`${BETA_API_ROOT}/account/availability`),
setMethod('GET'),
setParams(params),
Expand Down Expand Up @@ -149,3 +152,55 @@ export const signAgreement = (data: Partial<Agreements>) => {
setData(data)
);
};

/**
* getChildAccounts
*
* This endpoint will return a paginated list of all Child Accounts with a Parent Account.
* The response will be similar to /account, except that it will list details for multiple accounts.
*/
export const getChildAccounts = ({ filter, params, headers }: RequestOptions) =>
Request<ResourcePage<Account>>(
setURL(`${API_ROOT}/account/child-accounts`),
setMethod('GET'),
setHeaders(headers),
setParams(params),
setXFilter(filter)
);

/**
* getChildAccount
*
* This endpoint will function similarly to /account/child-accounts,
* except that it will return account details for only a specific euuid.
*/
export const getChildAccount = ({ euuid, headers }: ChildAccountPayload) =>
Request<Account>(
setURL(`${API_ROOT}/account/child-accounts/${encodeURIComponent(euuid)}`),
setMethod('GET'),
setHeaders(headers)
);

/**
* createChildAccountPersonalAccessToken
*
* This endpoint will allow Parent Account Users with the "child_account_access" grant to
* create an ephemeral token for their proxy user on a child account, using the euuid of
* that child account. As noted in previous sections, this Token will inherit the
* permissions of the Proxy User, and the token itself will not be subject to additional
* restrictions.
*
* setHeaders() will be used for creating tokens from within the proxy account.
*/
export const createChildAccountPersonalAccessToken = ({
euuid,
headers,
}: ChildAccountPayload) =>
Request<Token>(
setURL(
`${API_ROOT}/account/child-accounts/${encodeURIComponent(euuid)}/token`
),
setMethod('POST'),
setHeaders(headers),
setData(euuid)
);
24 changes: 14 additions & 10 deletions packages/api-v4/src/account/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { APIWarning } from '../types';
import type { APIWarning, RequestOptions } from '../types';
import type { Capabilities, Region } from '../regions';

export type UserType = 'child' | 'parent' | 'proxy';
Expand Down Expand Up @@ -168,19 +168,19 @@ export interface Grant {
label: string;
}
export type GlobalGrantTypes =
| 'add_linodes'
| 'add_longview'
| 'longview_subscription'
| 'account_access'
| 'child_account_access'
| 'cancel_account'
| 'add_domains'
| 'add_stackscripts'
| 'add_nodebalancers'
| 'add_firewalls'
| 'add_images'
| 'add_linodes'
| 'add_longview'
| 'add_nodebalancers'
| 'add_stackscripts'
| 'add_volumes'
| 'add_firewalls'
| 'add_vpcs';
| 'add_vpcs'
| 'cancel_account'
| 'child_account_access'
| 'longview_subscription';

export interface GlobalGrants {
global: Record<GlobalGrantTypes, boolean | GrantLevel>;
Expand Down Expand Up @@ -226,6 +226,10 @@ export interface CancelAccountPayload {
comments: string;
}

export interface ChildAccountPayload extends RequestOptions {
euuid: string;
}

export type AgreementType = 'eu_model' | 'privacy_policy';

export interface Agreements {
Expand Down
27 changes: 27 additions & 0 deletions packages/api-v4/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ export interface Params {
page_size?: number;
}

export interface RequestOptions {
params?: Params;
filter?: Filter;
headers?: RequestHeaders;
}

interface FilterConditionTypes {
'+and'?: Filter[];
'+or'?: Filter[] | string[];
Expand Down Expand Up @@ -88,3 +94,24 @@ type LinodeFilter =
// },
// ],
// };

type RequestHeaderValue = string | string[] | number | boolean | null;

type RequestContentType =
| RequestHeaderValue
| 'application/json'
| 'application/octet-stream'
| 'application/x-www-form-urlencoded'
| 'multipart/form-data'
| 'text/html'
| 'text/plain';

export interface RequestHeaders {
[key: string]: RequestHeaderValue | undefined;
Accept?: string;
Authorization?: string;
'Content-Encoding'?: string;
'Content-Length'?: number;
'User-Agent'?: string;
'Content-Type'?: RequestContentType;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add new grants and RQ queries for Parent/Child account switching ([#9944](https://github.com/linode/manager/pull/9944))
64 changes: 60 additions & 4 deletions packages/manager/src/queries/account.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import {
Account,
createChildAccountPersonalAccessToken,
getAccountInfo,
getChildAccount,
getChildAccounts,
updateAccountInfo,
} from '@linode/api-v4/lib/account';
import { APIError } from '@linode/api-v4/lib/types';
} from '@linode/api-v4';
import { useMutation, useQuery, useQueryClient } from 'react-query';

import { useProfile } from 'src/queries/profile';
import { useGrants, useProfile } from 'src/queries/profile';

import { queryPresets } from './base';

import type {
APIError,
Account,
ChildAccountPayload,
RequestOptions,
ResourcePage,
Token,
} from '@linode/api-v4';

export const queryKey = 'account';

export const useAccount = () => {
Expand All @@ -31,3 +41,49 @@ export const useMutateAccount = () => {
},
});
};

/**
* For useChildAccounts and useChildAccount:
* Assuming authorization in the header implies parent account.
* API error expected if parent account lacks child_account_access grant.
*/
export const useChildAccounts = ({
filter,
headers,
params,
}: RequestOptions) => {
const { data: grants } = useGrants();
const hasExplicitAuthToken = Boolean(headers?.Authorization);

return useQuery<ResourcePage<Account>, APIError[]>(
[queryKey, 'childAccounts', 'paginated', params, filter],
() => getChildAccounts({ filter, headers, params }),
{
enabled:
Boolean(grants?.global?.child_account_access) || hasExplicitAuthToken,
keepPreviousData: true,
}
);
};

export const useChildAccount = ({ euuid, headers }: ChildAccountPayload) => {
const { data: grants } = useGrants();
const hasExplicitAuthToken = Boolean(headers?.Authorization);

return useQuery<Account, APIError[]>(
[queryKey, 'childAccounts', 'childAccount', euuid],
() => getChildAccount({ euuid }),
{
enabled:
Boolean(grants?.global?.child_account_access) || hasExplicitAuthToken,
}
);
};

export const useCreateChildAccountPersonalAccessTokenMutation = ({
euuid,
headers,
}: ChildAccountPayload) =>
useMutation<Token, APIError[], ChildAccountPayload>(() =>
createChildAccountPersonalAccessToken({ euuid, headers })
);
21 changes: 21 additions & 0 deletions packages/manager/src/request.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,24 @@ describe('injectEuuidToProfile', () => {
expect(injectEuuidToProfile(accountResponse as any).data).toEqual(profile);
});
});

describe('setupInterceptors', () => {
it('should set the authorization header if it is explicitly set', () => {
const config = {
headers: {
Authorization: 'Bearer 1234',
},
};

const state = store.getState();
const token = state.authentication?.token ?? '';

const headers = new AxiosHeaders(config.headers);
const hasExplicitAuthToken = headers.hasAuthorization();
const bearer = hasExplicitAuthToken ? headers.getAuthorization() : token;

headers.setAuthorization(bearer);

expect(headers.getAuthorization()).toEqual('Bearer 1234');
});
});
10 changes: 8 additions & 2 deletions packages/manager/src/request.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,14 +192,20 @@ export const getXCustomerUuidHeader = (
export const setupInterceptors = (store: ApplicationStore) => {
baseRequest.interceptors.request.use((config) => {
const state = store.getState();
/** Will end up being "Admin: 1234" or "Bearer 1234" */
/** Will end up being "Admin 1234" or "Bearer 1234" */
const token = ACCESS_TOKEN || (state.authentication?.token ?? '');

const url = getURL(config);

const headers = new AxiosHeaders(config.headers);

headers.setAuthorization(token);
// If headers are explicitly passed to our endpoint via
// setHeaders(), we don't want this overridden.
const hasExplicitAuthToken = headers.hasAuthorization();

const bearer = hasExplicitAuthToken ? headers.getAuthorization() : token;

headers.setAuthorization(bearer);

return {
...config,
Expand Down

0 comments on commit 0989de5

Please sign in to comment.