mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 18:17:55 +00:00
UI: pki import issuer (#18634)
* create pki ca import component * add serial number to cert parser * convert to ts * remove comments * reset yarn.lock * fixed yarn lock * fix comment * add declaration for base cert
This commit is contained in:
@@ -21,6 +21,22 @@ export default class PkiIssuerAdapter extends ApplicationAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
createRecord(store, type, snapshot) {
|
||||
const { record, adapterOptions } = snapshot;
|
||||
let url = this.urlForQuery(record.backend);
|
||||
if (adapterOptions.import) {
|
||||
url = `${url}/import/bundle`;
|
||||
} else {
|
||||
// TODO WIP generate root or intermediate CSR actions from issuers index page
|
||||
// certType = 'root' || 'intermediate', // record.type is internal or exported
|
||||
// url = ` ${url}/generate/${certType}/${record.type}`;
|
||||
throw new Error('createRecord method in adapters/pki/issuer.js is incomplete.');
|
||||
}
|
||||
return this.ajax(url, 'POST', { data: this.serialize(snapshot) }).then((resp) => {
|
||||
return resp;
|
||||
});
|
||||
}
|
||||
|
||||
query(store, type, query) {
|
||||
return this.ajax(this.urlForQuery(query.backend), 'GET', this.optionsForQuery());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { helper } from '@ember/component/helper';
|
||||
import * as asn1js from 'asn1js';
|
||||
import { fromBase64, stringToArrayBuffer } from 'pvutils';
|
||||
import { Convert } from 'pvtsutils';
|
||||
import { Certificate } from 'pkijs';
|
||||
|
||||
export function parseCertificate(certificateContent) {
|
||||
@@ -43,9 +44,13 @@ export function parseCertificate(certificateContent) {
|
||||
// field themselves are Time values.
|
||||
const expiryDate = cert?.notAfter?.value;
|
||||
const issueDate = cert?.notBefore?.value;
|
||||
const serialNumber = Convert.ToHex(cert.serialNumber.valueBlock.valueHex)
|
||||
.match(/.{1,2}/g)
|
||||
.join(':');
|
||||
return {
|
||||
can_parse: true,
|
||||
common_name: commonName,
|
||||
serial_number: serialNumber,
|
||||
expiry_date: expiryDate,
|
||||
issue_date: issueDate,
|
||||
not_valid_after: expiryDate.valueOf(),
|
||||
|
||||
@@ -42,6 +42,11 @@ export default class PkiCertificateBaseModel extends Model {
|
||||
@attr('date') notValidAfter;
|
||||
@attr('date') notValidBefore;
|
||||
|
||||
// For importing
|
||||
@attr('string') pemBundle;
|
||||
@attr importedIssuers;
|
||||
@attr importedKeys;
|
||||
|
||||
@lazyCapabilities(apiPath`${'backend'}/revoke`, 'backend') revokePath;
|
||||
get canRevoke() {
|
||||
return this.revokePath.get('isLoading') || this.revokePath.get('canCreate') !== false;
|
||||
|
||||
36
ui/lib/pki/addon/components/pki-ca-certificate-import.hbs
Normal file
36
ui/lib/pki/addon/components/pki-ca-certificate-import.hbs
Normal file
@@ -0,0 +1,36 @@
|
||||
<div class="field">
|
||||
<div class="form-section">
|
||||
<label class="title has-padding-top is-5">
|
||||
Certificate parameters
|
||||
</label>
|
||||
<form {{on "submit" (perform this.submitForm)}} data-test-pki-ca-cert-import-form>
|
||||
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
|
||||
<div class="box is-sideless is-fullwidth is-marginless has-top-padding-l">
|
||||
<TextFile @onChange={{this.onFileUploaded}} @label="PEM Bundle" />
|
||||
<p class="has-top-margin-m has-bottom-margin-l">
|
||||
Issuer URLs (Issuing certificates, CRL distribution points, OCSP servers, and delta CRL URLs) can be specified by
|
||||
editing the individual issuer once it is uploaded to Vault.
|
||||
</p>
|
||||
</div>
|
||||
<div class="has-top-padding-s">
|
||||
<button
|
||||
type="submit"
|
||||
class="button is-primary {{if this.submitForm.isRunning 'is-loading'}}"
|
||||
disabled={{this.submitForm.isRunning}}
|
||||
data-test-pki-ca-cert-import
|
||||
>
|
||||
Import issuer
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button has-left-margin-s"
|
||||
disabled={{this.submitForm.isRunning}}
|
||||
{{on "click" this.cancel}}
|
||||
data-test-pki-ca-cert-cancel
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
60
ui/lib/pki/addon/components/pki-ca-certificate-import.ts
Normal file
60
ui/lib/pki/addon/components/pki-ca-certificate-import.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { action } from '@ember/object';
|
||||
import Component from '@glimmer/component';
|
||||
import FlashMessageService from 'vault/services/flash-messages';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
import PkiBaseCertificateModel from 'vault/models/pki/certificate/base';
|
||||
|
||||
/**
|
||||
* @module PkiCaCertificateImport
|
||||
* PkiCaCertificateImport components are used to import PKI CA certificates and keys via pem_bundle.
|
||||
* https://github.com/hashicorp/vault/blob/main/website/content/api-docs/secret/pki.mdx#import-ca-certificates-and-keys
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <PkiCaCertificateImport @model={{this.model}} />
|
||||
* ```
|
||||
*
|
||||
* @param {Object} model - certificate model from route
|
||||
* @callback onCancel - Callback triggered when cancel button is clicked.
|
||||
* @callback onSubmit - Callback triggered on submit success.
|
||||
*/
|
||||
|
||||
interface Args {
|
||||
onSave: CallableFunction;
|
||||
onCancel: CallableFunction;
|
||||
model: PkiBaseCertificateModel;
|
||||
}
|
||||
|
||||
export default class PkiCaCertificateImport extends Component<Args> {
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
|
||||
@tracked errorBanner = '';
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*submitForm(event: Event) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
yield this.args.model.save({ adapterOptions: { import: true } });
|
||||
this.flashMessages.success('Successfully imported certificate.');
|
||||
this.args.onSave();
|
||||
} catch (error) {
|
||||
this.errorBanner = errorMessage(error);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onFileUploaded({ value }: { value: string }) {
|
||||
this.args.model.pemBundle = value;
|
||||
}
|
||||
|
||||
@action
|
||||
cancel() {
|
||||
this.args.model.unloadRecord();
|
||||
this.args.onCancel();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,15 @@
|
||||
import PkiIssuersIndexRoute from '.';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { withConfirmLeave } from 'core/decorators/confirm-leave';
|
||||
|
||||
@withConfirmLeave()
|
||||
export default class PkiIssuersImportRoute extends PkiIssuersIndexRoute {
|
||||
@service store;
|
||||
|
||||
model() {
|
||||
return this.store.createRecord('pki/issuer');
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
controller.breadcrumbs.push({ label: 'import' });
|
||||
|
||||
@@ -1,2 +1,16 @@
|
||||
{{! https://github.com/hashicorp/vault/blob/main/website/content/api-docs/secret/pki.mdx#import-ca-certificates-and-keys }}
|
||||
route: issuers.import POST /pki/issuers/import/bundle POST /pki/issuers/import/cert
|
||||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-pki-issuer-page-title>
|
||||
Import a CA
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<PkiCaCertificateImport
|
||||
@model={{this.model}}
|
||||
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
|
||||
/>
|
||||
@@ -254,6 +254,7 @@
|
||||
"highlight.js": "^10.4.1",
|
||||
"js-yaml": "^3.13.1",
|
||||
"lodash": "^4.17.13",
|
||||
"node-notifier": "^8.0.1"
|
||||
"node-notifier": "^8.0.1",
|
||||
"pvtsutils": "^1.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
141
ui/tests/integration/components/pki/pki-issuer-import-test.js
Normal file
141
ui/tests/integration/components/pki/pki-issuer-import-test.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, click, fillIn } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
|
||||
module('Integration | Component | pki issuer import', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
setupEngine(hooks, 'pki'); // https://github.com/ember-engines/ember-engines/pull/653
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.model = this.store.createRecord('pki/issuer');
|
||||
this.backend = 'pki-test';
|
||||
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
|
||||
this.secretMountPath.currentPath = this.backend;
|
||||
this.pemBundle = `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDRTCCAi2gAwIBAgIUdKagCL6TnN5xLkwhPbNY8JEcY0YwDQYJKoZIhvcNAQEL
|
||||
BQAwGzEZMBcGA1UEAxMQd3d3LnRlc3QtaW50LmNvbTAeFw0yMzAxMDkxOTA1NTBa
|
||||
Fw0yMzAyMTAxOTA2MjBaMBsxGTAXBgNVBAMTEHd3dy50ZXN0LWludC5jb20wggEi
|
||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCfd5o9JfyRAXH+E1vE2U0xjSqs
|
||||
A/cxDqsDXRHBnNJvzAa+7gPKXCDQZbr6chjxLXpP6Bv2/O+dZHq1fo/f6q9PDDGW
|
||||
JYIluwbACpe7W1UB7q9xFkZg85yQsNYokGZlwv/AMGpFBxDwVlNGL+4fxvFTv7uF
|
||||
mIlDzSIPrzByyCrqAFMNNqNwlAerDt/C6DMZae/rTGXIXsTfUpxPy21bzkeA+70I
|
||||
YCV1ffK8UnAeBYNUJ+v8+XgTQ5KhRyQ+fscUkO3T2s6f3O9Q2sWxswkf2YmZB+V1
|
||||
cTZ5w6hqiuFdBXz7GRnACi1/gbWbaExQTJRplArFwIHka7dqJh8tYkXDjai3AgMB
|
||||
AAGjgYAwfjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
|
||||
FgQU68/xXIgvsleKkuA8clK/6YslB/IwHwYDVR0jBBgwFoAU68/xXIgvsleKkuA8
|
||||
clK/6YslB/IwGwYDVR0RBBQwEoIQd3d3LnRlc3QtaW50LmNvbTANBgkqhkiG9w0B
|
||||
AQsFAAOCAQEAWSff0BH3SJv/XqwN/flqc1CVzOios72/IJ+KBBv0AzFCZ8wJPi+c
|
||||
hH1bw7tqi01Bgh595TctogDFN1b6pjN+jrlIP4N+FF9Moj79Q+jHQMnuJomyPuI7
|
||||
i07vqUcxgSmvEBBWOWS+/vxe6TfWDg18nyPf127CWQN8IHTo1f/GavX+XmRve6XT
|
||||
EWoqcQshEk9i87oqCbaT7B40jgjTAd1r4Cc6P4s1fAGPt9e9eqMj13kTyVDNuCoD
|
||||
FSZYalrlkASpg+c9oDQIh2MikGQINXHv/zIEHOW93siKMWeA4ni6phHtMg/p5eJt
|
||||
SxnVZsSzj8QLy2uwX1AADR0QUvJzMxptyA==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAn3eaPSX8kQFx/hNbxNlNMY0qrAP3MQ6rA10RwZzSb8wGvu4D
|
||||
ylwg0GW6+nIY8S16T+gb9vzvnWR6tX6P3+qvTwwxliWCJbsGwAqXu1tVAe6vcRZG
|
||||
YPOckLDWKJBmZcL/wDBqRQcQ8FZTRi/uH8bxU7+7hZiJQ80iD68wcsgq6gBTDTaj
|
||||
cJQHqw7fwugzGWnv60xlyF7E31KcT8ttW85HgPu9CGAldX3yvFJwHgWDVCfr/Pl4
|
||||
E0OSoUckPn7HFJDt09rOn9zvUNrFsbMJH9mJmQfldXE2ecOoaorhXQV8+xkZwAot
|
||||
f4G1m2hMUEyUaZQKxcCB5Gu3aiYfLWJFw42otwIDAQABAoIBADC+vZ4Ne4vTtkWl
|
||||
Izsj9Y29Chs0xx3uzuWjUGcvib/0zOcWGICF8t3hCuu9btRiQ24jlFDGdnRVH5FV
|
||||
E6OtuFLgdlPgOU1RQzn2wvTZcT26+VQHLBI8xVIRTBVwNmzK06Sq6AEbrNjaenAM
|
||||
/KwoAuLHzAmFXAgmr0++DIA5oayPWyi5IoyFO7EoRv79Xz5LWfu5j8CKOFXmI5MT
|
||||
vEVYM6Gb2xHRa2Ng0SJ4VzwC09GcXlHKRAz+CubJuncvjbcM/EryvexozKkUq4XA
|
||||
KqGr9xxdZ4XDlo3Rj9S9P9JaOin0I1mwwz6p+iwMF0zr+/ldjE4oPBdB1PUgSJ7j
|
||||
2CZcS1kCgYEAwIZ3UsMIXqkMlkMz/7nu2sqzV3EgQjY5QRoz98ligKg4fhYKz+K4
|
||||
yXvJrRyLkwEBaPdLppCZbs4xsuuv3jiqUHV5n7sfpUA5HVKkKh6XY7jnszbqV732
|
||||
iB1mQVEjzM92/amew2hDKLGQDW0nglrg6uV+bx0Lnp6Glahr8NOAyk0CgYEA1Ar3
|
||||
jTqTkU+NQX7utlxx0HPVL//JH/erp/Gnq9fN8dZhK/yjwX5savUlNHpgePoXf1pE
|
||||
lgi21/INQsvp7O2AUKuj96k+jBHQ0SS58AQGFv8iNDkLE57N74vCO6+Xdi1rHj/Y
|
||||
7jglr00box/7SOmvb4SZz2o0jm0Ejsg2M0aBuRMCgYEAgTB6F34qOqMDgD1eQka5
|
||||
QfXs/Es8E1Ihf08e+jIXuC+poOoXnUINL56ySUizXBS7pnzzNbUoUFNqxB4laF/r
|
||||
4YvC7m15ocED0mpnIKBghBlK2VaLUA93xAS+XiwdcszwkuzkTUnEbyUfffL2JSHo
|
||||
dZdEDTmXV3wW4Ywfyn2Sma0CgYAeNNG/FLEg6iw9QE/ROqob/+RGyjFklGunqQ0x
|
||||
tbRo1xlQotTRI6leMz3xk91aXoYqZjmPBf7GFH0/Hr1cOxkkZM8e4MVAPul4Ybr7
|
||||
LheP/xhoSBgD24OKtGYfCoyRETdJP98vUGBN8LYXLt8lK+UKBeHDYmXKRE156ZuP
|
||||
AmRIcQKBgFvp+xMoyAsBeOlTjVDZ0mTnFh1yp8f7N3yXdHPpFShwjXjlqLmLO5RH
|
||||
mZAvaH0Ux/wCfvwHhdC46jBrs9S4zLBvj3+44NYOzvz2dBWP/5MuXgzFe30h9Yd0
|
||||
zUlyEaWm0jY2Ylzax8ECKRL0td2bv36vxOYtTax8MSB15szsnPJ+
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`;
|
||||
});
|
||||
|
||||
test('it renders import and updates model', async function (assert) {
|
||||
assert.expect(3);
|
||||
await render(
|
||||
hbs`
|
||||
<PkiCaCertificateImport
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
assert.dom('[data-test-pki-ca-cert-import-form]').exists('renders form');
|
||||
assert.dom('[data-test-component="text-file"]').exists('renders text file input');
|
||||
await click('[data-test-text-toggle]');
|
||||
await fillIn('[data-test-text-file-textarea]', this.pemBundle);
|
||||
assert.strictEqual(this.model.pemBundle, this.pemBundle);
|
||||
});
|
||||
|
||||
test('it sends correct payload to import endpoint', async function (assert) {
|
||||
assert.expect(3);
|
||||
this.server.post(`/${this.backend}/issuers/import/bundle`, (schema, req) => {
|
||||
assert.ok(true, 'Request made to the correct endpoint to import issuer');
|
||||
const request = JSON.parse(req.requestBody);
|
||||
assert.propEqual(
|
||||
request,
|
||||
{
|
||||
pem_bundle: `${this.pemBundle}`,
|
||||
},
|
||||
'sends params in correct type'
|
||||
);
|
||||
return {};
|
||||
});
|
||||
|
||||
this.onSave = () => assert.ok(true, 'onSave callback fires on save success');
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<PkiCaCertificateImport
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
await click('[data-test-text-toggle]');
|
||||
await fillIn('[data-test-text-file-textarea]', this.pemBundle);
|
||||
assert.strictEqual(this.model.pemBundle, this.pemBundle);
|
||||
await click('[data-test-pki-ca-cert-import]');
|
||||
});
|
||||
|
||||
test('it should unload record on cancel', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.onCancel = () => assert.ok(true, 'onCancel callback fires');
|
||||
await render(
|
||||
hbs`
|
||||
<PkiCaCertificateImport
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
await click('[data-test-pki-ca-cert-cancel]');
|
||||
assert.true(this.model.isDestroyed, 'new model is unloaded on cancel');
|
||||
});
|
||||
});
|
||||
5
ui/types/generate-declaration.md
Normal file
5
ui/types/generate-declaration.md
Normal file
@@ -0,0 +1,5 @@
|
||||
To generate a declaration file run `yarn tsc <javascript file to declare> --declaration --allowJs --emitDeclarationOnly --outDir <type file location>`
|
||||
|
||||
For example, the following command generates a declaration file called base.d.ts for the pki certificate base.js model:
|
||||
|
||||
`yarn tsc ./app/models/pki/certificate/base.js --declaration --allowJs --emitDeclarationOnly --outDir types/vault/models/pki/certificate`
|
||||
22
ui/types/vault/models/pki/certificate/base.d.ts
vendored
Normal file
22
ui/types/vault/models/pki/certificate/base.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import Model from '@ember-data/model';
|
||||
export default class PkiCertificateBaseModel extends Model {
|
||||
secretMountPath: class;
|
||||
get useOpenAPI(): boolean;
|
||||
get backend(): string;
|
||||
getHelpUrl(): void;
|
||||
commonName: string;
|
||||
caChain: string;
|
||||
certificate: string;
|
||||
expiration: number;
|
||||
issuingCa: string;
|
||||
privateKey: string;
|
||||
privateKeyType: string;
|
||||
serialNumber: string;
|
||||
notValidAfter: date;
|
||||
notValidBefore: date;
|
||||
pemBundle: string;
|
||||
importedIssuers: string[];
|
||||
importedKeys: string[];
|
||||
revokePath: string;
|
||||
get canRevoke(): boolean;
|
||||
}
|
||||
@@ -15994,6 +15994,13 @@ punycode@^2.1.0, punycode@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||
|
||||
pvtsutils@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.2.tgz#9f8570d132cdd3c27ab7d51a2799239bf8d8d5de"
|
||||
integrity sha512-+Ipe2iNUyrZz+8K/2IOo+kKikdtfhRKzNpQbruF2URmqPtoqAs8g3xS7TJvFF2GcPXjh7DkqMnpVveRFq4PgEQ==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
pvutils@^1.0.17, pvutils@latest:
|
||||
version "1.0.17"
|
||||
resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.0.17.tgz#ade3c74dfe7178944fe44806626bd2e249d996bf"
|
||||
@@ -18218,7 +18225,7 @@ tslib@^2.0.3:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
|
||||
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
|
||||
|
||||
tslib@^2.1.0:
|
||||
tslib@^2.1.0, tslib@^2.4.0:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
|
||||
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
|
||||
|
||||
Reference in New Issue
Block a user