/** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: BUSL-1.1 */ import Model, { attr } from '@ember-data/model'; import { inject as service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; import { withFormFields } from 'vault/decorators/model-form-fields'; import { withModelValidations } from 'vault/decorators/model-validations'; const validations = { type: [{ type: 'presence', message: 'Type is required.' }], commonName: [{ type: 'presence', message: 'Common name is required.' }], issuerName: [ { validator(model) { if ( (model.actionType === 'generate-root' || model.actionType === 'rotate-root') && model.issuerName === 'default' ) return false; return true; }, message: `Issuer name must be unique across all issuers and not be the reserved value 'default'.`, }, ], keyName: [ { validator(model) { if (model.keyName === 'default') return false; return true; }, message: `Key name cannot be the reserved value 'default'`, }, ], }; /** * This model maps to multiple PKI endpoints, specifically the ones that make up the * configuration/create workflow. These endpoints also share a nontypical behavior in that * a POST request to the endpoints don't necessarily result in a single entity created -- * depending on the inputs, some number of issuers, keys, and certificates can be created * from the API. */ @withModelValidations(validations) @withFormFields() export default class PkiActionModel extends Model { @service secretMountPath; @tracked actionType; // used to toggle between different form fields when creating configuration /* actionType import */ @attr('string') pemBundle; // parsed attrs from parse-pki-cert util if certificate on response @attr parsedCertificate; // readonly attrs returned after importing @attr importedIssuers; @attr importedKeys; @attr mapping; @attr('string', { readOnly: true, isCertificate: true }) certificate; /* actionType generate-root */ // readonly attrs returned right after root generation @attr serialNumber; @attr('string', { label: 'Issuing CA', readOnly: true, isCertificate: true }) issuingCa; // end of readonly @attr('string', { possibleValues: ['exported', 'internal', 'existing', 'kms'], noDefault: true, }) type; @attr('string') issuerName; @attr('string') keyName; @attr('string', { defaultValue: 'default', label: 'Key reference', }) keyRef; // type=existing only @attr('string') commonName; // REQUIRED @attr('string', { label: 'Subject Alternative Names (SANs)', editType: 'stringArray', }) altNames; @attr('string', { label: 'IP Subject Alternative Names (IP SANs)', editType: 'stringArray', }) ipSans; @attr('string', { label: 'URI Subject Alternative Names (URI SANs)', editType: 'stringArray', }) uriSans; @attr('string', { label: 'Other SANs', editType: 'stringArray', }) otherSans; @attr('string', { defaultValue: 'pem', possibleValues: ['pem', 'der', 'pem_bundle'], }) format; @attr('string', { defaultValue: 'der', possibleValues: ['der', 'pkcs8'], }) privateKeyFormat; @attr('string', { defaultValue: 'rsa', possibleValues: ['rsa', 'ed25519', 'ec'], }) keyType; @attr('string', { defaultValue: '0', // options management happens in pki-key-parameters }) keyBits; @attr('number', { defaultValue: -1, }) maxPathLength; @attr('boolean', { label: 'Exclude common name from SANs', subText: 'If checked, the common name will not be included in DNS or Email Subject Alternate Names. This is useful if the CN is a human-readable identifier, not a hostname or email address.', defaultValue: false, }) excludeCnFromSans; @attr('string', { label: 'Permitted DNS domains', }) permittedDnsDomains; @attr('string', { label: 'Organizational Units (OU)', subText: 'A list of allowed serial numbers to be requested during certificate issuance. Shell-style globbing is supported. If empty, custom-specified serial numbers will be forbidden.', editType: 'stringArray', }) ou; @attr({ editType: 'stringArray' }) organization; @attr({ editType: 'stringArray' }) country; @attr({ editType: 'stringArray' }) locality; @attr({ editType: 'stringArray' }) province; @attr({ editType: 'stringArray' }) streetAddress; @attr({ editType: 'stringArray' }) postalCode; @attr('string', { subText: "Specifies the requested Subject's named Serial Number value. This has no impact on the Certificate's serial number randomly generated by Vault.", }) subjectSerialNumber; // this is different from the number (16:5e:a0...) randomly generated by Vault // https://developer.hashicorp.com/vault/api-docs/secret/pki#serial_number @attr('boolean', { subText: 'Whether to add a Basic Constraints extension with CA: true.', }) addBasicConstraints; @attr({ label: 'Backdate validity', detailsLabel: 'Issued certificate backdating', helperTextDisabled: 'Vault will use the default value, 30s', helperTextEnabled: 'Also called the not_before_duration property. Allows certificates to be valid for a certain time period before now. This is useful to correct clock misalignment on various systems when setting up your CA.', editType: 'ttl', defaultValue: '30s', }) notBeforeDuration; @attr('string') managedKeyName; @attr('string', { label: 'Managed key UUID', }) managedKeyId; @attr({ label: 'Not valid after', detailsLabel: 'Issued certificates expire after', subText: 'The time after which this certificate will no longer be valid. This can be a TTL (a range of time from now) or a specific date.', editType: 'yield', }) customTtl; @attr('string') ttl; @attr('date') notAfter; @attr('string', { label: 'Issuer ID', readOnly: true, detailLinkTo: 'issuers.issuer.details' }) issuerId; // returned from generate-root action // For generating and signing a CSR @attr('string', { label: 'CSR', isCertificate: true }) csr; @attr caChain; @attr('string', { label: 'Key ID', detailLinkTo: 'keys.key.details' }) keyId; @attr('string', { isCertificate: true }) privateKey; @attr('string') privateKeyType; get backend() { return this.secretMountPath.currentPath; } // To determine which endpoint the config adapter should use, // we want to check capabilities on the newer endpoints (those // prefixed with "issuers") and use the old path as fallback // if user does not have permissions. @lazyCapabilities(apiPath`${'backend'}/issuers/import/bundle`, 'backend') importBundlePath; @lazyCapabilities(apiPath`${'backend'}/issuers/generate/root/${'type'}`, 'backend', 'type') generateIssuerRootPath; @lazyCapabilities(apiPath`${'backend'}/issuers/generate/intermediate/${'type'}`, 'backend', 'type') generateIssuerCsrPath; @lazyCapabilities(apiPath`${'backend'}/issuers/cross-sign`, 'backend') crossSignPath; get canImportBundle() { return this.importBundlePath.get('canCreate') === true; } get canGenerateIssuerRoot() { return this.generateIssuerRootPath.get('canCreate') === true; } get canGenerateIssuerIntermediate() { return this.generateIssuerCsrPath.get('canCreate') === true; } get canCrossSign() { return this.crossSignPath.get('canCreate') === true; } }