Split ST V3 modal into option tabs, re-modularized authn methods

This commit is contained in:
Tuan Dang
2023-11-03 22:37:50 +02:00
parent f9c28ab045
commit 176d92546c
15 changed files with 336 additions and 298 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ import {
import { TelemetryService } from "../services"; import { TelemetryService } from "../services";
import { client, getEncryptionKey, getRootEncryptionKey } from "../config"; import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
import { EEAuditLogService, EELogService, EESecretService } from "../ee/services"; import { EEAuditLogService, EELogService, EESecretService } from "../ee/services";
import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/authn/authDataExtractors"; import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/authn/helpers";
import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService"; import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService";
import picomatch from "picomatch"; import picomatch from "picomatch";
import path from "path"; import path from "path";
+1 -1
View File
@@ -2,7 +2,7 @@ import jwt from "jsonwebtoken";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { AuthMode } from "../variables"; import { AuthMode } from "../variables";
import { AuthData } from "../interfaces/middleware"; import { AuthData } from "../interfaces/middleware";
import { extractAuthMode, getAuthData } from "../utils/authn/authMode"; import { extractAuthMode, getAuthData } from "../utils/authn/helpers";
import { UnauthorizedRequestError } from "../utils/errors"; import { UnauthorizedRequestError } from "../utils/errors";
declare module "jsonwebtoken" { declare module "jsonwebtoken" {
@@ -1,53 +0,0 @@
import { AuthData } from "../../../interfaces/middleware";
import {
ServiceAccount,
ServiceTokenData,
ServiceTokenDataV3,
User
} from "../../../models";
/**
* Returns an object containing the id of the authentication data payload
* @param {AuthData} authData - authentication data object
* @returns
*/
export const getAuthDataPayloadIdObj = (authData: AuthData) => {
if (authData.authPayload instanceof User) {
return { userId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceAccount) {
return { serviceAccountId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceTokenData) {
return { serviceTokenDataId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceTokenDataV3) {
return { serviceTokenDataId: authData.authPayload._id };
}
};
/**
* Returns an object containing the user associated with the authentication data payload
* @param {AuthData} authData - authentication data object
* @returns
*/
export const getAuthDataPayloadUserObj = (authData: AuthData) => {
if (authData.authPayload instanceof User) {
return { user: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceAccount) {
return { user: authData.authPayload.user };
}
if (authData.authPayload instanceof ServiceTokenData) {
return { user: authData.authPayload.user };
}
if (authData.authPayload instanceof ServiceTokenDataV3) {
return { user: authData.authPayload.user };
}
}
@@ -1,4 +0,0 @@
export {
extractAuthMode,
getAuthData
} from "./helpers";
@@ -0,0 +1,5 @@
export * from "./apiKey";
export * from "./apiKeyV2";
export * from "./jwt";
export * from "./serviceTokenV2";
export * from "./serviceTokenV3";
@@ -0,0 +1,53 @@
import { AuthData } from "../../../interfaces/middleware";
import {
ServiceAccount,
ServiceTokenData,
ServiceTokenDataV3,
User
} from "../../../models";
/**
* Returns an object containing the id of the authentication data payload
* @param {AuthData} authData - authentication data object
* @returns
*/
export const getAuthDataPayloadIdObj = (authData: AuthData) => {
if (authData.authPayload instanceof User) {
return { userId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceAccount) {
return { serviceAccountId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceTokenData) {
return { serviceTokenDataId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceTokenDataV3) {
return { serviceTokenDataId: authData.authPayload._id };
}
};
/**
* Returns an object containing the user associated with the authentication data payload
* @param {AuthData} authData - authentication data object
* @returns
*/
export const getAuthDataPayloadUserObj = (authData: AuthData) => {
if (authData.authPayload instanceof User) {
return { user: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceAccount) {
return { user: authData.authPayload.user };
}
if (authData.authPayload instanceof ServiceTokenData) {
return { user: authData.authPayload.user };
}
if (authData.authPayload instanceof ServiceTokenDataV3) {
return { user: authData.authPayload.user };
}
}
@@ -1,15 +1,19 @@
import { AuthData } from "../../../interfaces/middleware";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { getAuthSecret } from "../../../config"; import { getAuthSecret } from "../../../config";
import { ActorType } from "../../../ee/models"; import { ActorType } from "../../../ee/models";
import { AuthMode, AuthTokenType } from "../../../variables"; import { AuthMode, AuthTokenType } from "../../../variables";
import { UnauthorizedRequestError } from "../../errors"; import { UnauthorizedRequestError } from "../../errors";
import { validateAPIKey } from "./apiKey"; import {
import { validateAPIKeyV2 } from "./apiKeyV2"; validateAPIKey,
import { validateServiceTokenV2 } from "./serviceTokenV2"; validateAPIKeyV2,
import { validateServiceTokenV3 } from "./serviceTokenV3"; validateJWT,
import { validateJWT } from "./jwt"; validateServiceTokenV2,
validateServiceTokenV3
} from "../authModeValidators";
import { getUserAgentType } from "../../posthog"; import { getUserAgentType } from "../../posthog";
import { AuthData } from "../../../interfaces/middleware";
export * from "./authDataExtractors";
interface ExtractAuthModeParams { interface ExtractAuthModeParams {
headers: { [key: string]: string | string[] | undefined } headers: { [key: string]: string | string[] | undefined }
@@ -1,10 +1,10 @@
import { APIKeySection } from "../APIKeySection"; import { APIKeySection } from "../APIKeySection";
import { APIKeyV2Section } from "../APIKeyV2Section"; // import { APIKeyV2Section } from "../APIKeyV2Section";
export const PersonalAPIKeyTab = () => { export const PersonalAPIKeyTab = () => {
return ( return (
<> <>
<APIKeyV2Section /> {/* <APIKeyV2Section /> */}
<APIKeySection /> <APIKeySection />
</> </>
); );
@@ -1,10 +1,10 @@
import { ServiceTokenSection } from "../ServiceTokenSection"; import { ServiceTokenSection } from "../ServiceTokenSection";
import { ServiceTokenV3Section } from "../ServiceTokenV3Section"; // import { ServiceTokenV3Section } from "../ServiceTokenV3Section";
export const ProjectServiceTokensTab = () => { export const ProjectServiceTokensTab = () => {
return ( return (
<> <>
<ServiceTokenV3Section /> {/* <ServiceTokenV3Section /> */}
<ServiceTokenSection /> <ServiceTokenSection />
</> </>
); );
@@ -1,11 +1,13 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark, faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faCopy,faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { motion } from "framer-motion";
import nacl from "tweetnacl"; import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util"; import { encodeBase64 } from "tweetnacl-util";
import * as yup from "yup"; import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { import {
decryptAssymmetric, decryptAssymmetric,
@@ -21,8 +23,11 @@ import {
Select, Select,
SelectItem, SelectItem,
Switch, Switch,
UpgradePlanModal Tab,
} from "@app/components/v2"; TabList,
TabPanel,
Tabs,
UpgradePlanModal} from "@app/components/v2";
import { import {
useSubscription, useSubscription,
useWorkspace useWorkspace
@@ -42,6 +47,11 @@ import {
} from "@app/hooks/api/serviceTokens/types"; } from "@app/hooks/api/serviceTokens/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
enum TabSections {
General = "general",
Advanced = "advanced"
}
const expirations = [ const expirations = [
{ label: "Never", value: "" }, { label: "Never", value: "" },
{ label: "1 day", value: "86400" }, { label: "1 day", value: "86400" },
@@ -343,246 +353,269 @@ export const AddServiceTokenV3Modal = ({
<ModalContent title={`${popUp?.serviceTokenV3?.data ? "Update" : "Create"} Service Token V3`}> <ModalContent title={`${popUp?.serviceTokenV3?.data ? "Update" : "Create"} Service Token V3`}>
{!hasServiceTokenJSON ? ( {!hasServiceTokenJSON ? (
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onFormSubmit)}>
<Controller <Tabs defaultValue={TabSections.General}>
control={control} <TabList>
defaultValue="" <div className="flex flex-row border-b border-mineshaft-600 w-full">
name="name" <Tab value={TabSections.General}>General</Tab>
render={({ field, fieldState: { error } }) => ( <Tab value={TabSections.Advanced}>Advanced</Tab>
<FormControl </div>
label="Name" </TabList>
isError={Boolean(error)} <TabPanel value={TabSections.General}>
errorText={error?.message} <motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
> >
<Input <Controller
{...field} control={control}
placeholder="My ST V3" defaultValue=""
/> name="name"
</FormControl> render={({ field, fieldState: { error } }) => (
)}
/>
{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"
>
<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 } }) => {
return (
<FormControl <FormControl
className="mb-0 flex-grow" label="Name"
label={index === 0 ? "Trusted IP" : undefined}
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
> >
<Input <Input
value={field.value} {...field}
onChange={(e) => { placeholder="My ST V3"
/>
</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"
>
<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>
<Controller
control={control}
name="expiresIn"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label={`${popUp?.serviceTokenV3?.data ? "Update" : ""} Refresh Token Expires In`}
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>
)}
/>
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Advanced}>
<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 } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Trusted IP" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan");
}}
placeholder="123.456.789.0"
/>
</FormControl>
);
}}
/>
<IconButton
onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
field.onChange(e); removeTrustedIp(index);
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
placeholder="123.456.789.0" 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={() => {
if (subscription?.ipAllowlisting) {
appendTrustedIp({
ipAddress: "0.0.0.0/0"
})
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<Controller
control={control}
defaultValue="7200"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="7200"
/> />
</FormControl> </FormControl>
); )}
}} />
/> <div className="mt-8">
<IconButton <Controller
onClick={() => { control={control}
if (subscription?.ipAllowlisting) { name="isRefreshTokenRotationEnabled"
removeTrustedIp(index); render={({ field: { onChange, value } }) => (
return; <Switch
} id="label-refresh-token-rotation"
onCheckedChange={(isChecked) => onChange(isChecked)}
handlePopUpOpen("upgradePlan"); isChecked={value}
}} >
size="lg" Refresh Token Rotation
colorSchema="danger" </Switch>
variant="plain" )}
ariaLabel="update" />
className="p-3" <p className="mt-4 text-sm font-normal text-mineshaft-400">When enabled, as a result of exchanging a refresh token, a new refresh token will be issued and the existing token will be invalidated.</p>
> </div>
<FontAwesomeIcon icon={faXmark} /> </div>
</IconButton> </TabPanel>
</div> </Tabs>
))} <div className="flex items-center">
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendTrustedIp({
ipAddress: "0.0.0.0/0"
})
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<Controller
control={control}
name="expiresIn"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label={`${popUp?.serviceTokenV3?.data ? "Update" : ""} Refresh Token Expires In`}
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>
)}
/>
<Controller
control={control}
defaultValue="7200"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="7200"
/>
</FormControl>
)}
/>
<div className="mt-8 mb-[2.36rem]">
<Controller
control={control}
name="isRefreshTokenRotationEnabled"
render={({ field: { onChange, value } }) => (
<Switch
id="label-refresh-token-rotation"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
>
Refresh Token Rotation
</Switch>
)}
/>
</div>
<div className="mt-8 flex items-center">
<Button <Button
className="mr-4" className="mr-4"
size="sm" size="sm"