Add basic signing UI to repo tags view
This commit is contained in:
parent
9601fd44f6
commit
dec14647a8
8 changed files with 241 additions and 2 deletions
|
@ -1170,6 +1170,10 @@ a:focus {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.co-table thead td.unorderable-col:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.co-table thead td.current:after {
|
.co-table thead td.current:after {
|
||||||
content: "\f175";
|
content: "\f175";
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
|
|
@ -81,6 +81,10 @@
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo-panel-tags-element .signing-col {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.repo-panel-tags-element .security-scan-col span {
|
.repo-panel-tags-element .security-scan-col span {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
55
static/css/directives/ui/tag-signing-display.css
Normal file
55
static/css/directives/ui/tag-signing-display.css
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
.tag-signing-display-element {
|
||||||
|
text-align: center;
|
||||||
|
display: inline-block;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element .fa {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element .fa.fa-question-circle {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element .signing-load-error {
|
||||||
|
color: #CCC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element .signing-not-signed {
|
||||||
|
color: #9B9B9B;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element .signing-valid .okay,
|
||||||
|
.tag-signing-display-element .signing-valid .expires-soon {
|
||||||
|
color: #2FC98E;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.tag-signing-display-element .signing-valid .expires-soon {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element .signing-valid .expires-soon:after {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0px;
|
||||||
|
z-index: 1;
|
||||||
|
display: inline-block;
|
||||||
|
content: " ";
|
||||||
|
background-color: #FCA657;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.tag-signing-display-element .signing-valid .expired {
|
||||||
|
color: #FCA657;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element .signing-invalid {
|
||||||
|
color: #D64456;
|
||||||
|
}
|
|
@ -85,13 +85,17 @@
|
||||||
<td ng-class="tablePredicateClass('name', options.predicate, options.reverse)">
|
<td ng-class="tablePredicateClass('name', options.predicate, options.reverse)">
|
||||||
<a ng-click="orderBy('name')">Tag</a>
|
<a ng-click="orderBy('name')">Tag</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="hidden-xs unorderable-col"
|
||||||
|
style="width: 46px;"
|
||||||
|
quay-require="['SIGNING']"
|
||||||
|
ng-if="repository.trust_enabled">Sign
|
||||||
|
</td>
|
||||||
<td class="hidden-xs"
|
<td class="hidden-xs"
|
||||||
ng-class="tablePredicateClass('last_modified_datetime', options.predicate, options.reverse)"
|
ng-class="tablePredicateClass('last_modified_datetime', options.predicate, options.reverse)"
|
||||||
style="width: 140px;">
|
style="width: 140px;">
|
||||||
<a ng-click="orderBy('last_modified_datetime')">Last Modified</a>
|
<a ng-click="orderBy('last_modified_datetime')">Last Modified</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden-xs"
|
<td class="hidden-xs"
|
||||||
ng-class="tablePredicateClass('security_scanned', options.predicate, options.reverse)"
|
|
||||||
style="width: 200px;"
|
style="width: 200px;"
|
||||||
quay-require="['SECURITY_SCANNER']">
|
quay-require="['SECURITY_SCANNER']">
|
||||||
Security Scan
|
Security Scan
|
||||||
|
@ -124,6 +128,11 @@
|
||||||
<tr ng-class="expandedView ? 'expanded-view': ''">
|
<tr ng-class="expandedView ? 'expanded-view': ''">
|
||||||
<td><span class="cor-checkable-item" controller="checkedTags" item="tag"></span></td>
|
<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></td>
|
<td class="co-flowing-col"><span class="tag-span"><span bo-text="tag.name"></span></span></td>
|
||||||
|
<td class="signing-col hidden-xs"
|
||||||
|
quay-require="['SIGNING']"
|
||||||
|
ng-if="repository.trust_enabled">
|
||||||
|
<tag-signing-display tag="tag" signatures="repoSignatureInfo"></tag-signing-display>
|
||||||
|
</td>
|
||||||
<td class="hidden-xs">
|
<td class="hidden-xs">
|
||||||
<span bo-if="tag.last_modified" data-title="{{ tag.last_modified | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}" bs-tooltip>
|
<span bo-if="tag.last_modified" data-title="{{ tag.last_modified | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}" bs-tooltip>
|
||||||
<span am-time-ago="tag.last_modified"></span>
|
<span am-time-ago="tag.last_modified"></span>
|
||||||
|
|
|
@ -39,9 +39,26 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
$scope.labelCache = {};
|
$scope.labelCache = {};
|
||||||
|
|
||||||
$scope.imageVulnerabilities = {};
|
$scope.imageVulnerabilities = {};
|
||||||
|
$scope.repoSignatureInfo = null;
|
||||||
|
|
||||||
$scope.defcon1 = {};
|
$scope.defcon1 = {};
|
||||||
$scope.hasDefcon1 = false;
|
$scope.hasDefcon1 = false;
|
||||||
|
|
||||||
|
var loadRepoSignatures = function() {
|
||||||
|
$scope.repoSignatureError = false;
|
||||||
|
$scope.repoSignatureInfo = null;
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.getRepoSignatures(null, params).then(function(resp) {
|
||||||
|
$scope.repoSignatureInfo = resp;
|
||||||
|
}, function() {
|
||||||
|
$scope.repoSignatureInfo = {'error': true};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
var setTagState = function() {
|
var setTagState = function() {
|
||||||
if (!$scope.repository || !$scope.selectedTags) { return; }
|
if (!$scope.repository || !$scope.selectedTags) { return; }
|
||||||
|
|
||||||
|
@ -190,6 +207,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
|
|
||||||
// Process each of the tags.
|
// Process each of the tags.
|
||||||
setTagState();
|
setTagState();
|
||||||
|
loadRepoSignatures();
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.loadImageVulnerabilities = function(image_id, imageData) {
|
$scope.loadImageVulnerabilities = function(image_id, imageData) {
|
||||||
|
@ -244,7 +262,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
|
|
||||||
$scope.getImageVulnerabilities = function(image_id) {
|
$scope.getImageVulnerabilities = function(image_id) {
|
||||||
if (!$scope.repository) {
|
if (!$scope.repository) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$scope.imageVulnerabilities[image_id]) {
|
if (!$scope.imageVulnerabilities[image_id]) {
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
<span class="tag-signing-display-element">
|
||||||
|
<span ng-switch on="$ctrl.signingStatus($ctrl.tag, $ctrl.signatures)">
|
||||||
|
<!-- Loading -->
|
||||||
|
<span ng-switch-when="loading">
|
||||||
|
<span class="cor-loader-inline"></span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Error -->
|
||||||
|
<span class="signing-load-error" ng-switch-when="error"
|
||||||
|
data-title="Could not load signing information" bs-tooltip>
|
||||||
|
<i class="fa fa-question-circle"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Not Signed -->
|
||||||
|
<span class="signing-not-signed" ng-switch-when="not-signed"
|
||||||
|
data-title="This tag has not been signed" bs-tooltip>
|
||||||
|
<i class="fa shield-icon ci-shield-none"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Signature Valid -->
|
||||||
|
<span class="signing-valid" ng-switch-when="valid-signature">
|
||||||
|
<span ng-switch on="$ctrl.expirationStatus($ctrl.tag, $ctrl.signatures)">
|
||||||
|
<!-- But expired -->
|
||||||
|
<span class="expired" ng-switch-when="expired"
|
||||||
|
data-title="This tag has a matching, but expired signature" bs-tooltip>
|
||||||
|
<i class="fa shield-icon ci-shield-invalid-outline"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Expires soon -->
|
||||||
|
<span class="expires-soon" ng-switch-when="expires-soon"
|
||||||
|
data-title="This tag has a valid and matching signature, but it is expiring soon on {{ $ctrl.signatures.expiration }}" bs-tooltip>
|
||||||
|
<i class="fa shield-icon ci-shield-check-outline"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Okay -->
|
||||||
|
<span class="okay" ng-switch-when="okay"
|
||||||
|
data-title="This tag has a valid and matching signature" bs-tooltip>
|
||||||
|
<i class="fa shield-icon ci-shield-check-outline"></i>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Signature Invalid -->
|
||||||
|
<span class="signing-invalid" ng-switch-when="invalid-signature"
|
||||||
|
data-title="The signed digest for this tag does not match the one pushed.<br><br>Signed: {{ this.signedDigest.substr(0, 12) }}<br>Pushed: {{ this.pushedDigest.substr(0, 12) }}" data-html="true" bs-tooltip>
|
||||||
|
<i class="fa shield-icon ci-shield-invalid-outline"></i>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { Input, Component, Inject } from 'ng-metadata/core';
|
||||||
|
import * as moment from "moment";
|
||||||
|
|
||||||
|
interface ApostilleSignatureDocument {
|
||||||
|
// When the signed document expires.
|
||||||
|
expiration: string
|
||||||
|
|
||||||
|
// Object of information for each tag.
|
||||||
|
tags: {string: ApostilleTagDocument}
|
||||||
|
|
||||||
|
// If true, an error occurred while trying to load this document.
|
||||||
|
error: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApostilleTagDocument {
|
||||||
|
// The length of the document.
|
||||||
|
length: number
|
||||||
|
|
||||||
|
// The hashes for the tag.
|
||||||
|
hashes: {string: string}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component that displays the signing status of a tag in the repository view.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'tag-signing-display',
|
||||||
|
templateUrl: '/static/js/directives/ui/tag-signing-display/tag-signing-display.component.html',
|
||||||
|
})
|
||||||
|
export class TagSigningDisplayComponent {
|
||||||
|
@Input('<') public tag: any;
|
||||||
|
@Input('=') public signatures: ApostilleSignatureDocument;
|
||||||
|
|
||||||
|
private signedDigest: string;
|
||||||
|
private pushedDigest: string;
|
||||||
|
|
||||||
|
constructor (@Inject("$sanitize") private $sanitize: ng.sanitize.ISanitizeService) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private base64ToHex(base64String: string): string {
|
||||||
|
// Based on: http://stackoverflow.com/questions/39460182/decode-base64-to-hexadecimal-string-with-javascript
|
||||||
|
var raw = atob(base64String);
|
||||||
|
var hexString = '';
|
||||||
|
for (var i = 0; i < raw.length; ++i) {
|
||||||
|
var char = raw.charCodeAt(i);
|
||||||
|
var hex = char.toString(16)
|
||||||
|
hexString += (hex.length == 2 ? hex : '0' + hex);
|
||||||
|
}
|
||||||
|
return hexString;
|
||||||
|
}
|
||||||
|
|
||||||
|
private expirationStatus(tag: any, signatures: ApostilleSignatureDocument): string {
|
||||||
|
if (!signatures || !signatures.expiration) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
var expires = moment(signatures.expiration);
|
||||||
|
var now = moment();
|
||||||
|
|
||||||
|
if (expires.isSameOrBefore(now)) {
|
||||||
|
return 'expired';
|
||||||
|
}
|
||||||
|
|
||||||
|
var withOneWeek = moment().add('1', 'w');
|
||||||
|
if (expires.isSameOrBefore(withOneWeek)) {
|
||||||
|
return 'expires-soon';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'okay';
|
||||||
|
}
|
||||||
|
|
||||||
|
private signingStatus(tag: any, signatures: ApostilleSignatureDocument): string {
|
||||||
|
if (!tag || !signatures) {
|
||||||
|
return 'loading';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signatures.error || !signatures.tags) {
|
||||||
|
return 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
var tag_info = signatures.tags[tag.name];
|
||||||
|
if (!tag_info || !tag.manifest_digest) {
|
||||||
|
return 'not-signed';
|
||||||
|
}
|
||||||
|
|
||||||
|
var digest_without_prefix = tag.manifest_digest.substr('sha256:'.length);
|
||||||
|
var hex_signature = this.base64ToHex(tag_info.hashes['sha256']);
|
||||||
|
|
||||||
|
if (hex_signature == digest_without_prefix) {
|
||||||
|
return 'valid-signature';
|
||||||
|
} else {
|
||||||
|
this.signedDigest = this.$sanitize(hex_signature);
|
||||||
|
this.pushedDigest = this.$sanitize(digest_without_prefix);
|
||||||
|
return 'invaid-signature';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import { VisibilityIndicatorComponent } from './directives/ui/visibility-indicat
|
||||||
import { CorTableComponent } from './directives/ui/cor-table/cor-table.component';
|
import { CorTableComponent } from './directives/ui/cor-table/cor-table.component';
|
||||||
import { CorTableColumn } from './directives/ui/cor-table/cor-table-col.component';
|
import { CorTableColumn } from './directives/ui/cor-table/cor-table-col.component';
|
||||||
import { ChannelIconComponent } from './directives/ui/channel-icon/channel-icon.component';
|
import { ChannelIconComponent } from './directives/ui/channel-icon/channel-icon.component';
|
||||||
|
import { TagSigningDisplayComponent } from './directives/ui/tag-signing-display/tag-signing-display.component';
|
||||||
import { BuildServiceImpl } from './services/build/build.service.impl';
|
import { BuildServiceImpl } from './services/build/build.service.impl';
|
||||||
import { AvatarServiceImpl } from './services/avatar/avatar.service.impl';
|
import { AvatarServiceImpl } from './services/avatar/avatar.service.impl';
|
||||||
import { DockerfileServiceImpl } from './services/dockerfile/dockerfile.service.impl';
|
import { DockerfileServiceImpl } from './services/dockerfile/dockerfile.service.impl';
|
||||||
|
@ -44,6 +45,7 @@ import { QuayRequireDirective } from './directives/structural/quay-require/quay-
|
||||||
CorTableColumn,
|
CorTableColumn,
|
||||||
ChannelIconComponent,
|
ChannelIconComponent,
|
||||||
QuayRequireDirective,
|
QuayRequireDirective,
|
||||||
|
TagSigningDisplayComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ViewArrayImpl,
|
ViewArrayImpl,
|
||||||
|
|
Reference in a new issue