Add trusted IP rules to ST V3

This commit is contained in:
Tuan Dang
2023-09-30 20:52:04 +01:00
parent cdf4440848
commit eb2f433f43
11 changed files with 404 additions and 205 deletions
@@ -87,12 +87,14 @@ export const useUpdateServiceTokenV3 = () => {
name,
isActive,
scopes,
trustedIps,
expiresIn
}) => {
const { data: { serviceTokenData } } = await apiRequest.patch(`/api/v3/service-token/${serviceTokenDataId}`, {
name,
isActive,
scopes,
trustedIps,
expiresIn
});
@@ -44,6 +44,13 @@ export type ServiceTokenV3Scope = {
secretPath: string;
};
export type ServiceTokenV3TrustedIp = {
_id: string;
ipAddress: string;
type: "ipv4" | "ipv6";
prefix?: number;
}
export type ServiceTokenDataV3 = {
_id: string;
name: string;
@@ -52,6 +59,7 @@ export type ServiceTokenDataV3 = {
lastUsed?: string;
usageCount: number;
scopes: ServiceTokenV3Scope[];
trustedIps: ServiceTokenV3TrustedIp[];
expiresAt?: string;
createdAt: string;
updatedAt: string;
@@ -62,6 +70,9 @@ export type CreateServiceTokenDataV3DTO = {
workspaceId: string;
publicKey: string;
scopes: ServiceTokenV3Scope[];
trustedIps: {
ipAddress: string;
}[];
expiresIn?: number;
encryptedKey: string;
nonce: string;
@@ -77,6 +88,9 @@ export type UpdateServiceTokenDataV3DTO = {
isActive?: boolean;
name?: string;
scopes?: ServiceTokenV3Scope[];
trustedIps?: {
ipAddress: string;
}[];
expiresIn?: number;
}
@@ -20,11 +20,7 @@ import {
Modal,
ModalContent,
Select,
SelectItem,
// Accordion,
// AccordionItem,
// AccordionTrigger,
// AccordionContent
SelectItem
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
@@ -35,7 +31,10 @@ import {
import {
Permission
} from "@app/hooks/api/serviceTokens/enums";
import { ServiceTokenV3Scope } from "@app/hooks/api/serviceTokens/types";
import {
ServiceTokenV3Scope,
ServiceTokenV3TrustedIp
} from "@app/hooks/api/serviceTokens/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const expirations = [
@@ -58,23 +57,32 @@ const schema = yup.object({
name: yup.string().required("ST V3 name is required"),
expiresIn: yup.string(),
scopes: yup
.array(
yup.object({
permission: yup.string().oneOf(Object.keys(permissionsMap), "Invalid permission").required().label("Permission"),
environment: yup.string().max(50).required().label("Environment"),
secretPath: yup
.string()
.required()
.default("/")
.label("Secret Path")
.transform((val) =>
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
)
})
)
.min(1)
.required()
.label("Scope"),
trustedIps: yup
.array(
yup.object({
permission: yup.string().oneOf(Object.keys(permissionsMap), "Invalid permission").required().label("Permission"),
environment: yup.string().max(50).required().label("Environment"),
secretPath: yup
.string()
.required()
.default("/")
.label("Secret Path")
.transform((val) =>
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
)
ipAddress: yup.string().max(50).required().label("IP Address")
})
)
.min(1)
.required()
.label("Scope")
.label("Trusted IP")
}).required();
export type FormData = yup.InferType<typeof schema>;
@@ -107,6 +115,9 @@ export const AddServiceTokenV3Modal = ({
permission: "read",
environment: currentWorkspace?.environments?.[0]?.slug,
secretPath: "/",
}],
trustedIps: [{
ipAddress: "0.0.0.0/0"
}]
}
});
@@ -116,6 +127,7 @@ export const AddServiceTokenV3Modal = ({
serviceTokenDataId: string;
name: string;
scopes: ServiceTokenV3Scope[];
trustedIps: ServiceTokenV3TrustedIp[];
};
if (serviceTokenData) {
@@ -132,6 +144,14 @@ export const AddServiceTokenV3Modal = ({
secretPath: "/",
permission
})
}),
trustedIps: serviceTokenData.trustedIps.map(({
ipAddress,
prefix
}: ServiceTokenV3TrustedIp) => {
return ({
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
});
})
});
} else {
@@ -141,17 +161,22 @@ export const AddServiceTokenV3Modal = ({
permission: "read",
environment: currentWorkspace?.environments?.[0]?.slug,
secretPath: "/",
}],
trustedIps: [{
ipAddress: "0.0.0.0/0"
}]
});
}
}, [popUp?.serviceTokenV3?.data]);
const { fields: tokenScopes, append, remove } = useFieldArray({ control, name: "scopes" });
const { fields: tokenTrustedIps, append: appendTrustedIp, remove: removeTrustedIp } = useFieldArray({ control, name: "trustedIps" });
const onFormSubmit = async ({
name,
expiresIn,
scopes
scopes,
trustedIps
}: FormData) => {
try {
const serviceTokenData = popUp?.serviceTokenV3?.data as {
@@ -167,14 +192,16 @@ export const AddServiceTokenV3Modal = ({
secretPath: scope.secretPath,
permissions: permissionsMap[scope.permission]
});
})
});
if (serviceTokenData) {
// update
await updateMutateAsync({
serviceTokenDataId: serviceTokenData.serviceTokenDataId,
name,
scopes: reformattedScopes,
trustedIps,
expiresIn: expiresIn === "" ? undefined : Number(expiresIn)
});
} else {
@@ -206,6 +233,7 @@ export const AddServiceTokenV3Modal = ({
workspaceId: currentWorkspace._id,
publicKey,
scopes: reformattedScopes,
trustedIps,
expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
encryptedKey: ciphertext,
nonce
@@ -270,175 +298,171 @@ export const AddServiceTokenV3Modal = ({
</FormControl>
)}
/>
{tokenScopes.map(({ id }, index) => (
<div className="flex items-end space-x-2 mb-3" key={id}>
<Controller
control={control}
name={`scopes.${index}.permission`}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
className="mb-0"
label={index === 0 ? "Permission" : undefined}
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-36"
>
<SelectItem value="read" key="st-v3-read">
Read
</SelectItem>
<SelectItem value="readWrite" key="st-v3-write">
Read &amp; Write
</SelectItem>
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name={`scopes.${index}.environment`}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
className="mb-0"
label={index === 0 ? "Environment" : undefined}
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-36"
>
{currentWorkspace?.environments.map(({ name, slug }) => (
<SelectItem value={slug} key={slug}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name={`scopes.${index}.secretPath`}
defaultValue="/"
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Secrets Path" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="can be /, /nested/**, /**/deep" />
</FormControl>
)}
/>
<IconButton
onClick={() => remove(index)}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
{tokenScopes.map(({ id }, index) => (
<div className="flex items-end space-x-2 mb-3" key={id}>
<Controller
control={control}
name={`scopes.${index}.permission`}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
className="mb-0"
label={index === 0 ? "Permission" : undefined}
errorText={error?.message}
isError={Boolean(error)}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() =>
append({
permission: "read",
environment: currentWorkspace?.environments?.[0]?.slug || "",
secretPath: "/"
})
}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-36"
>
<SelectItem value="read" key="st-v3-read">
Read
</SelectItem>
<SelectItem value="readWrite" key="st-v3-write">
Read &amp; Write
</SelectItem>
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name={`scopes.${index}.environment`}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
className="mb-0"
label={index === 0 ? "Environment" : undefined}
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-36"
>
{currentWorkspace?.environments.map(({ name, slug }) => (
<SelectItem value={slug} key={slug}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name={`scopes.${index}.secretPath`}
defaultValue="/"
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Secrets Path" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="can be /, /nested/**, /**/deep" />
</FormControl>
)}
/>
<IconButton
onClick={() => remove(index)}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() =>
append({
permission: "read",
environment: currentWorkspace?.environments?.[0]?.slug || "",
secretPath: "/"
})
}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add Scope
</Button>
</div>
{tokenTrustedIps.map(({ id }, index) => (
<div className="flex items-end space-x-2 mb-3" key={id}>
<Controller
control={control}
name={`trustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Trusted IP" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="123.456.789.0" />
</FormControl>
)}
/>
<IconButton
onClick={() => removeTrustedIp(index)}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
Add Scope
</Button>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
<Controller
control={control}
name="expiresIn"
defaultValue="15552000"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label={`${popUp?.serviceTokenV3?.data ? "Update" : ""} Expire In`}
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() =>
appendTrustedIp({
ipAddress: "0.0.0.0/0"
})
}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<Controller
control={control}
name="expiresIn"
defaultValue="15552000"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label={`${popUp?.serviceTokenV3?.data ? "Update" : ""} Expire In`}
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{expirations.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={`api-key-expiration-${label}`}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{/* <Accordion
type="multiple"
className="w-full"
>
<AccordionItem value="section-1">
<AccordionTrigger>Scopes</AccordionTrigger>
<AccordionContent>Description of Section 1</AccordionContent>
</AccordionItem>
</Accordion> */}
{/* <h3 className="text-mineshaft-400 text-sm mb-2">Temporariness</h3>
<Switch
id={`enable-ephemerality`}
onCheckedChange={(value) => setIsTemporary(value)}
isChecked={isTemporary}
>
<div className="w-96 mr-4">
<p className="text-gray-400 text-md">This token will be deactivated after your specified duration.</p>
</div>
</Switch>
{isTemporary && (
<Controller
control={control}
name="expiresIn"
defaultValue="15552000"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Duration"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{expirations.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={`api-key-expiration-${label}`}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)} */}
{expirations.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={`api-key-expiration-${label}`}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
@@ -22,7 +22,7 @@ import {
useUpdateServiceTokenV3
} from "@app/hooks/api";
import { Permission } from "@app/hooks/api/serviceTokens/enums"
import { ServiceTokenV3Scope } from "@app/hooks/api/serviceTokens/types"
import { ServiceTokenV3Scope, ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types"
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
@@ -32,6 +32,7 @@ type Props = {
serviceTokenDataId?: string;
name?: string;
scopes?: ServiceTokenV3Scope[];
trustedIps?: ServiceTokenV3TrustedIp[];
}
) => void;
};
@@ -89,7 +90,8 @@ export const ServiceTokenV3Table = ({
<Th>Name</Th>
<Th>Status</Th>
<Th>Scopes</Th>
<Th># Times Used</Th>
<Th>Trusted IPs</Th>
{/* <Th># Times Used</Th> */}
<Th>Last Used</Th>
<Th>Created At</Th>
<Th>Expires At</Th>
@@ -106,8 +108,9 @@ export const ServiceTokenV3Table = ({
name,
isActive,
lastUsed,
usageCount,
// usageCount,
scopes,
trustedIps,
createdAt,
expiresAt
}) => {
@@ -154,7 +157,20 @@ export const ServiceTokenV3Table = ({
);
})}
</Td>
<Td>{usageCount}</Td>
<Td>
{trustedIps.map(({
_id: trustedIpId,
ipAddress,
prefix
}) => {
return (
<p key={`service-token-${_id}-}-trusted-ip-${trustedIpId}`}>
{`${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`}
</p>
);
})}
</Td>
{/* <Td>{usageCount}</Td> */}
<Td>{lastUsed ? formatDate(lastUsed) : "-"}</Td>
<Td>{formatDate(createdAt)}</Td>
<Td>{expiresAt ? formatDate(expiresAt) : "-"}</Td>
@@ -170,6 +186,7 @@ export const ServiceTokenV3Table = ({
serviceTokenDataId: _id,
name,
scopes,
trustedIps
});
}}
size="lg"