diff --git a/static/css/core-ui.css b/static/css/core-ui.css
index 36b2ce342..1c4672081 100644
--- a/static/css/core-ui.css
+++ b/static/css/core-ui.css
@@ -1170,6 +1170,10 @@ a:focus {
visibility: hidden;
}
+.co-table thead td.unorderable-col:after {
+ display: none;
+}
+
.co-table thead td.current:after {
content: "\f175";
visibility: visible;
diff --git a/static/css/directives/repo-view/repo-panel-tags.css b/static/css/directives/repo-view/repo-panel-tags.css
index 75d65b75e..7e03596bf 100644
--- a/static/css/directives/repo-view/repo-panel-tags.css
+++ b/static/css/directives/repo-view/repo-panel-tags.css
@@ -81,6 +81,10 @@
margin-right: 2px;
}
+.repo-panel-tags-element .signing-col {
+ text-align: center;
+}
+
.repo-panel-tags-element .security-scan-col span {
cursor: pointer;
}
diff --git a/static/css/directives/ui/tag-signing-display.css b/static/css/directives/ui/tag-signing-display.css
new file mode 100644
index 000000000..d05d4608b
--- /dev/null
+++ b/static/css/directives/ui/tag-signing-display.css
@@ -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;
+}
\ No newline at end of file
diff --git a/static/directives/repo-view/repo-panel-tags.html b/static/directives/repo-view/repo-panel-tags.html
index 46d248fa9..154dcfd97 100644
--- a/static/directives/repo-view/repo-panel-tags.html
+++ b/static/directives/repo-view/repo-panel-tags.html
@@ -85,13 +85,17 @@
Tag
|
+ Sign
+ |
Last Modified
|
Security Scan
@@ -124,6 +128,11 @@
|
|
|
+
+
+ |
diff --git a/static/js/directives/repo-view/repo-panel-tags.js b/static/js/directives/repo-view/repo-panel-tags.js
index 159a61d9a..7bb43555e 100644
--- a/static/js/directives/repo-view/repo-panel-tags.js
+++ b/static/js/directives/repo-view/repo-panel-tags.js
@@ -39,9 +39,26 @@ angular.module('quay').directive('repoPanelTags', function () {
$scope.labelCache = {};
$scope.imageVulnerabilities = {};
+ $scope.repoSignatureInfo = null;
+
$scope.defcon1 = {};
$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() {
if (!$scope.repository || !$scope.selectedTags) { return; }
@@ -190,6 +207,7 @@ angular.module('quay').directive('repoPanelTags', function () {
// Process each of the tags.
setTagState();
+ loadRepoSignatures();
});
$scope.loadImageVulnerabilities = function(image_id, imageData) {
@@ -244,7 +262,7 @@ angular.module('quay').directive('repoPanelTags', function () {
$scope.getImageVulnerabilities = function(image_id) {
if (!$scope.repository) {
- return
+ return;
}
if (!$scope.imageVulnerabilities[image_id]) {
diff --git a/static/js/directives/ui/tag-signing-display/tag-signing-display.component.html b/static/js/directives/ui/tag-signing-display/tag-signing-display.component.html
new file mode 100644
index 000000000..be57ff502
--- /dev/null
+++ b/static/js/directives/ui/tag-signing-display/tag-signing-display.component.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/static/js/directives/ui/tag-signing-display/tag-signing-display.component.ts b/static/js/directives/ui/tag-signing-display/tag-signing-display.component.ts
new file mode 100644
index 000000000..75c068fc2
--- /dev/null
+++ b/static/js/directives/ui/tag-signing-display/tag-signing-display.component.ts
@@ -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';
+ }
+ }
+}
\ No newline at end of file
diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts
index 91139a7a4..8538957d5 100644
--- a/static/js/quay.module.ts
+++ b/static/js/quay.module.ts
@@ -14,6 +14,7 @@ import { VisibilityIndicatorComponent } from './directives/ui/visibility-indicat
import { CorTableComponent } from './directives/ui/cor-table/cor-table.component';
import { CorTableColumn } from './directives/ui/cor-table/cor-table-col.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 { AvatarServiceImpl } from './services/avatar/avatar.service.impl';
import { DockerfileServiceImpl } from './services/dockerfile/dockerfile.service.impl';
@@ -44,6 +45,7 @@ import { QuayRequireDirective } from './directives/structural/quay-require/quay-
CorTableColumn,
ChannelIconComponent,
QuayRequireDirective,
+ TagSigningDisplayComponent,
],
providers: [
ViewArrayImpl,
|