mirror of
https://github.com/quay/quay.git
synced 2025-04-18 10:44:06 +03:00
ui: basic support for cosign in the UI (PROJQUAY-3965) (#1380)
* ui: basic support for cosign in the UI (PROJQUAY-3965) * sharpen regex for cosign manifest tag name convention Co-authored-by: Oleg Bulatov <oleg@bulatov.me> * fix broken signature match * addressing review comments * fix tool tip width issues Co-authored-by: Oleg Bulatov <oleg@bulatov.me>
This commit is contained in:
parent
8f5b14ba2b
commit
d9dbbd88db
@ -130,8 +130,6 @@
|
||||
content:"\f113";
|
||||
}
|
||||
|
||||
|
||||
|
||||
.ci-shield-none:before {
|
||||
content:"\f114";
|
||||
}
|
||||
|
@ -171,6 +171,27 @@
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .signing-valid .tooltip-inner {
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .cosign-signature-row {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 6px;
|
||||
padding-left: 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .cosign-signature-row:before {
|
||||
content: "\f040";
|
||||
font-family: FontAwesome;
|
||||
position: absolute;
|
||||
left: -22px;
|
||||
top: 0px;
|
||||
font-size: 15px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .labels-col {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
@ -10,6 +10,13 @@
|
||||
Expanded
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn" ng-class="!showCosignSignatures ? 'btn-default' : 'btn-primary active'"
|
||||
ng-click="toggleCosignSignatureDisplay()">
|
||||
{{ !showCosignSignatures ? 'Show Signatures' : 'Hide Signatures' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="tab-header"><span class="hidden-xs">Repository </span>Tags</h3>
|
||||
@ -22,9 +29,18 @@
|
||||
<div class="cor-checkable-menu-item" item-filter="noTagFilter(item)">
|
||||
<i class="fa fa-square-o"></i>No Tags
|
||||
</div>
|
||||
<div class="cor-checkable-menu-item" item-filter="signedTagFilter(item)">
|
||||
<i class="fa fa-shield"></i>Signed Tags
|
||||
</div>
|
||||
<div class="cor-checkable-menu-item" item-filter="unsignedTagFilter(item)">
|
||||
<i class="fa ci-shield-invalid-outline"></i>Unsigned Tags
|
||||
</div>
|
||||
<div class="cor-checkable-menu-item" item-filter="commitTagFilter(item)">
|
||||
<i class="fa fa-git"></i>Commit SHAs
|
||||
</div>
|
||||
<div class="cor-checkable-menu-item" item-filter="cosignTagFilter(item)">
|
||||
<i class="fa fa-pencil-square"></i>Cosign Signatures
|
||||
</div>
|
||||
|
||||
<div class="cor-checkable-menu-item" item-filter="manifestDigestFilter(mt.manifest_digest, item)"
|
||||
ng-repeat="mt in manifestTrackEntries" ng-if="::it.visible">
|
||||
@ -132,7 +148,12 @@
|
||||
<tr ng-class="expandedView ? 'expanded-view': ''">
|
||||
<td><span class="cor-checkable-item" controller="checkedTags" item="tag"></span></td>
|
||||
<td class="co-flowing-col">
|
||||
<span class="tag-span"><span bo-text="tag.name"></span></span>
|
||||
<span class="tag-span">
|
||||
<span bo-text="tag.name"></span>
|
||||
<span class="signing-valid" ng-if="::tag.cosign_signature_tag">
|
||||
<i class="fa shield-icon ci-shield-check-full" data-title="This tag has been signed via cosign." bs-tooltip></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="manifest-list-icons" bo-if="tag.is_manifest_list">
|
||||
<i class="manifest-list-manifest-icon fa fa-{{ manifest.os }}"
|
||||
ng-repeat="manifest in manifestsOf(tag)"
|
||||
@ -304,6 +325,11 @@
|
||||
<div class="manifest-label-list" repository="repository"
|
||||
manifest-digest="tag.manifest_digest" cache="labelCache"></div>
|
||||
|
||||
<!-- Cosign Signatures -->
|
||||
<div class="cosign-signature-row" ng-if="tag.cosign_signature_tag">
|
||||
<cosign-link repository="repository" image-id="tag.image_id" manifest-digest="tag.cosign_signature_manifest_digest" tag-name="tag.cosign_signature_tag" tag-name="tag.cosign_signature_tag" signed-image-id="tag.image_id" signed-manifest-digest="tag.manifest_digest"></cosign-link>
|
||||
</div>
|
||||
|
||||
<!-- Delegations -->
|
||||
<div class="signing-delegations-list" ng-if="repository.trust_enabled">
|
||||
<tag-signing-display compact="false" tag="tag" delegations="repoDelegationsInfo"></tag-signing-display>
|
||||
|
@ -43,10 +43,12 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||
$scope.tagsPerPage = 25;
|
||||
|
||||
$scope.expandedView = false;
|
||||
$scope.showCosignSignatures = false;
|
||||
$scope.labelCache = {};
|
||||
|
||||
$scope.manifestVulnerabilities = {};
|
||||
$scope.repoDelegationsInfo = null;
|
||||
$scope.cosignedManifests = {};
|
||||
|
||||
var loadRepoSignatures = function() {
|
||||
if (!$scope.repository || !$scope.repository.trust_enabled) {
|
||||
@ -67,6 +69,35 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||
});
|
||||
};
|
||||
|
||||
var matchCosignSignature = function(tag) {
|
||||
// matches cosign style tags and returns the match with a matching group containing the signed manifest digest
|
||||
|
||||
var cosignNamingPattern = new RegExp('^sha256-([a-f0-9]{64})\.sig$');
|
||||
tag = tag.trim()
|
||||
return tag.match(cosignNamingPattern);
|
||||
}
|
||||
|
||||
var getCosignSignatures = function() {
|
||||
if (!$scope.repositoryTags || !$scope.selectedTags) { return; }
|
||||
|
||||
// Build a list of all digests which are signed by cosign
|
||||
$scope.cosignedManifests = [];
|
||||
|
||||
for (var tag in $scope.repositoryTags) {
|
||||
if (!$scope.repositoryTags.hasOwnProperty(tag)) { continue; }
|
||||
|
||||
var cosignSignatureTag = matchCosignSignature(tag);
|
||||
|
||||
if (cosignSignatureTag) {
|
||||
signedManifestDigest = cosignSignatureTag[1]; // cosign signature tags contain the signature of the signed manifest
|
||||
$scope.cosignedManifests["sha256:" + signedManifestDigest] = { // map signed manifests to their cosign signature artifact
|
||||
'signatureTagName': tag,
|
||||
'signatureManifestDigest': $scope.repositoryTags[tag].manifest_digest
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var setTagState = function() {
|
||||
if (!$scope.repositoryTags || !$scope.selectedTags) { return; }
|
||||
|
||||
@ -75,11 +106,15 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||
for (var tag in $scope.repositoryTags) {
|
||||
if (!$scope.repositoryTags.hasOwnProperty(tag)) { continue; }
|
||||
|
||||
if (matchCosignSignature(tag) && !$scope.showCosignSignatures) { continue; }
|
||||
|
||||
var tagData = $scope.repositoryTags[tag];
|
||||
var tagInfo = $.extend(tagData, {
|
||||
'name': tag,
|
||||
'last_modified_datetime': TableService.getReversedTimestamp(tagData.last_modified),
|
||||
'expiration_date': tagData.expiration ? TableService.getReversedTimestamp(tagData.expiration) : null,
|
||||
'cosign_signature_tag': $scope.cosignedManifests.hasOwnProperty(tagData.manifest_digest) ? $scope.cosignedManifests[tagData.manifest_digest].signatureTagName : null,
|
||||
'cosign_signature_manifest_digest': $scope.cosignedManifests.hasOwnProperty(tagData.manifest_digest) ? $scope.cosignedManifests[tagData.manifest_digest].signatureManifestDigest : null,
|
||||
});
|
||||
|
||||
allTags.push(tagInfo);
|
||||
@ -230,6 +265,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||
$scope.$watch('options.predicate', setTagState);
|
||||
$scope.$watch('options.reverse', setTagState);
|
||||
$scope.$watch('options.filter', setTagState);
|
||||
$scope.$watch('showCosignSignatures', setTagState);
|
||||
|
||||
$scope.$watch('options.page', function(page) {
|
||||
if (page != null && $scope.checkedTags) {
|
||||
@ -247,6 +283,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||
|
||||
$scope.$watch('repository', function(updatedRepoObject, previousRepoObject) {
|
||||
// Process each of the tags.
|
||||
getCosignSignatures();
|
||||
setTagState();
|
||||
loadRepoSignatures();
|
||||
});
|
||||
@ -254,6 +291,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||
$scope.$watch('repositoryTags', function(newTags, oldTags) {
|
||||
if (newTags === oldTags) { return; }
|
||||
// Process each of the tags.
|
||||
getCosignSignatures();
|
||||
setTagState();
|
||||
loadRepoSignatures();
|
||||
}, true);
|
||||
@ -399,6 +437,19 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||
return tag.name.match(r);
|
||||
};
|
||||
|
||||
$scope.cosignTagFilter = function(tag) {
|
||||
var r = new RegExp('^sha256-[A-Fa-f0-9]{64}\.sig$');
|
||||
return tag.name.match(r);
|
||||
};
|
||||
|
||||
$scope.signedTagFilter = function(tag) {
|
||||
return tag.hasOwnProperty("cosign_signature_manifest_digest") && tag.cosign_signature_manifest_digest != null;
|
||||
};
|
||||
|
||||
$scope.unsignedTagFilter = function(tag) {
|
||||
return tag.hasOwnProperty("cosign_signature_manifest_digest") && tag.cosign_signature_manifest_digest == null;
|
||||
};
|
||||
|
||||
$scope.allTagFilter = function(tag) {
|
||||
return true;
|
||||
};
|
||||
@ -438,6 +489,10 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||
$scope.expandedView = expanded;
|
||||
};
|
||||
|
||||
$scope.toggleCosignSignatureDisplay = function() {
|
||||
$scope.showCosignSignatures = !$scope.showCosignSignatures;
|
||||
};
|
||||
|
||||
$scope.getTagNames = function(checked) {
|
||||
var names = checked.map(function(tag) {
|
||||
return tag.name;
|
||||
|
@ -0,0 +1,35 @@
|
||||
<span class="manifest-link">
|
||||
<span class="id-label cas" ng-if="::$ctrl.hasSHA256($ctrl.manifestDigest)"
|
||||
data-title="The artifact containing the cosign signature for this tag."
|
||||
data-container="body"
|
||||
ng-click="$ctrl.showCopyBox()"
|
||||
bs-tooltip>cosign</span>
|
||||
|
||||
<a ng-href="/repository/{{ ::$ctrl.repository.namespace }}/{{ ::$ctrl.repository.name }}/manifest/{{ ::$ctrl.manifestDigest }}">
|
||||
{{ $ctrl.tagName }}
|
||||
</a>
|
||||
|
||||
<span ng-if="::!$ctrl.hasSHA256($ctrl.manifestDigest)">{{ ::$ctrl.imageId.substr(0, 12) }}</span>
|
||||
|
||||
<div class="modal fade co-dialog" ng-if="$ctrl.showingCopyBox">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="$ctrl.hideCopyBox()"
|
||||
aria-hidden="true">×</button>
|
||||
<h4 class="modal-title"><span ng-if="$ctrl.hasSHA256($ctrl.manifestDigest)">Cosign Signature artifact</span><span ng-if="!$ctrl.hasSHA256($ctrl.manifestDigest)">V1 ID</span></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label>Signature digest:</label>
|
||||
<div class="copy-box" value="$ctrl.manifestDigest"></div>
|
||||
|
||||
<label style="margin-top: 20px;">Signature tag:</label>
|
||||
<div class="copy-box" value="$ctrl.tagName"></div>
|
||||
</div>
|
||||
<div class="modal-footer" ng-show="!working">
|
||||
<button type="button" class="btn btn-default" ng-click="$ctrl.hideCopyBox()">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
</span>
|
58
static/js/directives/ui/cosign-link/cosign-link.component.ts
Normal file
58
static/js/directives/ui/cosign-link/cosign-link.component.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { Input, Component, Inject } from 'ng-metadata/core';
|
||||
import { Repository } from '../../../types/common.types';
|
||||
|
||||
|
||||
/**
|
||||
* A component that links to a manifest view.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cosign-link',
|
||||
templateUrl: '/static/js/directives/ui/cosign-link/cosign-link.component.html'
|
||||
})
|
||||
export class CosignLinkComponent {
|
||||
|
||||
@Input('<') public repository: Repository;
|
||||
@Input('<') public manifestDigest: string;
|
||||
@Input('<') public imageId: string;
|
||||
@Input('<') public tagName: string;
|
||||
@Input('<') public signedTagName: string;
|
||||
@Input('<') public signedImageId: string;
|
||||
@Input('<') public signedManifestDigest: string;
|
||||
|
||||
private showingCopyBox: boolean = false;
|
||||
|
||||
constructor(@Inject('$timeout') private $timeout, @Inject('$element') private $element) {
|
||||
}
|
||||
|
||||
private hasSHA256(digest: string) {
|
||||
return digest && digest.indexOf('sha256:') == 0;
|
||||
}
|
||||
|
||||
private getShortDigest(digest: string) {
|
||||
if (!digest) { return ''; }
|
||||
return digest.substr('sha256:'.length).substr(0, 12);
|
||||
}
|
||||
|
||||
private getShortTagName(name: string) {
|
||||
if (!name) { return ''; }
|
||||
return name.substring(0, 'sha256-'.length + 10) + '...' + name.substring(name.length-('.sig').length-10,name.length);
|
||||
}
|
||||
|
||||
private showCopyBox() {
|
||||
this.showingCopyBox = true;
|
||||
|
||||
// Necessary to wait for digest cycle to complete.
|
||||
this.$timeout(() => {
|
||||
this.$element.find('.modal').modal('show');
|
||||
}, 10);
|
||||
};
|
||||
|
||||
private hideCopyBox() {
|
||||
this.$element.find('.modal').modal('hide');
|
||||
|
||||
// Wait for the modal to hide before removing from the DOM.
|
||||
this.$timeout(() => {
|
||||
this.showingCopyBox = false;
|
||||
}, 10);
|
||||
};
|
||||
}
|
@ -77,8 +77,8 @@
|
||||
if (resp.has_additional) {
|
||||
loadPaginatedRepositoryTags(page + 1);
|
||||
} else {
|
||||
$scope.viewScope.tagsLoading = false;
|
||||
}
|
||||
$scope.viewScope.tagsLoading = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -41,6 +41,7 @@ import { TriggerDescriptionComponent } from './directives/ui/trigger-description
|
||||
import { TimeAgoComponent } from './directives/ui/time-ago/time-ago.component';
|
||||
import { TimeDisplayComponent } from './directives/ui/time-display/time-display.component';
|
||||
import { AppSpecificTokenManagerComponent } from './directives/ui/app-specific-token-manager/app-specific-token-manager.component';
|
||||
import { CosignLinkComponent } from './directives/ui/cosign-link/cosign-link.component';
|
||||
import { ManifestLinkComponent } from './directives/ui/manifest-link/manifest-link.component';
|
||||
import { ManifestSecurityView } from './directives/ui/manifest-security-view/manifest-security-view.component';
|
||||
import { MarkdownModule } from './directives/ui/markdown/markdown.module';
|
||||
@ -89,6 +90,7 @@ import * as Clipboard from 'clipboard';
|
||||
TimeAgoComponent,
|
||||
TimeDisplayComponent,
|
||||
AppSpecificTokenManagerComponent,
|
||||
CosignLinkComponent,
|
||||
ManifestLinkComponent,
|
||||
ManifestSecurityView,
|
||||
RepoStateComponent,
|
||||
|
Loading…
x
Reference in New Issue
Block a user