diff --git a/changes/32722-ui-smallstep b/changes/32722-ui-smallstep new file mode 100644 index 0000000000..4804e54a0f --- /dev/null +++ b/changes/32722-ui-smallstep @@ -0,0 +1 @@ +- Updated UI to add Smallstep certificate authority integration. diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index 4785d9abb0..5ac77e5b10 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -60,6 +60,9 @@ export enum ActivityType { AddedHydrant = "added_hydrant", DeletedHydrant = "deleted_hydrant", EditedHydrant = "edited_hydrant", + AddedSmallstep = "added_smallstep", + DeletedSmallstep = "deleted_smallstep", + EditedSmallstep = "edited_smallstep", CreatedWindowsProfile = "created_windows_profile", DeletedWindowsProfile = "deleted_windows_profile", EditedWindowsProfile = "edited_windows_profile", diff --git a/frontend/interfaces/certificates.ts b/frontend/interfaces/certificates.ts index 4d236b464b..9b0d452bfb 100644 --- a/frontend/interfaces/certificates.ts +++ b/frontend/interfaces/certificates.ts @@ -78,18 +78,30 @@ export interface ICertificatesCustomSCEP { challenge: string; } +export interface ICertificatesSmallstep { + id?: number; + type?: "smallstep"; + name: string; + url: string; + challenge_url: string; + username: string; + password: string; +} + export type ICertificateAuthorityType = | "ndes_scep_proxy" | "digicert" | "custom_scep_proxy" - | "hydrant"; + | "hydrant" + | "smallstep"; /** all the types of certificates */ export type ICertificateAuthority = | ICertificatesNDES | ICertificatesDigicert | ICertificatesHydrant - | ICertificatesCustomSCEP; + | ICertificatesCustomSCEP + | ICertificatesSmallstep; export const isNDESCertAuthority = ( integration: ICertificateAuthority @@ -130,3 +142,15 @@ export const isCustomSCEPCertAuthority = ( "name" in integration && "url" in integration && "challenge" in integration ); }; + +export const isSmallstepCertAuthority = ( + integration: ICertificateAuthority +): integration is ICertificatesSmallstep => { + return ( + "name" in integration && + "url" in integration && + "challenge_url" in integration && + "username" in integration && + "password" in integration + ); +}; diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx index 10412e7471..307df022d6 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx @@ -1656,19 +1656,22 @@ const getDetail = (activity: IActivity, isPremiumTier: boolean) => { } case ActivityType.AddedCustomScepProxy: case ActivityType.AddedDigicert: - case ActivityType.AddedHydrant: { + case ActivityType.AddedHydrant: + case ActivityType.AddedSmallstep: { return TAGGED_TEMPLATES.addedCertificateAuthority(activity.details?.name); } case ActivityType.DeletedCustomScepProxy: case ActivityType.DeletedDigicert: - case ActivityType.DeletedHydrant: { + case ActivityType.DeletedHydrant: + case ActivityType.DeletedSmallstep: { return TAGGED_TEMPLATES.deletedCertificateAuthority( activity.details?.name ); } case ActivityType.EditedCustomScepProxy: case ActivityType.EditedDigicert: - case ActivityType.EditedHydrant: { + case ActivityType.EditedHydrant: + case ActivityType.EditedSmallstep: { return TAGGED_TEMPLATES.editedCertificateAuthority( activity.details?.name ); diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/AddCertAuthorityModal.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/AddCertAuthorityModal.tsx index 623d59c5a3..95c2502008 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/AddCertAuthorityModal.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/AddCertAuthorityModal.tsx @@ -25,12 +25,14 @@ import CustomSCEPForm from "../CustomSCEPForm"; import { ICustomSCEPFormData } from "../CustomSCEPForm/CustomSCEPForm"; import HydrantForm from "../HydrantForm"; import { IHydrantFormData } from "../HydrantForm/HydrantForm"; +import { ISmallstepFormData } from "../SmallstepForm/SmallstepForm"; export type ICertFormData = | IDigicertFormData | IHydrantFormData | INDESFormData - | ICustomSCEPFormData; + | ICustomSCEPFormData + | ISmallstepFormData; const baseClass = "add-cert-authority-modal"; @@ -79,6 +81,16 @@ const AddCertAuthorityModal = ({ scepURL: "", challenge: "", }); + const [ + smallstepFormData, + setSmallstepFormData, + ] = useState({ + name: "", + scepURL: "", + challengeURL: "", + username: "", + password: "", + }); const onChangeDropdown = (value: ICertificateAuthorityType) => { setCertAuthorityType(value); @@ -104,6 +116,10 @@ const AddCertAuthorityModal = ({ setFormData = setCustomSCEPFormData; formData = customSCEPFormData; break; + case "smallstep": + setFormData = setSmallstepFormData; + formData = smallstepFormData; + break; default: return; } @@ -129,6 +145,9 @@ const AddCertAuthorityModal = ({ case "custom_scep_proxy": formData = customSCEPFormData; break; + case "smallstep": + formData = smallstepFormData; + break; default: return; } diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/helpers.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/helpers.tsx index 3283884d64..3aacb547ed 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/helpers.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/AddCertAuthorityModal/helpers.tsx @@ -13,7 +13,9 @@ import { ICertFormData } from "../AddCertAuthorityModal/AddCertAuthorityModal"; import { INDESFormData } from "../NDESForm/NDESForm"; import { ICustomSCEPFormData } from "../CustomSCEPForm/CustomSCEPForm"; import { IHydrantFormData } from "../HydrantForm/HydrantForm"; +import { ISmallstepFormData } from "../SmallstepForm/SmallstepForm"; +// FIXME: do we care about the order of these? Should we alphabetize them or something? const DEFAULT_CERT_AUTHORITY_OPTIONS: IDropdownOption[] = [ { label: "DigiCert", value: "digicert" }, { @@ -28,6 +30,7 @@ const DEFAULT_CERT_AUTHORITY_OPTIONS: IDropdownOption[] = [ label: "Custom SCEP (Simple Certificate Enrollment Protocol)", value: "custom_scep_proxy", }, + { label: "Smallstep", value: "smallstep" }, ]; /** @@ -131,6 +134,24 @@ export const generateAddCertAuthorityData = ( client_secret: clientSecret, }, }; + case "smallstep": + // eslint-disable-next-line no-case-declarations + const { + name: smallstepName, + scepURL: smallstepScepURL, + challengeURL, + username: smallstepUsername, + password: smallstepPassword, + } = formData as ISmallstepFormData; + return { + smallstep: { + name: smallstepName, + url: smallstepScepURL, + challenge_url: challengeURL, + username: smallstepUsername, + password: smallstepPassword, + }, + }; default: return undefined; } diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CertificateAuthorityList/CertificateAuthorityList.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CertificateAuthorityList/CertificateAuthorityList.tsx index b09fd32260..3d97d8adfa 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CertificateAuthorityList/CertificateAuthorityList.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/CertificateAuthorityList/CertificateAuthorityList.tsx @@ -34,6 +34,9 @@ export const generateListData = ( case "hydrant": description = "Hydrant (EST - Enrollment Over Secure Transport) "; break; + case "smallstep": + description = "Smallstep"; + break; default: description = "Unknown Certificate Authority Type"; } diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/EditCertAuthorityModal.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/EditCertAuthorityModal.tsx index 325cd16c7c..f5a112619c 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/EditCertAuthorityModal.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/EditCertAuthorityModal.tsx @@ -22,6 +22,7 @@ import { ICertFormData } from "../AddCertAuthorityModal/AddCertAuthorityModal"; import NDESForm from "../NDESForm"; import CustomSCEPForm from "../CustomSCEPForm"; import HydrantForm from "../HydrantForm"; +import SmallstepForm from "../SmallstepForm"; const baseClass = "edit-cert-authority-modal"; @@ -92,6 +93,16 @@ const EditCertAuthorityModal = ({ if (certAuthority.type === "hydrant") { return HydrantForm; } + if (certAuthority.type === "smallstep") { + return SmallstepForm; + } + + // FIXME: seems like we have some competing patterns in here where we sometimes do switch + // statements with a default and sometimes do if or if/else if with a final default return. We + // should probably standardize on one or the other. Also, do we really want this to be the + // default? Why not have an explicit check for custom_scep_proxy and have the final + // else throw an error? + return CustomSCEPForm; }; diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/helpers.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/helpers.tsx index 3cfd59bb9a..5cc17404cb 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/helpers.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/EditCertAuthorityModal/helpers.tsx @@ -11,6 +11,7 @@ import { IDigicertFormData } from "../DigicertForm/DigicertForm"; import { INDESFormData } from "../NDESForm/NDESForm"; import { ICustomSCEPFormData } from "../CustomSCEPForm/CustomSCEPForm"; import { IHydrantFormData } from "../HydrantForm/HydrantForm"; +import { ISmallstepFormData } from "../SmallstepForm/SmallstepForm"; export const generateDefaultFormData = ( certAuthority: ICertificateAuthority @@ -40,8 +41,22 @@ export const generateDefaultFormData = ( clientId: certAuthority.client_id, clientSecret: certAuthority.client_secret, }; + } else if (certAuthority.type === "smallstep") { + return { + name: certAuthority.name, + scepURL: certAuthority.url, + challengeURL: certAuthority.challenge_url, + username: certAuthority.username, + password: certAuthority.password, + }; } + // FIXME: seems like we have some competing patterns in here where we sometimes do switch + // statements with a default and sometimes do if or if/else if with a final default return. We + // should probably standardize on one or the other. Also, do we really want this to be the + // default? Why not have an explicit check for custom_scep_proxy and have the final + // else throw an error? + const customSCEPcert = certAuthority as ICertificatesCustomSCEP; return { name: customSCEPcert.name, @@ -122,6 +137,30 @@ export const generateEditCertAuthorityData = ( certAuthWithoutType ), }; + case "smallstep": + // eslint-disable-next-line no-case-declarations + const { + name: smallstepName, + scepURL: smallstepURL, + challengeURL: smallstepChallengeURL, + username: smallstepUsername, + password: smallstepPassword, + } = formData as ISmallstepFormData; + return { + smallstep: deepDifference( + { + name: smallstepName, + scep_url: smallstepURL, + challenge_url: smallstepChallengeURL, + username: smallstepUsername, + password: smallstepPassword, + }, + certAuthWithoutType + ), + }; + + // FIXME: do we really want this to be the default? why not have an explicit case for + // custom_scep_proxy and have the default throw an error? default: // custom_scep_proxy // eslint-disable-next-line no-case-declarations @@ -198,6 +237,19 @@ export const updateFormData = ( formData.clientSecret === "********" ? "" : formData.clientSecret, }; } + } else if (certAuthority.type === "smallstep") { + const formData = prevFormData as ISmallstepFormData; + if ( + update.name === "name" || + update.name === "scepURL" || + update.name === "challengeURL" || + update.name === "username" + ) { + return { + ...newData, + password: formData.password === "********" ? "" : formData.password, + }; + } } return newData; diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/SmallstepForm/SmallstepForm.tests.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/SmallstepForm/SmallstepForm.tests.tsx new file mode 100644 index 0000000000..d3f75ad013 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/SmallstepForm/SmallstepForm.tests.tsx @@ -0,0 +1,113 @@ +import React from "react"; +import { noop } from "lodash"; +import { render, screen } from "@testing-library/react"; + +import SmallstepForm, { ISmallstepFormData } from "./SmallstepForm"; + +const createTestFormData = (overrides?: Partial) => ({ + name: "TEST_NAME", + scepURL: "https://test.com", + challengeURL: "https://test.com/challenge", + username: "testuser", + password: "testpassword", + ...overrides, +}); + +describe("SmallstepForm", () => { + it("render the custom button text", () => { + render( + + ); + + expect(screen.getByRole("button", { name: "Submit" })).toBeVisible(); + }); + + it("enables submission depending on the form validation", async () => { + const testData = createTestFormData(); + render( + + ); + + // data is valid, so submit should be enabled + expect(screen.getByRole("button", { name: "Submit" })).toBeEnabled(); + }); + + it("disables submission when form is invalid", async () => { + const testData = createTestFormData(); + // make name invalid by setting it to an empty string + testData.name = ""; + render( + + ); + // name is required, so submit should be disabled + expect(screen.getByRole("button", { name: "Submit" })).toBeDisabled(); + }); + + it("disables submit when isSubmitting is set to true", () => { + render( + + ); + + expect(screen.getByRole("button", { name: "Submit" })).toBeDisabled(); + }); + + it("submit button is disabled if isDirty is false", () => { + render( + + ); + + expect(screen.getByRole("button", { name: "Submit" })).toBeDisabled(); + }); + + it("submit button is enabled if isDirty", () => { + render( + + ); + + expect(screen.getByRole("button", { name: "Submit" })).toBeEnabled(); + }); +}); diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/SmallstepForm/SmallstepForm.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/SmallstepForm/SmallstepForm.tsx new file mode 100644 index 0000000000..af6c968f49 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/SmallstepForm/SmallstepForm.tsx @@ -0,0 +1,151 @@ +import React, { useMemo } from "react"; + +import { ICertificateAuthorityPartial } from "interfaces/certificates"; + +// @ts-ignore +import InputField from "components/forms/fields/InputField"; +import Button from "components/buttons/Button"; +import TooltipWrapper from "components/TooltipWrapper"; + +import { generateFormValidations, validateFormData } from "./helpers"; + +const baseClass = "smallstep-form"; + +export interface ISmallstepFormData { + name: string; + scepURL: string; + challengeURL: string; + username: string; + password: string; +} + +interface ISmallstepFormProps { + certAuthorities?: ICertificateAuthorityPartial[]; + formData: ISmallstepFormData; + submitBtnText: string; + isSubmitting: boolean; + isEditing?: boolean; + isDirty?: boolean; + onChange: (update: { name: string; value: string }) => void; + onSubmit: () => void; + onCancel: () => void; +} + +const SmallstepForm = ({ + certAuthorities, + formData, + submitBtnText, + isSubmitting, + isEditing = false, + isDirty = true, + onChange, + onSubmit, + onCancel, +}: ISmallstepFormProps) => { + const validationsConfig = useMemo(() => { + return generateFormValidations(certAuthorities ?? [], isEditing); + }, [certAuthorities, isEditing]); + + const validations = useMemo(() => { + return validateFormData(formData, validationsConfig); + }, [formData, validationsConfig]); + + const { name, scepURL, challengeURL, username, password } = formData; + + const onSubmitForm = (evt: React.FormEvent) => { + evt.preventDefault(); + onSubmit(); + }; + + return ( +
+
+ + + + Smallstep calls this the SCEP Challenge URL. + + } + /> + + Smallstep calls this the{" "} + Challenge Basic Authentication Username. + + } + /> + + Smallstep calls this the{" "} + Challenge Basic Authentication Password. + + } + /> +
+
+ + + + +
+
+ ); +}; + +export default SmallstepForm; diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/SmallstepForm/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/SmallstepForm/_styles.scss new file mode 100644 index 0000000000..7826e8d981 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/SmallstepForm/_styles.scss @@ -0,0 +1,13 @@ +.smallstep-form { + &__fields { + display: flex; + flex-direction: column; + gap: $pad-large; + } + + &__cta { + display: flex; + flex-direction: row-reverse; + gap: $pad-medium; + } +} diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/SmallstepForm/helpers.ts b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/SmallstepForm/helpers.ts new file mode 100644 index 0000000000..fbc3ed6a9e --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/SmallstepForm/helpers.ts @@ -0,0 +1,169 @@ +import { ICertificateAuthorityPartial } from "interfaces/certificates"; + +import valid_url from "components/forms/validators/valid_url"; + +import { ISmallstepFormData } from "./SmallstepForm"; + +// TODO: create a validator abstraction for this and the other form validation files + +export interface ISmallstepFormValidation { + isValid: boolean; + name?: { isValid: boolean; message?: string }; + scepURL?: { isValid: boolean; message?: string }; + challengeURL?: { isValid: boolean; message?: string }; + username?: { isValid: boolean; message?: string }; + password?: { isValid: boolean; message?: string }; +} + +type IMessageFunc = (formData: ISmallstepFormData) => string; +type IValidationMessage = string | IMessageFunc; +type IFormValidationKey = keyof Omit; + +interface IValidation { + name: string; + isValid: (formData: ISmallstepFormData) => boolean; + message?: IValidationMessage; +} + +type IFormValidations = Record< + IFormValidationKey, + { validations: IValidation[] } +>; + +export const generateFormValidations = ( + smallstepIntegrations: ICertificateAuthorityPartial[], + isEditing: boolean +) => { + const FORM_VALIDATIONS: IFormValidations = { + name: { + validations: [ + { + name: "required", + isValid: (formData: ISmallstepFormData) => { + return formData.name.length > 0; + }, + }, + { + name: "invalidCharacters", + isValid: (formData: ISmallstepFormData) => { + return /^[a-zA-Z0-9_]+$/.test(formData.name); + }, + message: + "Invalid characters. Only letters, numbers and underscores allowed.", + }, + { + name: "unique", + isValid: (formData: ISmallstepFormData) => { + return ( + isEditing || + smallstepIntegrations.find( + (cert) => cert.name === formData.name + ) === undefined + ); + }, + message: "Name is already used by another custom SCEP CA.", + }, + ], + }, + scepURL: { + validations: [ + { + name: "required", + isValid: (formData: ISmallstepFormData) => { + return formData.scepURL.length > 0; + }, + }, + { + name: "validUrl", + isValid: (formData: ISmallstepFormData) => { + return valid_url({ url: formData.scepURL }); + }, + message: "Must be a valid URL.", + }, + ], + }, + challengeURL: { + validations: [ + { + name: "required", + isValid: (formData: ISmallstepFormData) => { + return formData.challengeURL.length > 0; + }, + }, + { + name: "validUrl", + isValid: (formData: ISmallstepFormData) => { + return valid_url({ url: formData.challengeURL }); + }, + message: "Must be a valid URL.", + }, + ], + }, + username: { + validations: [ + { + name: "required", + isValid: (formData: ISmallstepFormData) => { + return formData.username.length > 0; + }, + }, + ], + }, + password: { + validations: [ + { + name: "required", + isValid: (formData: ISmallstepFormData) => { + return formData.password.length > 0; + }, + }, + ], + }, + }; + + return FORM_VALIDATIONS; +}; + +const getErrorMessage = ( + formData: ISmallstepFormData, + message?: IValidationMessage +) => { + if (message === undefined || typeof message === "string") { + return message; + } + return message(formData); +}; + +// eslint-disable-next-line import/prefer-default-export +export const validateFormData = ( + formData: ISmallstepFormData, + validationConfig: IFormValidations +) => { + const formValidation: ISmallstepFormValidation = { + isValid: true, + }; + + Object.keys(validationConfig).forEach((key) => { + const objKey = key as keyof typeof validationConfig; + const failedValidation = validationConfig[objKey].validations.find( + (validation) => !validation.isValid(formData) + ); + + if (!failedValidation) { + formValidation[objKey] = { + isValid: true, + }; + } else { + formValidation.isValid = false; + formValidation[objKey] = { + isValid: false, + message: getErrorMessage(formData, failedValidation.message), + }; + } + + console.log("objKey", objKey); + console.log("formValidation", formValidation); + }); + + return formValidation; +}; diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/SmallstepForm/index.ts b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/SmallstepForm/index.ts new file mode 100644 index 0000000000..50168eeaa9 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/SmallstepForm/index.ts @@ -0,0 +1 @@ +export { default } from "./SmallstepForm"; diff --git a/frontend/services/entities/certificates.ts b/frontend/services/entities/certificates.ts index 02efe4cb47..bdfcaced27 100644 --- a/frontend/services/entities/certificates.ts +++ b/frontend/services/entities/certificates.ts @@ -7,6 +7,7 @@ import { ICertificatesDigicert, ICertificatesHydrant, ICertificatesNDES, + ICertificatesSmallstep, } from "interfaces/certificates"; type IGetCertAuthoritiesListResponse = { @@ -25,13 +26,15 @@ export type IAddCertAuthorityBody = | { digicert: ICertificatesDigicert } | { ndes_scep_proxy: ICertificatesNDES } | { custom_scep_proxy: ICertificatesCustomSCEP } - | { hydrant: ICertificatesHydrant }; + | { hydrant: ICertificatesHydrant } + | { smallstep: ICertificatesSmallstep }; export type IEditCertAuthorityBody = | { digicert: Partial } | { ndes_scep_proxy: Partial } | { custom_scep_proxy: Partial } - | { hydrant: Partial }; + | { hydrant: Partial } + | { smallstep: Partial }; export default { getCertificateAuthoritiesList: (): Promise => {