feat(api): add ACM certificates to the cloud explorer
CI / checks (pull_request) Successful in 27m28s
CI / checks (pull_request) Successful in 27m28s
Request public certificates (optionally exportable) or import your own, see each domain's DNS validation records and what uses a certificate, download the certificate and chain, export an exportable one with its encrypted key, and revoke it. ListCertificates asks for every key type, since AWS otherwise leaves EC certificates out.
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import type {CloudProvider, FieldSchema, ServiceSchema, TableColumnSchema} from './types'
|
||||
|
||||
/** A domain name, optionally a wildcard (`*.example.com`), as RequestCertificate accepts it. */
|
||||
export const CERTIFICATE_DOMAIN_PATTERN = '^(\\*\\.)?(?=.{1,253}$)([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$'
|
||||
|
||||
/** The key algorithms RequestCertificate accepts for a public certificate. */
|
||||
export const CERTIFICATE_KEY_ALGORITHMS = ['RSA_2048', 'EC_prime256v1', 'EC_secp384r1'] as const
|
||||
export const CERTIFICATE_VALIDATION_METHODS = ['DNS', 'EMAIL'] as const
|
||||
/** RevokeCertificate's reasons, as the API names them. */
|
||||
export const CERTIFICATE_REVOCATION_REASONS = [
|
||||
'UNSPECIFIED',
|
||||
'KEY_COMPROMISE',
|
||||
'CA_COMPROMISE',
|
||||
'AFFILIATION_CHANGED',
|
||||
'SUPERSEDED',
|
||||
'CESSATION_OF_OPERATION',
|
||||
'PRIVILEGE_WITHDRAWN',
|
||||
] as const
|
||||
|
||||
const certificateColumns: TableColumnSchema[] = [
|
||||
{name: 'name', label: 'Domain name'},
|
||||
{name: 'status', label: 'Status', format: 'badge'},
|
||||
{name: 'certificateType', label: 'Type', path: 'metadata.certificateType'},
|
||||
{name: 'inUse', label: 'In use', path: 'metadata.inUse'},
|
||||
{name: 'keyAlgorithm', label: 'Key algorithm', path: 'metadata.keyAlgorithm'},
|
||||
{name: 'notAfter', label: 'Expires', path: 'metadata.notAfter', format: 'datetime', emptyText: '—'},
|
||||
{name: 'id', label: 'Certificate ID', format: 'code'},
|
||||
]
|
||||
|
||||
const certificateFilters: FieldSchema[] = [{name: 'search', label: 'Search', type: 'text', required: false}]
|
||||
|
||||
const requested = {field: 'source', values: ['request']}
|
||||
const imported = {field: 'source', values: ['import']}
|
||||
|
||||
export function awsCertificatesSchema(): ServiceSchema {
|
||||
return {
|
||||
cloud: 'aws',
|
||||
service: 'certificates',
|
||||
displayName: 'AWS Certificate Manager',
|
||||
fields: [
|
||||
{
|
||||
name: 'source',
|
||||
label: 'Certificate type',
|
||||
type: 'select',
|
||||
required: true,
|
||||
defaultValue: 'request',
|
||||
options: [
|
||||
{label: 'Request a public certificate', value: 'request'},
|
||||
{label: 'Import a certificate', value: 'import'},
|
||||
],
|
||||
span: true,
|
||||
},
|
||||
{
|
||||
name: 'domainName',
|
||||
label: 'Domain name',
|
||||
type: 'text',
|
||||
required: false,
|
||||
requiredWhen: {field: 'source', equals: 'request'},
|
||||
visibleWhen: requested,
|
||||
description: 'For example notes.thecloud.localhost, or *.thecloud.localhost for every subdomain. TheCloud signs it with its own local certificate authority.',
|
||||
validation: {pattern: CERTIFICATE_DOMAIN_PATTERN, message: 'Use a domain name such as example.com or *.example.com'},
|
||||
},
|
||||
{
|
||||
name: 'subjectAlternativeNames',
|
||||
label: 'Additional names',
|
||||
type: 'textarea',
|
||||
required: false,
|
||||
visibleWhen: requested,
|
||||
description: 'Other names the certificate covers, one per line.',
|
||||
},
|
||||
{
|
||||
name: 'validationMethod',
|
||||
label: 'Validation method',
|
||||
type: 'select',
|
||||
required: false,
|
||||
defaultValue: 'DNS',
|
||||
visibleWhen: requested,
|
||||
description: 'How ACM proves you own the domain. TheCloud issues the certificate without checking, and still shows the DNS records AWS would ask for.',
|
||||
options: CERTIFICATE_VALIDATION_METHODS.map((value) => ({label: value, value})),
|
||||
},
|
||||
{
|
||||
name: 'keyAlgorithm',
|
||||
label: 'Key algorithm',
|
||||
type: 'select',
|
||||
required: false,
|
||||
defaultValue: 'RSA_2048',
|
||||
visibleWhen: requested,
|
||||
options: CERTIFICATE_KEY_ALGORITHMS.map((value) => ({label: value.replace('_', ' '), value})),
|
||||
},
|
||||
{
|
||||
name: 'export',
|
||||
label: 'Exportable',
|
||||
type: 'select',
|
||||
required: false,
|
||||
defaultValue: 'DISABLED',
|
||||
visibleWhen: requested,
|
||||
description: 'An exportable certificate can be downloaded with its private key, to use on servers outside AWS. This is chosen once and cannot be changed later.',
|
||||
options: [
|
||||
{label: 'No', value: 'DISABLED'},
|
||||
{label: 'Yes, allow export', value: 'ENABLED'},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'certificate',
|
||||
label: 'Certificate body (PEM)',
|
||||
type: 'textarea',
|
||||
required: false,
|
||||
requiredWhen: {field: 'source', equals: 'import'},
|
||||
visibleWhen: imported,
|
||||
span: true,
|
||||
description: 'Starts with -----BEGIN CERTIFICATE-----.',
|
||||
},
|
||||
{
|
||||
name: 'privateKey',
|
||||
label: 'Private key (PEM)',
|
||||
type: 'textarea',
|
||||
required: false,
|
||||
requiredWhen: {field: 'source', equals: 'import'},
|
||||
visibleWhen: imported,
|
||||
span: true,
|
||||
description: 'The unencrypted key that matches the certificate.',
|
||||
},
|
||||
{
|
||||
name: 'certificateChain',
|
||||
label: 'Certificate chain (PEM)',
|
||||
type: 'textarea',
|
||||
required: false,
|
||||
visibleWhen: imported,
|
||||
span: true,
|
||||
description: 'The intermediate certificates, if the certificate is not self-signed.',
|
||||
},
|
||||
],
|
||||
actions: ['list', 'create', 'delete', 'inspect'],
|
||||
capabilities: {
|
||||
resourceActions: [
|
||||
{name: 'list', label: 'List certificates', enabled: true, status: 'available', runtimeRequired: true},
|
||||
{name: 'create', label: 'Request or import a certificate', enabled: true, status: 'available', runtimeRequired: true},
|
||||
{name: 'delete', label: 'Delete certificate', enabled: true, status: 'available', runtimeRequired: true},
|
||||
{name: 'inspect', label: 'Inspect certificate', enabled: true, status: 'available', runtimeRequired: true},
|
||||
],
|
||||
certificateActions: [
|
||||
{name: 'download', label: 'Download the certificate and chain', enabled: true, status: 'available', runtimeRequired: true},
|
||||
{name: 'export', label: 'Export with the private key', enabled: true, status: 'available', runtimeRequired: true},
|
||||
{name: 'revoke', label: 'Revoke certificate', enabled: true, status: 'available', runtimeRequired: true},
|
||||
],
|
||||
},
|
||||
filters: certificateFilters,
|
||||
columns: certificateColumns,
|
||||
}
|
||||
}
|
||||
|
||||
export function certificatesSchemaFor(cloud: CloudProvider): ServiceSchema | null {
|
||||
return cloud === 'aws' ? awsCertificatesSchema() : null
|
||||
}
|
||||
@@ -239,6 +239,13 @@ export const SERVICE_CATALOG = {
|
||||
// AWS retains its dedicated page while Azure and GCP use Cloud Explorer.
|
||||
routeByCloud: {aws: '/secretsmanager'},
|
||||
},
|
||||
certificates: {
|
||||
displayName: 'Certificates',
|
||||
displayNameByCloud: {aws: 'Certificate Manager'},
|
||||
iconKey: 'certificates',
|
||||
group: 'Security',
|
||||
order: 35,
|
||||
},
|
||||
cdn: {
|
||||
displayName: 'CDN',
|
||||
displayNameByCloud: {aws: 'CloudFront'},
|
||||
|
||||
@@ -186,6 +186,7 @@ export type UserPoolActionName =
|
||||
| 'describeClient'
|
||||
| 'updateClientOAuth'
|
||||
export type DistributionActionName = 'setEnabled' | 'createInvalidation'
|
||||
export type CertificateActionName = 'download' | 'export' | 'revoke'
|
||||
export type MonitoringActionName = 'listMetrics' | 'getMetricData' | 'alarmHistory' | 'setAlarmState' | 'setAlarmActions'
|
||||
export type HttpApiActionName =
|
||||
| 'describe'
|
||||
@@ -245,6 +246,7 @@ export interface ServiceSchema {
|
||||
kubernetesActions?: CapabilitySchema<KubernetesActionName>[]
|
||||
userPoolActions?: CapabilitySchema<UserPoolActionName>[]
|
||||
distributionActions?: CapabilitySchema<DistributionActionName>[]
|
||||
certificateActions?: CapabilitySchema<CertificateActionName>[]
|
||||
monitoringActions?: CapabilitySchema<MonitoringActionName>[]
|
||||
/** API Gateway v2 (HTTP and WebSocket APIs): what the routes, stages and security panel can do. */
|
||||
httpApiActions?: CapabilitySchema<HttpApiActionName>[]
|
||||
@@ -515,6 +517,26 @@ export interface AlarmHistoryItem {
|
||||
summary: string
|
||||
}
|
||||
|
||||
/** A certificate and its chain as PEM text; never the private key. */
|
||||
export interface CertificatePem {
|
||||
certificate: string
|
||||
certificateChain: string | null
|
||||
}
|
||||
|
||||
/** An exported certificate: the private key is PEM, encrypted with the caller's passphrase. */
|
||||
export interface ExportedCertificate extends CertificatePem {
|
||||
privateKey: string
|
||||
}
|
||||
|
||||
export type CertificateRevocationReason =
|
||||
| 'UNSPECIFIED'
|
||||
| 'KEY_COMPROMISE'
|
||||
| 'CA_COMPROMISE'
|
||||
| 'AFFILIATION_CHANGED'
|
||||
| 'SUPERSEDED'
|
||||
| 'CESSATION_OF_OPERATION'
|
||||
| 'PRIVILEGE_WITHDRAWN'
|
||||
|
||||
export interface DistributionInvalidation {
|
||||
id: string
|
||||
status: string
|
||||
@@ -1182,6 +1204,9 @@ export interface CloudServiceAdapter {
|
||||
setAlarmState?(alarmName: string, state: AlarmStateValue, reason: string): Promise<void>
|
||||
setAlarmActionsEnabled?(alarmName: string, enabled: boolean): Promise<void>
|
||||
createDistributionInvalidation?(distributionId: string, paths: string[]): Promise<DistributionInvalidation>
|
||||
getCertificatePem?(certificateId: string): Promise<CertificatePem>
|
||||
exportCertificate?(certificateId: string, passphrase: string): Promise<ExportedCertificate>
|
||||
revokeCertificate?(certificateId: string, reason: CertificateRevocationReason): Promise<CloudResource>
|
||||
updateUserPoolClientOAuth?(poolId: string, clientId: string, input: UpdateUserPoolClientOAuthInput): Promise<UserPoolClientSettings>
|
||||
/** Clears the provider's locally captured email inbox, if it exposes one. */
|
||||
clearEmailInbox?(): Promise<void>
|
||||
|
||||
Reference in New Issue
Block a user