diff --git a/package-lock.json b/package-lock.json index b33b7e7e..6495ec5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "io.netfoundry.zac", - "version": "3.6.3", + "version": "3.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "io.netfoundry.zac", - "version": "3.6.3", + "version": "3.7.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -93,7 +93,7 @@ }, "dist/ziti-console-lib": { "name": "@openziti/ziti-console-lib", - "version": "0.6.5", + "version": "0.0.0-watch+1737585178432", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.0" diff --git a/projects/app-ziti-console/src/app/app-routing.module.ts b/projects/app-ziti-console/src/app/app-routing.module.ts index 595b45e5..1c451522 100644 --- a/projects/app-ziti-console/src/app/app-routing.module.ts +++ b/projects/app-ziti-console/src/app/app-routing.module.ts @@ -43,7 +43,10 @@ import { JwtSignersPageComponent, JwtSignerFormComponent, AuthPoliciesPageComponent, - AuthPolicyFormComponent + AuthPolicyFormComponent, + CertificateAuthoritiesPageComponent, + CertificateAuthorityFormComponent, + VerifyCertificateComponent } from "ziti-console-lib"; import {environment} from "./environments/environment"; import {URLS} from "./app-urls.constants"; @@ -250,8 +253,22 @@ const routes: Routes = [ }, { path: 'certificate-authorities', - component: ZacWrapperComponent, + component: CertificateAuthoritiesPageComponent, + canActivate: mapToCanActivate([AuthenticationGuard]), + runGuardsAndResolvers: 'always', + }, + { + path: 'certificate-authorities/:id', + component: CertificateAuthorityFormComponent, canActivate: mapToCanActivate([AuthenticationGuard]), + canDeactivate: [DeactivateGuardService], + runGuardsAndResolvers: 'always', + }, + { + path: 'certificate-authorities/:id/verify', + component: VerifyCertificateComponent, + canActivate: mapToCanActivate([AuthenticationGuard]), + canDeactivate: [DeactivateGuardService], runGuardsAndResolvers: 'always', }, { diff --git a/projects/app-ziti-console/src/app/app.component.scss b/projects/app-ziti-console/src/app/app.component.scss index dcca6638..dfe93be5 100644 --- a/projects/app-ziti-console/src/app/app.component.scss +++ b/projects/app-ziti-console/src/app/app.component.scss @@ -2,6 +2,7 @@ .main { display: flex; background-color: var(--navigation); + height: 100%; .modals { position: absolute; @@ -16,7 +17,7 @@ display: flex; margin: 0; padding: 0; - min-height: 100vh; + min-height: 100%; max-width: 23.75rem; background-color: var(--shaded); box-shadow: inset 0rem 0.25rem 1rem 0rem rgba(0, 0, 0, 0.08); diff --git a/projects/ziti-console-lib/src/lib/assets/fonts/icomoon.eot b/projects/ziti-console-lib/src/lib/assets/fonts/icomoon.eot new file mode 100644 index 00000000..fd0ecee5 Binary files /dev/null and b/projects/ziti-console-lib/src/lib/assets/fonts/icomoon.eot differ diff --git a/projects/ziti-console-lib/src/lib/assets/fonts/icomoon.svg b/projects/ziti-console-lib/src/lib/assets/fonts/icomoon.svg new file mode 100644 index 00000000..2321b8f3 --- /dev/null +++ b/projects/ziti-console-lib/src/lib/assets/fonts/icomoon.svg @@ -0,0 +1,118 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/projects/ziti-console-lib/src/lib/assets/fonts/icomoon.ttf b/projects/ziti-console-lib/src/lib/assets/fonts/icomoon.ttf new file mode 100644 index 00000000..fd44bd8d Binary files /dev/null and b/projects/ziti-console-lib/src/lib/assets/fonts/icomoon.ttf differ diff --git a/projects/ziti-console-lib/src/lib/assets/fonts/icomoon.woff b/projects/ziti-console-lib/src/lib/assets/fonts/icomoon.woff new file mode 100644 index 00000000..229c577b Binary files /dev/null and b/projects/ziti-console-lib/src/lib/assets/fonts/icomoon.woff differ diff --git a/projects/ziti-console-lib/src/lib/assets/fonts/icomoon.woff2 b/projects/ziti-console-lib/src/lib/assets/fonts/icomoon.woff2 new file mode 100644 index 00000000..56350bf2 Binary files /dev/null and b/projects/ziti-console-lib/src/lib/assets/fonts/icomoon.woff2 differ diff --git a/projects/ziti-console-lib/src/lib/assets/styles/icons.css b/projects/ziti-console-lib/src/lib/assets/styles/icons.css index 758e5d9c..f6aac06e 100644 --- a/projects/ziti-console-lib/src/lib/assets/styles/icons.css +++ b/projects/ziti-console-lib/src/lib/assets/styles/icons.css @@ -11,6 +11,19 @@ font-display: block; } +@font-face { + font-family: 'icomoon'; + src: url('../fonts/icomoon.eot?l2gtmj'); + src: url('../fonts/icomoon.eot?l2gtmj#iefix') format('embedded-opentype'), + url('../fonts/icomoon.woff2?l2gtmj') format('woff2'), + url('../fonts/icomoon.ttf?l2gtmj') format('truetype'), + url('../fonts/icomoon.woff?l2gtmj') format('woff'), + url('../fonts/icomoon.svg?l2gtmj#icomoon') format('svg'); + font-weight: normal; + font-style: normal; + font-display: block; +} + [class^="icon-"], [class*=" icon-"] { /* use !important to prevent issues with browser extensions that change fonts */ font-family: 'zac' !important; diff --git a/projects/ziti-console-lib/src/lib/features/projectable-forms/auth-policy/auth-policy-form.component.html b/projects/ziti-console-lib/src/lib/features/projectable-forms/auth-policy/auth-policy-form.component.html index e9473419..6080c3cf 100644 --- a/projects/ziti-console-lib/src/lib/features/projectable-forms/auth-policy/auth-policy-form.component.html +++ b/projects/ziti-console-lib/src/lib/features/projectable-forms/auth-policy/auth-policy-form.component.html @@ -18,7 +18,7 @@ [title]="'Name'" [label]="'Required'" > - + + +
+
+
+ + + + + + + + + + +
+
+
+
Auto Enroll
+
+
+
+ YES + NO +
+
+
+
+
+
+
+
OTT Enroll
+
+
+
+ YES + NO +
+
+
+
+
+
+
+
Auth Enabled
+
+
+
+ YES + NO +
+
+
+
+
+ +
+
+
+
+ Location +
+
+ +
+
+
+ Matcher +
+
+ +
+
+
+ Parser +
+
+ +
+
+
+
+
+ Index +
+
+ +
+
+
+ Match Criteria +
+
+ +
+
+
+ Parser Criteria +
+
+ +
+
+
+
+ +
+
+
+
+ Enter PEM +
+ + Select File +
+
+ +
+ +
+
+
+ +
+ + + +
+
+
+ + +
+ Verify Certificate +
+
+
+ +
+ +
+
+ +
+
+
+
+ +
+
+ + diff --git a/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/certificate-authority-form.component.scss b/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/certificate-authority-form.component.scss new file mode 100644 index 00000000..58e8be8a --- /dev/null +++ b/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/certificate-authority-form.component.scss @@ -0,0 +1,187 @@ +.form-group-column { + &.three-fifths { + min-width: 31.25rem; + } +} + +.jwt-signer-form-container { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + align-items: center; + gap: var(--gapXL); + + .select-file-button { + padding-left: var(--paddingMedium); + padding-right: var(--paddingMedium); + height: 1.5625rem; + width: fit-content; + white-space: nowrap; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 0.875rem; + background: var(--primary); + border-style: solid; + border-width: 0.0625rem; + border-color: var(--stroke); + color: var(--white); + cursor: pointer; + border-radius: var(--inputBorderRadius); + gap: var(--marginMedium); + margin-top: -0.3125rem; + + &:hover { + filter: brightness(.8); + } + &:active { + filter: grayscale(1); + transform: translateY(0.0625rem); + } + + .spinner { + display: inline-block; + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background: transparent; + border-top: 0.125rem solid white; + border-right: 0.125rem solid white; + border-bottom: 0.125rem solid transparent; + border-left: 0.125rem solid transparent; + -webkit-animation: loading 0.5s infinite linear; + animation: loading 0.5s infinite linear; + } + } + + .select-file-label-container { + gap: var(--marginMedium); + } + + .updb-input-options { + margin-top: var(--marginSmall); + background-color: var(--formGroup); + padding: var(--paddingMedium); + border-radius: var(--inputBorderRadius); + + .form-field-title { + color: var(--offWhite); + } + } +} + +::ng-deep .jwt-signer-form-container { + select, + input { + &:disabled { + opacity: 0.75 !important; + cursor: text; + background-color: var(--stroke) !important; + } + } +} + +::ng-deep .api-data-no-wrap{ + .jse-text-mode { + width: 100%; + .cm-editor { + width: 100%; + .cm-scroller { + width: 100%; + .cm-content { + width: 100%; + overflow: auto; + .ͼr { + white-space: nowrap; + } + } + } + } + } +} + +.radio-group-container { + display: flex; + gap: var(--marginLarge); + + &:focus { + .radio-button-container { + .radio-button-circle { + border-color: var(--primaryColor); + } + } + } + + .radio-button-container { + display: flex; + flex-direction: row; + align-items: center; + background-color: var(--formGroup); + border-radius: var(--inputBorderRadius); + width: 100%; + padding-left: var(--paddingMedium); + padding-right: var(--paddingMedium); + padding-top: var(--paddingLarge); + padding-bottom: var(--paddingLarge); + cursor: pointer; + gap: var(--marginMedium); + + .radio-button-circle { + display: flex; + width: 1.3125rem; + height: 1.3125rem; + border-radius: 1.3125rem; + border-width: 0.125rem; + border-style: solid; + border-color: var(--offWhite); + align-items: center; + justify-content: center; + background-color: var(--offWhite); + + .radio-button-inner-circle { + display: none; + width: 0.9375rem; + height: 0.9375rem; + border-radius: 0.9375rem; + align-items: center; + justify-content: center; + background-color: var(--primaryColor); + } + } + + &.selected { + .radio-button-circle { + .radio-button-inner-circle { + display: flex; + } + } + } + + &:active { + .radio-button-circle { + .radio-button-inner-circle { + display: flex; + background-color: var(--menu); + } + } + } + + .radio-button-label { + color: var(--offWhite); + font-size: 0.875rem; + font-weight: 600; + } + } +} + +.allowed-signers-container { + background-color: var(--formGroup); + padding: var(--paddingMedium); + border-radius: var(--inputBorderRadius); + + .form-field-title { + color: var(--offWhite); + } +} \ No newline at end of file diff --git a/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/certificate-authority-form.component.ts b/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/certificate-authority-form.component.ts new file mode 100644 index 00000000..b322abfd --- /dev/null +++ b/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/certificate-authority-form.component.ts @@ -0,0 +1,440 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { + Component, ElementRef, + EventEmitter, Inject, + Input, + OnDestroy, + OnInit, Output, + ViewChild, +} from '@angular/core'; +import {CertificateAuthorityFormService} from "./certificate-authority-form.service"; +import {SchemaService} from "../../../services/schema.service"; +import {KEY_CODES, ProjectableForm} from "../projectable-form.class"; +import {GrowlerService} from "../../messaging/growler.service"; +import {ExtensionService, SHAREDZ_EXTENSION} from "../../extendable/extensions-noop.service"; + +import {cloneDeep, defer, delay, forOwn, keys, invert, isEmpty, isNil, unset, sortedUniq, debounce} from 'lodash'; +import {GrowlerModel} from "../../messaging/growler.model"; +import {SETTINGS_SERVICE, SettingsService} from "../../../services/settings.service"; +import {ZITI_DATA_SERVICE, ZitiDataService} from "../../../services/ziti-data.service"; +import {ActivatedRoute, Router} from "@angular/router"; +import {Location} from "@angular/common"; +import {ValidationService} from "../../../services/validation.service"; +import {CertificateAuthority} from "../../../models/certificate-authority"; + +@Component({ + selector: 'lib-certificate-authority', + templateUrl: './certificate-authority-form.component.html', + styleUrls: ['./certificate-authority-form.component.scss'] +}) +export class CertificateAuthorityFormComponent extends ProjectableForm implements OnInit, OnDestroy { + + @Input() override formData: any = new CertificateAuthority(); + @Input() override errors: any = {}; + @Output() close: EventEmitter = new EventEmitter(); + + _externalIdClaim = false; + + formView = 'simple'; + options: any[] = []; + isEditing = !isEmpty(this.formData.id); + formDataInvalid = false; + editMode = false; + items: any = []; + settings: any = {}; + fileSelectOpening = false; + identityRoleAttributes = []; + override entityType = 'cas'; + override entityClass = CertificateAuthority; + + locations = ['COMMON_NAME', 'SAN_URI', 'SAN_EMAIL']; + matchers = ['ALL', 'PREFIX', 'SUFFIX', 'SCHEME']; + parsers = ['NONE', 'SPLIT']; + + @ViewChild('fileSelect') filterInput: ElementRef; + + constructor( + public svc: CertificateAuthorityFormService, + private schemaSvc: SchemaService, + growlerService: GrowlerService, + @Inject(SHAREDZ_EXTENSION) extService: ExtensionService, + @Inject(SETTINGS_SERVICE) public settingsService: SettingsService, + @Inject(ZITI_DATA_SERVICE) override zitiService: ZitiDataService, + protected override router: Router, + protected override route: ActivatedRoute, + location: Location, + private validationService: ValidationService + ) { + super(growlerService, extService, zitiService, router, route, location); + } + + override ngOnInit(): void { + super.ngOnInit(); + this.svc.getIdentityRoleAttributes().then((results) => { + this.identityRoleAttributes = results?.data || []; + }); + this.settingsService.settingsChange.subscribe((results:any) => { + this.settings = results; + }); + } + + protected override entityUpdated() { + super.entityUpdated(); + } + + ngOnDestroy(): void { + this.clearForm(); + } + + headerActionRequested(event) { + switch(event.name) { + case 'save': + this.save(event); + break; + case 'close': + this.returnToListPage(); + break; + case 'toggle-view': + this.formView = event.data; + break; + } + } + + externalIdClaimChanged() { + if (this.externalIdClaim && !this.formData.externalIdClaim) { + this.formData.externalIdClaim = { + location: '', + matcher: '', + parser: '', + index: 0, + matcherCriteria: '', + parserCriteria: '' + } + } + } + + override clear() { + this.formData.configTypeId = ''; + this.clearForm(); + } + + clearForm() { + this.items.forEach((item: any) => { + if (item?.component) item.component.destroy(); + }); + this.errors = {}; + this.items = []; + this.formData = new CertificateAuthority(); + if (this.subscription) this.subscription.unsubscribe(); + } + + async save(event?: any) { + if(!this.validate()) { + return; + } + const tagVals = this.getTagValues(); + if (!isEmpty(tagVals)) { + forOwn(tagVals, (value, key) => { + this.formData.tags[key] = value; + }); + } + const apiData = this.apiData; + apiData.id = this.formData.id; + this.isLoading = true; + this.svc.save(apiData).then((result) => { + if (this.isModal) { + this.closeModal(true, true); + return; + } + this.initData = this.formData; + this._dataChange = false; + this.returnToListPage(); + }).finally(() => { + this.isLoading = false; + }); + } + + validate() { + this.errors = {}; + const labels = []; + const growlers = []; + if (isEmpty(this.formData.name)) { + this.errors.name = true; + labels.push('Name'); + } + if (this.externalIdClaim) { + if (isEmpty(this.claimLocation)) { + this.errors.claimLocation = true; + labels.push('Location'); + } + if (isEmpty(this.parser)) { + this.errors.parser = true; + labels.push('Parser'); + } + if (isEmpty(this.matcher)) { + this.errors.matcher = true; + labels.push('Matcher'); + } + if (isEmpty(this.parserCriteria)) { + this.errors.parserCriteria = true; + labels.push('Parser Criteria'); + } + if (isEmpty(this.matcherCriteria)) { + this.errors.matcherCriteria = true; + labels.push('Matcher Criteria'); + } + if (isNaN(this.index) || this.index < 0) { + this.errors.index = true; + labels.push('Index'); + } + } + if (!this.validationService.isValidPEM(this.formData.certPem)) { + this.errors.certPem = true; + labels.push('Cert PEM'); + } + if (!isEmpty(this.errors)) { + let errorLabels = ''; + labels.forEach((label, index) => { + errorLabels += `
  • ${label}
  • `; + }); + errorLabels = `
      ${errorLabels}
    `; + growlers.push(new GrowlerModel( + 'error', + 'Invalid', + `Missing Form Data`, + `Please enter a value for the highlighted fields: ${errorLabels}`, + )); + } + if (!isEmpty(growlers)) { + growlers.forEach((growlerData) => { + this.growlerService.show(growlerData); + }); + return isEmpty(this.errors); + } + + growlers.forEach((growlerData) => { + this.growlerService.show(growlerData); + }); + return isEmpty(this.errors); + } + + get apiCallURL() { + return this.settings.selectedEdgeController + '/edge/management/v1/cas' + (this.formData.id ? `/${this.formData.id}` : ''); + } + + get apiData(): any { + const data: any = { + name: this.formData.name || '', + isAutoCaEnrollmentEnabled: this.formData.isAutoCaEnrollmentEnabled, + isOttCaEnrollmentEnabled: this.formData.isOttCaEnrollmentEnabled, + identityRoles: this.formData.identityRoles || [], + identityNameFormat: this.formData.identityNameFormat, + isAuthEnabled: this.formData.isAuthEnabled, + certPem: this.formData.certPem, + + tags: this.formData.tags || {} + }; + if (this.externalIdClaim) { + data.externalIdClaim = this.formData.externalIdClaim; + } + this._apiData = data; + return this._apiData; + } + + _apiData: any = {}; + set apiData(data) { + this._apiData = data; + } + + dataChanged(event) { + this._apiData = cloneDeep(this.apiData); + } + + get externalIdClaim() { + if (this.formData.externalIdClaim) { + this._externalIdClaim = true; + } + return this._externalIdClaim; + } + + set externalIdClaim(val) { + if (!val) { + this.formData.externalIdClaim = undefined; + } + this._externalIdClaim = val; + } + + toggleSwitch(prop) { + if (prop === 'externalIdClaim') { + this.externalIdClaim = !this.externalIdClaim; + } + this.formData[prop] = !this.formData[prop]; + } + + openFileSelect(event: any) { + this.filterInput.nativeElement.click(); + this.fileSelectOpening = true; + delay(() => { + this.fileSelectOpening = false; + }, 1000); + } + + selectPemFile(event: any) { + console.log(event); + const file: File = event?.target?.files[0]; + + if (file) { + const fileReader = new FileReader(); + fileReader.onload = (fileLoadedEvent) => { + const textFromSelectedFile: any = fileLoadedEvent.target?.result; + if (!this.validationService.isValidPEM(textFromSelectedFile?.trim())) { + this.errors.certPem = true; + const growlerData = new GrowlerModel( + 'error', + 'Invalid', + `Cert PEM Invalid`, + `The file selected for the Cert PEM field is invalid. Please check your input and try again.`, + ); + this.growlerService.show(growlerData); + this.formData.certPem = 'Invalid PEM. Please select or enter a valid PEM certificate.'; + } else { + unset(this.errors, 'certPem'); + this.formData.certPem = textFromSelectedFile; + } + }; + fileReader.readAsText(file, "UTF-8"); + } + } + + apiActionRequested(action) { + switch (action.id) { + case 'cli': + this.copyCLICommand(); + break; + case 'curl': + this.copyCURLCommand(); + break; + } + } + + copyCLICommand() { + let command = `ziti edge ${this.formData.id ? 'update' : 'create'} ca ${this.formData.id ? `'${this.formData.id}'` : ''} ${this.formData.id ? '--name' : ''} '${this.formData.name}' ${this.formData.id ? '' : this.formData.certPem} --identity-name-format '${this.formData.identityNameFormat}' --auth '${this.formData.isAuthEnabled}' --auth '${this.formData.isAuthEnabled}' --autoca '${this.formData.isAutoCaEnrollmentEnabled}' --ottca '${this.formData.isOttCaEnrollmentEnabled}'`; + if (this.externalIdClaim) { + command += ` --location '${this.formData.externalIdClaim.location}' --matcher '${this.formData.externalIdClaim.matcher}' --parser '${this.formData.externalIdClaim.parser}' --index '${this.formData.externalIdClaim.index}' --matcher-criteria '${this.formData.externalIdClaim.matcherCriteria}' --parser-criteria '${this.formData.externalIdClaim.parserCriteria}'` + } + + navigator.clipboard.writeText(command); + const growlerData = new GrowlerModel( + 'success', + 'Success', + `Text Copied`, + `CLI command copied to clipboard`, + ); + this.growlerService.show(growlerData); + } + + copyCURLCommand() { + const command = `curl '${this.apiCallURL}' \\ + ${this.formData.id ? '--request PATCH \\' : '\\'} + -H 'accept: application/json' \\ + -H 'content-type: application/json' \\ + -H 'zt-session: ${this.settings.session.id}' \\ + --data-raw '${JSON.stringify(this.apiData)}'`; + + navigator.clipboard.writeText(command); + const growlerData = new GrowlerModel( + 'success', + 'Success', + `Text Copied`, + `CURL command copied to clipboard`, + ); + this.growlerService.show(growlerData); + } + + verifyCertificate(event) { + event.stopPropagation(); + event.preventDefault(); + this.router?.navigateByUrl(`${this.basePath}/${this.formData.id}/verify`); + } + + get claimLocation() { + return this.formData.externalIdClaim?.location; + } + + set claimLocation(loc) { + if (!this.formData.externalIdClaim) { + return; + } + this.formData.externalIdClaim.location = loc; + } + + get matcher() { + return this.formData.externalIdClaim?.matcher; + } + + set matcher(matcher) { + if (!this.formData.externalIdClaim) { + return; + } + this.formData.externalIdClaim.matcher = matcher; + } + + get parser() { + return this.formData.externalIdClaim?.parser; + } + + set parser(parser) { + if (!this.formData.externalIdClaim) { + return; + } + this.formData.externalIdClaim.parser = parser; + } + + get index() { + return this.formData.externalIdClaim?.index; + } + + set index(index) { + if (!this.formData.externalIdClaim) { + return; + } + this.formData.externalIdClaim.index = index; + } + + get matcherCriteria() { + return this.formData.externalIdClaim?.matcherCriteria; + } + + set matcherCriteria(matcherCriteria) { + if (!this.formData.externalIdClaim) { + return; + } + this.formData.externalIdClaim.matcherCriteria = matcherCriteria; + } + + get parserCriteria() { + return this.formData.externalIdClaim?.parserCriteria; + } + + set parserCriteria(parserCriteria) { + if (!this.formData.externalIdClaim) { + return; + } + this.formData.externalIdClaim.parserCriteria = parserCriteria; + } +} diff --git a/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/certificate-authority-form.service.ts b/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/certificate-authority-form.service.ts new file mode 100644 index 00000000..2fc7f5ba --- /dev/null +++ b/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/certificate-authority-form.service.ts @@ -0,0 +1,113 @@ +import {Injectable, Inject} from '@angular/core'; +import {ZITI_DATA_SERVICE, ZitiDataService} from "../../../services/ziti-data.service"; +import {cloneDeep, defer, invert, isBoolean, isEmpty, isNil, keys} from "lodash"; +import {GrowlerModel} from "../../messaging/growler.model"; +import {GrowlerService} from "../../messaging/growler.service"; +import {ExtensionService, SHAREDZ_EXTENSION} from "../../extendable/extensions-noop.service"; +import {ValidationService} from "../../../services/validation.service"; +import {AuthPolicy} from "../../../models/auth-policy"; +import {CertificateAuthority} from "../../../models/certificate-authority"; + +@Injectable({ + providedIn: 'root' +}) +export class CertificateAuthorityFormService { + + items: any[] = []; + errors: any[] = []; + saveDisabled = false; + + jwtSigners = []; + jwtSignerNamedAttributes = []; + jwtSignerNamedAttributesMap = {}; + jwtSignersLoading = false; + + selectedSecondaryJwtSigner; + filteredJwtSigners; + + paging = { + filter: "", + noSearch: true, + order: "asc", + page: 1, + searchOn: "name", + sort: "name", + total: 100 + } + + constructor( + @Inject(ZITI_DATA_SERVICE) private dataService: ZitiDataService, + @Inject(SHAREDZ_EXTENSION)private extService: ExtensionService, + private growlerService: GrowlerService, + private validationService: ValidationService + ) { + } + + save(formData) { + const isUpdate = !isEmpty(formData.id); + const data: any = this.getAuthPolicyDataModel(formData, isUpdate); + let prom; + if (isUpdate) { + prom = this.dataService.put('cas', data, formData.id, true); + } else { + prom = this.dataService.post('cas', data, true); + } + + return prom.then(async (result: any) => { + const id = isUpdate ? formData.id : (result?.data?.id || result?.id); + let config = await this.dataService.getSubdata('cas', id, '').then((svcData) => { + return svcData.data; + }); + return this.extService.formDataSaved(config).then((formSavedResult: any) => { + if (!formSavedResult) { + return config; + } + const growlerData = new GrowlerModel( + 'success', + 'Success', + `Certificate Authority ${isUpdate ? 'Updated' : 'Created'}`, + `Successfully ${isUpdate ? 'updated' : 'created'} Certificate Authority: ${formData.name}`, + ); + this.growlerService.show(growlerData); + return config; + }).catch((result) => { + return false; + }); + }).catch((resp) => { + let errorMessage; + if (resp?.error?.error?.cause?.message) { + errorMessage = resp?.error?.error?.cause?.message; + } else if (resp?.error?.error?.cause?.reason) { + errorMessage = resp?.error?.error?.cause?.reason; + }else if (resp?.error?.message) { + errorMessage = resp?.error?.message; + } else { + errorMessage = 'An unknown error occurred'; + } + const growlerData = new GrowlerModel( + 'error', + 'Error', + `Error Creating Certificate Authority`, + errorMessage, + ); + this.growlerService.show(growlerData); + throw resp; + }) + } + + getIdentityRoleAttributes() { + return this.dataService.get('identity-role-attributes', {}, []); + } + + getAuthPolicyDataModel(formData, isUpdate) { + const saveModel = new CertificateAuthority(); + const modelProperties = keys(saveModel); + modelProperties.forEach((prop) => { + switch(prop) { + default: + saveModel[prop] = formData[prop]; + } + }); + return saveModel; + } +} diff --git a/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/verify-certificate/verify-certificate.component.html b/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/verify-certificate/verify-certificate.component.html new file mode 100644 index 00000000..80b7e3a4 --- /dev/null +++ b/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/verify-certificate/verify-certificate.component.html @@ -0,0 +1,65 @@ +
    + +
    +
    +
    + +
      +
    • Generate a certificate with the common name CN=XeoiWd16J
    • +
    • Upload the generated cert, or copy & paste it using the text field below
    • +
    • Click the "Verify" button to submit certificate for verification
    • +
    +
    + +
    + {{formData.verificationToken}} +
    +
    +
    + + {{certErrorMessage}} +
    +
    +
    +
    + Enter PEM +
    + + Select File +
    +
    + +
    + +
    +
    +
    +
    +
    Oops, no get me out of here
    +
    Verify
    +
    +
    +
    +
    +
    + diff --git a/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/verify-certificate/verify-certificate.component.scss b/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/verify-certificate/verify-certificate.component.scss new file mode 100644 index 00000000..7c796f48 --- /dev/null +++ b/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/verify-certificate/verify-certificate.component.scss @@ -0,0 +1,138 @@ +::ng-deep .verify-certificate-container lib-form-header .form-header-container { + max-width: 50rem; +} + +.verify-certificate-container { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + align-items: center; + gap: var(--gapXL); + + .projectable-form-main-column { + max-width: 50rem; + } + + .copy-token-container { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + cursor: pointer; + background-color: var(--stroke); + border-radius: var(--inputBorderRadius); + transition: all .2s; + + .copy-icon { + filter: grayscale(1); + } + + &:hover { + .copy-token-button { + transform: scale(1.02); + } + .copy-icon { + transform: scale(1.02); + filter: none; + } + } + + .copy-token-button { + cursor: pointer; + align-items: center; + margin-left: var(--marginMedium); + display: flex; + } + } + + .instructions-list { + margin: 0; + padding-left: 1rem; + display: flex; + flex-direction: column; + gap: .4rem; + font-size: .9rem; + } + + .select-file-button { + padding-left: var(--paddingMedium); + padding-right: var(--paddingMedium); + height: 1.5625rem; + width: fit-content; + white-space: nowrap; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 0.875rem; + background: var(--primary); + border-style: solid; + border-width: 0.0625rem; + border-color: var(--stroke); + color: var(--white); + cursor: pointer; + border-radius: var(--inputBorderRadius); + gap: var(--marginMedium); + margin-top: -0.3125rem; + + &:hover { + filter: brightness(.8); + } + &:active { + filter: grayscale(1); + transform: translateY(0.0625rem); + } + + .spinner { + display: inline-block; + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background: transparent; + border-top: 0.125rem solid white; + border-right: 0.125rem solid white; + border-bottom: 0.125rem solid transparent; + border-left: 0.125rem solid transparent; + -webkit-animation: loading 0.5s infinite linear; + animation: loading 0.5s infinite linear; + } + } + + .select-file-label-container { + gap: var(--marginMedium); + } + + .cert-error-text { + height: .7rem; + } + + .save-button { + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 0.875rem; + background: var(--primary); + padding: 0.4375rem; + border-radius: var(--inputBorderRadius); + color: var(--white); + cursor: pointer; + box-shadow: 0 0.1875rem 0.5625rem 0 var(--primaryColorOpaque); + } +} + +::ng-deep .verify-certificate-container { + select, + input { + &:disabled { + opacity: 0.75 !important; + cursor: text; + background-color: var(--stroke) !important; + } + } +} + +::ng-deep .verify-certificate-wrapper lib-form-header .form-header-container { + max-width: 50rem; +} diff --git a/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/verify-certificate/verify-certificate.component.ts b/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/verify-certificate/verify-certificate.component.ts new file mode 100644 index 00000000..03fbb6ad --- /dev/null +++ b/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/verify-certificate/verify-certificate.component.ts @@ -0,0 +1,245 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { + Component, ElementRef, + EventEmitter, Inject, + Input, + OnDestroy, + OnInit, Output, + ViewChild, +} from '@angular/core'; +import {VerifyCertificateService} from "./verify-certificate.service"; +import {SchemaService} from "../../../../services/schema.service"; +import {KEY_CODES, ProjectableForm} from "../../projectable-form.class"; +import {GrowlerService} from "../../../messaging/growler.service"; +import {ExtensionService, SHAREDZ_EXTENSION} from "../../../extendable/extensions-noop.service"; + +import {cloneDeep, defer, delay, forOwn, keys, invert, isEmpty, isNil, unset, sortedUniq, debounce} from 'lodash'; +import {GrowlerModel} from "../../../messaging/growler.model"; +import {SETTINGS_SERVICE, SettingsService} from "../../../../services/settings.service"; +import {ZITI_DATA_SERVICE, ZitiDataService} from "../../../../services/ziti-data.service"; +import {ActivatedRoute, Router} from "@angular/router"; +import {Location} from "@angular/common"; +import {ValidationService} from "../../../../services/validation.service"; +import {CertificateAuthority} from "../../../../models/certificate-authority"; + +@Component({ + selector: 'lib-certificate-authority', + templateUrl: './verify-certificate.component.html', + styleUrls: ['./verify-certificate.component.scss'] +}) +export class VerifyCertificateComponent extends ProjectableForm implements OnInit, OnDestroy { + + @Input() override formData: any = new CertificateAuthority(); + @Input() override errors: any = {}; + @Output() close: EventEmitter = new EventEmitter(); + + _externalIdClaim = false; + + formView = 'simple'; + options: any[] = []; + isEditing = !isEmpty(this.formData.id); + formDataInvalid = false; + editMode = false; + items: any = []; + settings: any = {}; + fileSelectOpening = false; + identityRoleAttributes = []; + override entityType = 'cas'; + override entityClass = CertificateAuthority; + + locations = ['COMMON_NAME', 'SAN_URI', 'SAN_EMAIL']; + matchers = ['ALL', 'PREFIX', 'SUFFIX', 'SCHEME']; + parsers = ['NONE', 'SPLIT']; + + certificate = ''; + certErrorMessage = ''; + + @ViewChild('fileSelect') filterInput: ElementRef; + + constructor( + public svc: VerifyCertificateService, + private schemaSvc: SchemaService, + growlerService: GrowlerService, + @Inject(SHAREDZ_EXTENSION) extService: ExtensionService, + @Inject(SETTINGS_SERVICE) public settingsService: SettingsService, + @Inject(ZITI_DATA_SERVICE) override zitiService: ZitiDataService, + protected override router: Router, + protected override route: ActivatedRoute, + location: Location, + private validationService: ValidationService + ) { + super(growlerService, extService, zitiService, router, route, location); + } + + override ngOnInit(): void { + super.ngOnInit(); + this.settingsService.settingsChange.subscribe((results:any) => { + this.settings = results; + }); + } + + protected override entityUpdated() { + super.entityUpdated(); + } + + ngOnDestroy(): void { + this.clearForm(); + } + + headerActionRequested(event) { + switch(event.name) { + case 'save': + this.save(event); + break; + case 'close': + this.returnToListPage(); + break; + case 'toggle-view': + this.formView = event.data; + break; + } + } + + override clear() { + this.formData.configTypeId = ''; + this.clearForm(); + } + + clearForm() { + this.items.forEach((item: any) => { + if (item?.component) item.component.destroy(); + }); + this.errors = {}; + this.items = []; + this.formData = new CertificateAuthority(); + if (this.subscription) this.subscription.unsubscribe(); + } + + override copyToClipboard(val) { + navigator.clipboard.writeText(val); + const growlerData = new GrowlerModel( + 'info', + 'Information', + `Token Copied`, + `Token copied to clipboard`, + ); + this.growlerService.show(growlerData); + } + + async save(event?: any) { + if(!this.validate()) { + return; + } + const tagVals = this.getTagValues(); + if (!isEmpty(tagVals)) { + forOwn(tagVals, (value, key) => { + this.formData.tags[key] = value; + }); + } + const apiData = this.apiData; + apiData.id = this.formData.id; + this.isLoading = true; + this.svc.save(apiData, this.certificate).then((result) => { + if (this.isModal) { + this.closeModal(true, true); + return; + } + this.initData = this.formData; + this._dataChange = false; + this.returnToListPage(); + }).catch((errorMessage) => { + this.errors.certPem = true; + this.certErrorMessage = errorMessage; + }).finally(() => { + this.isLoading = false; + }); + } + + validate() { + this.errors = {}; + const labels = []; + const growlers = []; + if (isEmpty(this.certificate)) { + this.errors.certPem = true; + this.certErrorMessage = 'Please enter or upload a valid certificate'; + growlers.push(new GrowlerModel( + 'error', + 'Invalid', + `Missing Certificate`, + this.certErrorMessage, + )); + } + return isEmpty(this.errors); + } + + get apiCallURL() { + return this.settings.selectedEdgeController + '/edge/management/v1/verify-certificate' + (this.formData.id ? `/${this.formData.id}` : ''); + } + + get apiData(): any { + const data: any = {}; + this._apiData = data; + return this._apiData; + } + + _apiData: any = {}; + set apiData(data) { + this._apiData = data; + } + + dataChanged(event) { + this._apiData = cloneDeep(this.apiData); + } + + + openFileSelect(event: any) { + this.filterInput.nativeElement.click(); + this.fileSelectOpening = true; + delay(() => { + this.fileSelectOpening = false; + }, 1000); + } + + selectPemFile(event: any) { + console.log(event); + const file: File = event?.target?.files[0]; + + if (file) { + const fileReader = new FileReader(); + fileReader.onload = (fileLoadedEvent) => { + const textFromSelectedFile: any = fileLoadedEvent.target?.result; + if (!this.validationService.isValidPEM(textFromSelectedFile?.trim())) { + this.errors.certPem = true; + const growlerData = new GrowlerModel( + 'error', + 'Invalid', + `Cert Invalid`, + `The file selected for the Certificate field is invalid. Please check your input and try again.`, + ); + this.growlerService.show(growlerData); + this.certificate = 'Invalid cert. Please select or enter a valid certificate.'; + } else { + unset(this.errors, 'certPem'); + this.certificate = textFromSelectedFile; + } + }; + fileReader.readAsText(file, "UTF-8"); + } + } + +} diff --git a/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/verify-certificate/verify-certificate.service.ts b/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/verify-certificate/verify-certificate.service.ts new file mode 100644 index 00000000..cde65bd7 --- /dev/null +++ b/projects/ziti-console-lib/src/lib/features/projectable-forms/certificate-authority/verify-certificate/verify-certificate.service.ts @@ -0,0 +1,57 @@ +import {Injectable, Inject} from '@angular/core'; +import {ZITI_DATA_SERVICE, ZitiDataService} from "../../../../services/ziti-data.service"; +import {GrowlerModel} from "../../../messaging/growler.model"; +import {GrowlerService} from "../../../messaging/growler.service"; +import {ExtensionService, SHAREDZ_EXTENSION} from "../../../extendable/extensions-noop.service"; +import {ValidationService} from "../../../../services/validation.service"; +import {CertificateAuthority} from "../../../../models/certificate-authority"; + +@Injectable({ + providedIn: 'root' +}) +export class VerifyCertificateService { + + items: any[] = []; + errors: any[] = []; + saveDisabled = false; + + paging = { + filter: "", + noSearch: true, + order: "asc", + page: 1, + searchOn: "name", + sort: "name", + total: 100 + } + + constructor( + @Inject(ZITI_DATA_SERVICE) private dataService: ZitiDataService, + @Inject(SHAREDZ_EXTENSION)private extService: ExtensionService, + private growlerService: GrowlerService, + private validationService: ValidationService + ) { + } + + save(formData, cert) { + return this.dataService.post(`cas/${formData.id}/verify`, cert, true, 'text/plain').then(async (result: any) => { + const growlerData = new GrowlerModel( + 'success', + 'Success', + `Certificate Verified`, + `Successfully verified certificate for this certificate authority`, + ); + this.growlerService.show(growlerData); + }).catch((resp) => { + const errorMessage = this.dataService.getErrorMessage(resp); + const growlerData = new GrowlerModel( + 'error', + 'Error', + `Error Verifying Certificate`, + errorMessage, + ); + this.growlerService.show(growlerData); + throw errorMessage; + }) + } +} diff --git a/projects/ziti-console-lib/src/lib/features/projectable-forms/form-field-container/form-field-container.component.html b/projects/ziti-console-lib/src/lib/features/projectable-forms/form-field-container/form-field-container.component.html index 2ecb9624..bfaea14c 100644 --- a/projects/ziti-console-lib/src/lib/features/projectable-forms/form-field-container/form-field-container.component.html +++ b/projects/ziti-console-lib/src/lib/features/projectable-forms/form-field-container/form-field-container.component.html @@ -21,6 +21,31 @@ matTooltipPosition="below" > +
    + + OFF + +
    +
    +
    +
    +
    +
    + + ON + +
    diff --git a/projects/ziti-console-lib/src/lib/features/projectable-forms/form-field-container/form-field-container.component.ts b/projects/ziti-console-lib/src/lib/features/projectable-forms/form-field-container/form-field-container.component.ts index 3678e0fb..18b9f56e 100644 --- a/projects/ziti-console-lib/src/lib/features/projectable-forms/form-field-container/form-field-container.component.ts +++ b/projects/ziti-console-lib/src/lib/features/projectable-forms/form-field-container/form-field-container.component.ts @@ -36,12 +36,26 @@ export class FormFieldContainerComponent { @Input() class = ''; @Input() contentStyle: any = ''; @Input() showHeader: any = true; + @Input() showToggle = false; @Input() headerActions: any[]; + @Input() headerToggle = false; @Output() actionRequested: EventEmitter = new EventEmitter(); + @Output() headerToggleChange: EventEmitter = new EventEmitter(); constructor() {} actionClicked(action) { this.actionRequested.emit(action); } + + toggleHeader(option?) { + if (option == 'left') { + this.headerToggle = false; + } else if (option === 'right') { + this.headerToggle = true; + } else { + this.headerToggle = !this.headerToggle; + } + this.headerToggleChange.emit(this.headerToggle); + } } diff --git a/projects/ziti-console-lib/src/lib/features/projectable-forms/form-header/form-header.component.html b/projects/ziti-console-lib/src/lib/features/projectable-forms/form-header/form-header.component.html index 59feedd4..57a29162 100644 --- a/projects/ziti-console-lib/src/lib/features/projectable-forms/form-header/form-header.component.html +++ b/projects/ziti-console-lib/src/lib/features/projectable-forms/form-header/form-header.component.html @@ -11,14 +11,14 @@
    -
    - - FORM - +
    + + FORM +
    = new EventEmitter(); @Output() actionRequested: EventEmitter = new EventEmitter(); diff --git a/projects/ziti-console-lib/src/lib/features/sidebars/side-banner/side-banner.component.scss b/projects/ziti-console-lib/src/lib/features/sidebars/side-banner/side-banner.component.scss index 7b680eb4..dc9439bc 100644 --- a/projects/ziti-console-lib/src/lib/features/sidebars/side-banner/side-banner.component.scss +++ b/projects/ziti-console-lib/src/lib/features/sidebars/side-banner/side-banner.component.scss @@ -2,7 +2,7 @@ max-width: 23.75rem; position: relative; display:inline-block; - height:100vh; + height:100%; background-image: url(../../../assets/images/Login.jpg); background-size: cover; background-position: center center; diff --git a/projects/ziti-console-lib/src/lib/features/sidebars/side-toolbar/side-toolbar.component.scss b/projects/ziti-console-lib/src/lib/features/sidebars/side-toolbar/side-toolbar.component.scss index 082d5bb0..b2e256fa 100644 --- a/projects/ziti-console-lib/src/lib/features/sidebars/side-toolbar/side-toolbar.component.scss +++ b/projects/ziti-console-lib/src/lib/features/sidebars/side-toolbar/side-toolbar.component.scss @@ -7,7 +7,7 @@ justify-content: space-between; width: 4rem; background: linear-gradient(to bottom, var(--primary) 0%, var(--secondary) 100%); - height: 100vh; + height: 100%; padding-top: 1rem; padding-bottom:1rem; diff --git a/projects/ziti-console-lib/src/lib/models/certificate-authority.ts b/projects/ziti-console-lib/src/lib/models/certificate-authority.ts new file mode 100644 index 00000000..85521f34 --- /dev/null +++ b/projects/ziti-console-lib/src/lib/models/certificate-authority.ts @@ -0,0 +1,13 @@ +export class CertificateAuthority { + name: string = ''; + id: string = ''; + isAutoCaEnrollmentEnabled: boolean = false; + isOttCaEnrollmentEnabled: boolean = false; + identityRoles: any[] = []; + identityNameFormat: string = ''; + isAuthEnabled: boolean = false; + certPem: string = ''; + externalIdClaim: any = undefined; + tags: any = {}; + verificationToke: string = ''; +} \ No newline at end of file diff --git a/projects/ziti-console-lib/src/lib/pages/auth-policies/auth-policies-page.component.html b/projects/ziti-console-lib/src/lib/pages/auth-policies/auth-policies-page.component.html index 810d7083..22aa697c 100644 --- a/projects/ziti-console-lib/src/lib/pages/auth-policies/auth-policies-page.component.html +++ b/projects/ziti-console-lib/src/lib/pages/auth-policies/auth-policies-page.component.html @@ -1,17 +1,17 @@ -
    +
    - + + + + +
    diff --git a/projects/ziti-console-lib/src/lib/pages/certificate-authorities/certificate-authorities-page.component.scss b/projects/ziti-console-lib/src/lib/pages/certificate-authorities/certificate-authorities-page.component.scss new file mode 100644 index 00000000..b330fda1 --- /dev/null +++ b/projects/ziti-console-lib/src/lib/pages/certificate-authorities/certificate-authorities-page.component.scss @@ -0,0 +1,8 @@ +.certificate-authorities { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + position: relative; + flex: 1 1 auto; +} diff --git a/projects/ziti-console-lib/src/lib/pages/certificate-authorities/certificate-authorities-page.component.ts b/projects/ziti-console-lib/src/lib/pages/certificate-authorities/certificate-authorities-page.component.ts new file mode 100644 index 00000000..1be00de1 --- /dev/null +++ b/projects/ziti-console-lib/src/lib/pages/certificate-authorities/certificate-authorities-page.component.ts @@ -0,0 +1,117 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import {Component, OnInit} from '@angular/core'; +import {DataTableFilterService} from "../../features/data-table/data-table-filter.service"; +import {CertificateAuthoritiesPageService} from "./certificate-authorities-page.service"; +import {TabNameService} from "../../services/tab-name.service"; +import {ListPageComponent} from "../../shared/list-page-component.class"; +import {ConsoleEventsService} from "../../services/console-events.service"; +import {MatDialog} from "@angular/material/dialog"; +import {ConfirmComponent} from "../../features/confirm/confirm.component"; +import {firstValueFrom} from "rxjs"; + + +@Component({ + selector: 'lib-auth-policies', + templateUrl: './certificate-authorities-page.component.html', + styleUrls: ['./certificate-authorities-page.component.scss'] +}) +export class CertificateAuthoritiesPageComponent extends ListPageComponent implements OnInit { + title = 'Certificate Authorities' + tabs: { url: string, label: string }[] ; + isLoading: boolean; + formDataChanged = false; + + constructor( + override svc: CertificateAuthoritiesPageService, + filterService: DataTableFilterService, + private tabNames: TabNameService, + consoleEvents: ConsoleEventsService, + dialogForm: MatDialog + ) { + super(filterService, svc, consoleEvents, dialogForm); + } + + override ngOnInit() { + this.tabs = this.tabNames.getTabs('authentication'); + this.svc.refreshData = this.refreshData; + super.ngOnInit(); + } + + headerActionClicked(action: string) { + switch (action) { + case 'add': + this.svc.openEditForm(); + break; + case 'edit': + this.svc.openEditForm(); + break; + case 'delete': + const selectedItems = this.rowData.filter((row) => { + return row.selected; + }); + const label = selectedItems.length > 1 ? 'certificate authorities' : 'certificate authority'; + this.openBulkDelete(selectedItems, label); + break; + default: + } + } + + tableAction(event: any) { + switch(event?.action) { + case 'toggleAll': + case 'toggleItem': + this.itemToggled(event.item) + break; + case 'update': + this.svc.openEditForm(event.item?.id); + break; + case 'create': + this.svc.openEditForm(); + break; + case 'delete': + this.deleteItem(event.item); + break; + case 'verify': + this.svc.verifyCert(event.item.id); + break; + case 'download-all': + this.downloadAllItems(); + break; + case 'download-selected': + this.svc.downloadItems(this.selectedItems); + break; + default: + break; + } + } + + deleteItem(item: any) { + this.openBulkDelete([item], 'certificate authority'); + } + + closeModal(event: any) { + this.svc.sideModalOpen = false; + if(event?.refresh) { + this.refreshData(); + } + } + + dataChanged(event) { + this.formDataChanged = event; + } +} diff --git a/projects/ziti-console-lib/src/lib/pages/certificate-authorities/certificate-authorities-page.service.ts b/projects/ziti-console-lib/src/lib/pages/certificate-authorities/certificate-authorities-page.service.ts new file mode 100644 index 00000000..a67bcc8a --- /dev/null +++ b/projects/ziti-console-lib/src/lib/pages/certificate-authorities/certificate-authorities-page.service.ts @@ -0,0 +1,214 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import {Inject, Injectable} from '@angular/core'; +import {DataTableFilterService, FilterObj} from "../../features/data-table/data-table-filter.service"; +import _, {isEmpty, unset} from "lodash"; +import moment from "moment"; +import {ListPageServiceClass} from "../../shared/list-page-service.class"; +import { + TableColumnDefaultComponent +} from "../../features/data-table/column-headers/table-column-default/table-column-default.component"; +import {CallbackResults} from "../../features/list-page-features/list-page-form/list-page-form.component"; +import {SchemaService} from "../../services/schema.service"; +import {SETTINGS_SERVICE, SettingsService} from "../../services/settings.service"; +import {CsvDownloadService} from "../../services/csv-download.service"; +import {ExtensionService, SHAREDZ_EXTENSION} from "../../features/extendable/extensions-noop.service"; +import {Service} from "../../models/service"; +import {TableCellNameComponent} from "../../features/data-table/cells/table-cell-name/table-cell-name.component"; +import {Router} from "@angular/router"; +import {ConfirmComponent} from "../../features/confirm/confirm.component"; +import {firstValueFrom} from "rxjs"; +import {MatDialog} from "@angular/material/dialog"; + +@Injectable({ + providedIn: 'root' +}) +export class CertificateAuthoritiesPageService extends ListPageServiceClass { + + private paging = this.DEFAULT_PAGING; + + resourceType = 'cas'; + selectedConfig: any = {}; + modalType = ''; + + override menuItems = [ + {name: 'Edit', action: 'update'}, + {name: 'Verify', action: 'verify'}, + {name: 'Delete', action: 'delete'}, + ] + + constructor( + private schemaSvc: SchemaService, + @Inject(SETTINGS_SERVICE) settings: SettingsService, + filterService: DataTableFilterService, + csvDownloadService: CsvDownloadService, + @Inject(SHAREDZ_EXTENSION) extService: ExtensionService, + protected override router: Router, + private dialogForm: MatDialog + ) { + super(settings, filterService, csvDownloadService, extService, router); + } + + initTableColumns(): any { + + const verifiedHeaderComponentParams = { + filterType: 'SELECT', + enableSorting: true, + filterOptions: [ + { label: 'All', value: '' }, + { label: 'Verified', value: true }, + { label: 'Unverified', value: false }, + ] + }; + const enabledComponentParams = { + filterType: 'SELECT', + enableSorting: true, + filterOptions: [ + { label: 'All', value: '' }, + { label: 'Enabled', value: true }, + { label: 'Disabled', value: false }, + ] + }; + const createdAtHeaderComponentParams = { + filterType: 'DATETIME', + }; + return [ + { + colId: 'name', + field: 'name', + headerName: 'Name', + headerComponent: TableColumnDefaultComponent, + headerComponentParams: this.headerComponentParams, + cellRenderer: TableCellNameComponent, + cellRendererParams: { pathRoot: this.basePath }, + onCellClicked: (data) => { + if (this.hasSelectedText()) { + return; + } + this.openEditForm(data?.data?.id); + }, + resizable: true, + cellClass: 'nf-cell-vert-align tCol', + sortable: true, + filter: true, + sortColumn: this.sort.bind(this) + }, + { + colId: 'isVerified', + field: 'isVerified', + headerName: 'Verified', + headerComponent: TableColumnDefaultComponent, + headerComponentParams: verifiedHeaderComponentParams, + resizable: true, + cellClass: 'nf-cell-vert-align tCol', + sortable: true, + filter: true, + sortColumn: this.sort.bind(this), + }, + { + colId: 'isAutoCaEnrollmentEnabled', + field: 'isAutoCaEnrollmentEnabled', + headerName: 'Auto Enrollment', + headerComponent: TableColumnDefaultComponent, + headerComponentParams: enabledComponentParams, + resizable: true, + cellClass: 'nf-cell-vert-align tCol', + sortable: true, + filter: true, + sortColumn: this.sort.bind(this), + }, + { + colId: 'isOttCaEnrollmentEnabled', + field: 'isOttCaEnrollmentEnabled', + headerName: 'OTT Auto', + headerComponent: TableColumnDefaultComponent, + headerComponentParams: enabledComponentParams, + resizable: true, + cellClass: 'nf-cell-vert-align tCol', + sortable: true, + filter: true, + sortColumn: this.sort.bind(this), + }, + { + colId: 'isAuthEnabled', + field: 'isAuthEnabled', + headerName: 'Auth Enabled', + headerComponent: TableColumnDefaultComponent, + headerComponentParams: enabledComponentParams, + resizable: true, + cellClass: 'nf-cell-vert-align tCol', + sortable: true, + filter: true, + sortColumn: this.sort.bind(this), + }, + { + colId: 'createdAt', + field: 'createdAt', + headerName: 'Created At', + headerComponent: TableColumnDefaultComponent, + headerComponentParams: createdAtHeaderComponentParams, + valueFormatter: this.createdAtFormatter, + resizable: true, + cellClass: 'nf-cell-vert-align tCol', + } + ]; + } + + getData(filters?: FilterObj[], sort?: any) { + // we can customize filters or sorting here before moving on... + return super.getTableData(this.resourceType, this.paging, filters, sort) + .then((results: any) => { + return this.processData(results); + }); + } + + validate = (formData): Promise => { + return Promise.resolve({ passed: true}); + } + + private processData(results: any) { + if (!isEmpty(results?.data)) { + //pre-process data before rendering + results.data = this.addActionsPerRow(results); + } + return results; + } + + private addActionsPerRow(results: any): any[] { + return results.data.map((row) => { + row.actionList = ['update', 'verify', 'delete',]; + return row; + }); + } + + public openUpdate(item?: any) { + this.modalType = 'cas'; + if (item) { + this.selectedConfig = item; + this.selectedConfig.badges = []; + unset(this.selectedConfig, '_links'); + } else { + this.selectedConfig = new Service(); + } + this.sideModalOpen = true; + } + + public verifyCert(itemId, basePath?) { + basePath = basePath ? basePath : this.basePath; + this.router?.navigateByUrl(`${basePath}/${itemId}/verify`); + } +} diff --git a/projects/ziti-console-lib/src/lib/services/validation.service.ts b/projects/ziti-console-lib/src/lib/services/validation.service.ts index 5baaaf88..0e8e09df 100644 --- a/projects/ziti-console-lib/src/lib/services/validation.service.ts +++ b/projects/ziti-console-lib/src/lib/services/validation.service.ts @@ -93,21 +93,8 @@ export class ValidationService { } isValidPEM(cert) { - const header = '-----BEGIN CERTIFICATE-----'; - const footer = '-----END CERTIFICATE-----'; - - if (!cert.startsWith(header) || !cert.endsWith(footer)) { - return false; - } - - const base64Content = cert.slice(header.length, -footer.length).trim(); - - const isBase64 = (str) => { - const base64Pattern = /^(?:[A-Z0-9+/=]+\n)*[A-Z0-9+/=]+$/i; - return base64Pattern.test(str); - }; - - return isBase64(base64Content); + const pemRegex = /^-----BEGIN CERTIFICATE-----([A-Za-z0-9+/=\n\r]+)-----END CERTIFICATE-----$/; + return pemRegex.test(cert); } isValidURI(uri) { diff --git a/projects/ziti-console-lib/src/lib/services/ziti-controller-data.service.ts b/projects/ziti-console-lib/src/lib/services/ziti-controller-data.service.ts index 2cbdd8b6..f05e3c99 100644 --- a/projects/ziti-console-lib/src/lib/services/ziti-controller-data.service.ts +++ b/projects/ziti-console-lib/src/lib/services/ziti-controller-data.service.ts @@ -42,13 +42,19 @@ export class ZitiControllerDataService extends ZitiDataService { super(logger, growler, settingsService, httpClient, router); } - post(type, model, chained = false): Promise { + post(type, model, chained = false, contentType?): Promise { const apiVersions = this.settingsService.apiVersions || {}; const prefix = apiVersions["edge-management"]?.v1?.path; const url = this.settingsService.settings.selectedEdgeController; const serviceUrl = url + prefix + "/" + type; - - return firstValueFrom(this.httpClient.post(serviceUrl, model, {}).pipe( + let options = {}; + if (contentType) { + const headers = { + 'Content-Type': 'text/plain' + } + options = { headers }; + } + return firstValueFrom(this.httpClient.post(serviceUrl, model, options).pipe( catchError((err: any) => { const error = "Server Not Accessible"; if (err.code !== "ECONNREFUSED") throw(err); diff --git a/projects/ziti-console-lib/src/lib/services/ziti-data.service.ts b/projects/ziti-console-lib/src/lib/services/ziti-data.service.ts index e8060699..b5b9dcbb 100644 --- a/projects/ziti-console-lib/src/lib/services/ziti-data.service.ts +++ b/projects/ziti-console-lib/src/lib/services/ziti-data.service.ts @@ -52,7 +52,7 @@ export abstract class ZitiDataService { protected router: Router ) {} - abstract post(type, model, chained?): Promise; + abstract post(type, model, chained?, contentType?): Promise; abstract put(type, model, id, chained?): Promise; abstract patch(type, model, id, chained?): Promise; abstract get(type: string, paging: any, filters: FilterObj[], url?): Promise; @@ -86,4 +86,20 @@ export abstract class ZitiDataService { } return filters; } + + getErrorMessage(resp) { + let errorMessage; + if (resp?.error?.error?.message) { + errorMessage = resp?.error?.error?.message; + } else if (resp?.error?.error?.cause?.message) { + errorMessage = resp?.error?.error?.cause?.message; + } else if (resp?.error?.error?.cause?.reason) { + errorMessage = resp?.error?.error?.cause?.reason; + }else if (resp?.error?.message) { + errorMessage = resp?.error?.message; + } else { + errorMessage = 'An unknown error occurred'; + } + return errorMessage; + } } diff --git a/projects/ziti-console-lib/src/lib/shared-assets/styles/global.scss b/projects/ziti-console-lib/src/lib/shared-assets/styles/global.scss index 113cfc54..57c7433b 100644 --- a/projects/ziti-console-lib/src/lib/shared-assets/styles/global.scss +++ b/projects/ziti-console-lib/src/lib/shared-assets/styles/global.scss @@ -649,6 +649,15 @@ lib-form-field-container { background-color: var(--navigation); } } + +a { + &.download-button { + &:hover { + color: var(--white) + } + } +} + .download-button { display: flex; flex-direction: row; @@ -656,7 +665,7 @@ lib-form-field-container { justify-content: space-between; width: 100%; background-color: var(--primaryColor); - color: var(--background); + color: var(--white); text-transform: uppercase; font-size: 0.875rem; font-weight: 600; @@ -670,6 +679,7 @@ lib-form-field-container { outline: 0; filter: brightness(90%); box-shadow: 0 0.125rem 0.1875rem 0 var(--primaryColorOpaque); + color: var(--white); } &:active { @@ -693,8 +703,8 @@ lib-form-field-container { z-index: 10; } + .verify-certificate, .tap-to-download { - font-family: zac!important; speak: none; font-style: normal; font-weight: 400; @@ -707,10 +717,21 @@ lib-form-field-container { -moz-osx-font-smoothing: grayscale; position: relative; width: 2.8125rem; - margin-right: 0.625rem; background-size: contain; background-repeat: no-repeat; + } + .verify-certificate { + font-family: icomoon !important; + margin-left: var(--marginMedium); + &:before { + content: "\e958"; + } + } + + .tap-to-download { + font-family: zac !important; + margin-right: var(--marginMedium); &:before { content: "\e92d"; } @@ -1036,6 +1057,18 @@ lib-tag-selector { transition: 0.5s; left: 0; + &.form-toggle-switch-inverse { + .form-toggle-indicator { + border-color: var(--red); + } + + &.toggle-right { + .form-toggle-indicator { + border-color: var(--green); + } + } + } + &.toggle-right { left: 0.625rem; @@ -1097,6 +1130,11 @@ lib-tag-selector { .form-field-title { color: var(--offWhite); } + + &.disabled { + opacity: .8; + pointer-events: none; + } } .loginForm { @@ -1538,3 +1576,109 @@ p-dropdown { border: none; } } + +.radio-group-container { + display: flex; + gap: var(--marginLarge); + + &:focus { + .radio-button-container { + .radio-button-circle { + border-color: var(--primaryColor); + } + } + } + + .radio-button-container { + display: flex; + flex-direction: row; + align-items: center; + background-color: var(--formGroup); + border-radius: var(--inputBorderRadius); + width: 100%; + padding-left: var(--paddingMedium); + padding-right: var(--paddingMedium); + padding-top: var(--paddingLarge); + padding-bottom: var(--paddingLarge); + cursor: pointer; + gap: var(--marginMedium); + + .radio-button-circle { + display: flex; + width: 1.3125rem; + height: 1.3125rem; + border-radius: 1.3125rem; + border-width: 0.125rem; + border-style: solid; + border-color: var(--offWhite); + align-items: center; + justify-content: center; + background-color: var(--offWhite); + + .radio-button-inner-circle { + display: none; + width: 0.9375rem; + height: 0.9375rem; + border-radius: 0.9375rem; + align-items: center; + justify-content: center; + background-color: var(--primaryColor); + } + } + + &.selected { + .radio-button-circle { + .radio-button-inner-circle { + display: flex; + } + } + } + + &:active { + .radio-button-circle { + .radio-button-inner-circle { + display: flex; + background-color: var(--menu); + } + } + } + + .radio-button-label { + color: var(--offWhite); + font-size: 0.875rem; + font-weight: 600; + } + } +} + +.api-data-no-wrap{ + .jse-text-mode { + width: 100%; + .cm-editor { + width: 100%; + .cm-scroller { + width: 100%; + .cm-content { + width: 100%; + overflow: auto; + .ͼr { + white-space: nowrap; + } + } + } + } + } +} + +.copy-icon { + background-image: url(/assets/svgs/copy.svg); + background-size: 1.125rem; + background-repeat: no-repeat; + height: 1.25rem; + width: 1.25rem; + margin-left: var(--marginSmall);; +} + +.error-text { + color: var(--red); +} \ No newline at end of file diff --git a/projects/ziti-console-lib/src/lib/ziti-console-lib.module.ts b/projects/ziti-console-lib/src/lib/ziti-console-lib.module.ts index b8f8baa4..16dfe32e 100644 --- a/projects/ziti-console-lib/src/lib/ziti-console-lib.module.ts +++ b/projects/ziti-console-lib/src/lib/ziti-console-lib.module.ts @@ -113,7 +113,12 @@ import {JwtSignersPageComponent} from "./pages/jwt-signers/jwt-signers-page.comp import {JwtSignerFormComponent} from "./features/projectable-forms/jwt-signer/jwt-signer-form.component"; import {AuthPoliciesPageComponent} from "./pages/auth-policies/auth-policies-page.component"; import {AuthPolicyFormComponent} from "./features/projectable-forms/auth-policy/auth-policy-form.component"; +import {CertificateAuthoritiesPageComponent} from "./pages/certificate-authorities/certificate-authorities-page.component"; import {PreviewSelectionsComponent} from "./features/preview-selections/preview-selections.component"; +import {CertificateAuthorityFormComponent} from "./features/projectable-forms/certificate-authority/certificate-authority-form.component"; +import { + VerifyCertificateComponent +} from "./features/projectable-forms/certificate-authority/verify-certificate/verify-certificate.component"; export function playerFactory() { return import(/* webpackChunkName: 'lottie-web' */ 'lottie-web'); @@ -198,7 +203,10 @@ export function playerFactory() { JwtSignerFormComponent, AuthPoliciesPageComponent, AuthPolicyFormComponent, - PreviewSelectionsComponent + PreviewSelectionsComponent, + CertificateAuthoritiesPageComponent, + CertificateAuthorityFormComponent, + VerifyCertificateComponent ], imports: [ CommonModule, diff --git a/projects/ziti-console-lib/src/public-api.ts b/projects/ziti-console-lib/src/public-api.ts index e3595843..ee3409e5 100644 --- a/projects/ziti-console-lib/src/public-api.ts +++ b/projects/ziti-console-lib/src/public-api.ts @@ -24,6 +24,8 @@ export * from './lib/pages/jwt-signers/jwt-signers-page.component'; export * from './lib/pages/jwt-signers/jwt-signers-page.service'; export * from './lib/pages/auth-policies/auth-policies-page.component'; export * from './lib/pages/auth-policies/auth-policies-page.service'; +export * from './lib/pages/certificate-authorities/certificate-authorities-page.component'; +export * from './lib/pages/certificate-authorities/certificate-authorities-page.service'; export * from './lib/services/login-service.class'; export * from './lib/services/noop-login.service'; export * from './lib/services/settings-service.class'; @@ -81,6 +83,9 @@ export * from './lib/features/projectable-forms/jwt-signer/jwt-signer-form.servi export * from './lib/features/projectable-forms/jwt-signer/jwt-signer-form.component'; export * from './lib/features/projectable-forms/auth-policy/auth-policy-form.service'; export * from './lib/features/projectable-forms/auth-policy/auth-policy-form.component'; +export * from './lib/features/projectable-forms/certificate-authority/certificate-authority-form.service'; +export * from './lib/features/projectable-forms/certificate-authority/certificate-authority-form.component'; +export * from './lib/features/projectable-forms/certificate-authority/verify-certificate/verify-certificate.component'; export * from './lib/features/reset-enrollment/reset-enrollment.service'; export * from './lib/features/extendable/extensions-noop.service'; export * from './lib/features/projectable-forms/form-header/form-header.component'; diff --git a/release-notes.md b/release-notes.md index 60aba326..b09b9f44 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,6 +1,5 @@ # app-ziti-console-v3.7.1 # ziti-console-lib-v0.7.1 - ## Bug Fixes * [Issue #605](https://github.com/openziti/ziti-console/issues/605) - Remove delay in username & password change events during login * [Issue #607](https://github.com/openziti/ziti-console/issues/607) - Network Visualizer: Fix in node type name, color and subgroup size