Implement updated UI for displaying the signing status of a tag, now that we support multiple delegations
The icon now represents the status of the multiple delegations, and we show each delegation in the "Expanded" view.
This commit is contained in:
parent
7c6196f78a
commit
b7f88d2df2
9 changed files with 368 additions and 99 deletions
|
@ -27,9 +27,4 @@ class RepositorySignatures(RepositoryParamResource):
|
||||||
if repo is None or not repo.trust_enabled:
|
if repo is None or not repo.trust_enabled:
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
tag_data, expiration = tuf_metadata_api.get_default_tags_with_expiration(namespace, repository)
|
return {'delegations': tuf_metadata_api.get_all_tags_with_expiration(namespace, repository)}
|
||||||
return {
|
|
||||||
'tags': tag_data,
|
|
||||||
'expiration': expiration
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
|
|
||||||
.repo-panel-tags-element .image-track-line.start {
|
.repo-panel-tags-element .image-track-line.start {
|
||||||
top: 18px;
|
top: 18px;
|
||||||
height: 25px;
|
height: 28px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,6 +144,10 @@
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo-panel-tags-element .signing-delegations-list {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
@media (max-width: 1000px) {
|
||||||
.repo-panel-tags-element .image-track {
|
.repo-panel-tags-element .image-track {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -1,5 +1,17 @@
|
||||||
.manifest-label-list-element {
|
.manifest-label-list-element {
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manifest-label-list-element:before {
|
||||||
|
content: "\f02c";
|
||||||
|
font-family: FontAwesome;
|
||||||
|
position: absolute;
|
||||||
|
left: -22px;
|
||||||
|
top: 0px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
.manifest-label-list-element .none {
|
.manifest-label-list-element .none {
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-signing-display-element .fa {
|
.tag-signing-display-element .fa {
|
||||||
font-size: 18px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-signing-display-element .fa.fa-question-circle {
|
.tag-signing-display-element .fa.fa-question-circle {
|
||||||
|
@ -22,34 +23,145 @@
|
||||||
color: #9B9B9B;
|
color: #9B9B9B;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-signing-display-element .signing-valid .okay,
|
.tag-signing-display-element .signing-valid.okay-release {
|
||||||
.tag-signing-display-element .signing-valid .expires-soon {
|
|
||||||
color: #2FC98E;
|
color: #2FC98E;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element .signing-valid.okay {
|
||||||
.tag-signing-display-element .signing-valid .expires-soon {
|
color: #5f9dd0;
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-signing-display-element .signing-valid .expires-soon:after {
|
.tag-signing-display-element .signing-valid.partial-okay {
|
||||||
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;
|
color: #FCA657;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-signing-display-element .signing-invalid {
|
.tag-signing-display-element .signing-invalid {
|
||||||
color: #D64456;
|
color: #D64456;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element .indicator {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element .expiring-soon {
|
||||||
|
border-radius: 100%;
|
||||||
|
background-color: #ffe0c4;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element .expired {
|
||||||
|
border-radius: 100%;
|
||||||
|
background-color: #ffcad1;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element .invalid {
|
||||||
|
border-radius: 100%;
|
||||||
|
background-color: #ffcad1;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element.extended {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element.extended .fa {
|
||||||
|
color: #888 !important;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element.extended .indicator {
|
||||||
|
font-size: 16px;
|
||||||
|
position: absolute;
|
||||||
|
left: -22px;
|
||||||
|
top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element.extended .delegations {
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 6px;
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element.extended .delegations td {
|
||||||
|
padding: 4px;
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element.extended .delegations .delegation {
|
||||||
|
padding: 4px;
|
||||||
|
background-color: #eee;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
padding-right: 6px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element.extended .delegations .delegation:before {
|
||||||
|
content: "\f00c";
|
||||||
|
font-family: FontAwesome;
|
||||||
|
margin-right: 2px;
|
||||||
|
margin-left: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element.extended .delegations .delegation.okay {
|
||||||
|
background-color: #bdf1dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element.extended .delegations .delegation.okay:before {
|
||||||
|
color: #2FC98E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element.extended .delegations .delegation.warning {
|
||||||
|
background-color: #ffe0c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element.extended .delegations .delegation.warning:before {
|
||||||
|
content: "\f12a";
|
||||||
|
color: #FCA657;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element.extended .delegations .delegation.error {
|
||||||
|
background-color: #ffcad1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element.extended .delegations .delegation.error:before {
|
||||||
|
content: "\f00d";
|
||||||
|
color: #D64456;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element.extended .delegations .delegation-name {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-signing-display-element.extended .delegations .delegation-info {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -131,7 +131,7 @@
|
||||||
<td class="signing-col hidden-xs"
|
<td class="signing-col hidden-xs"
|
||||||
quay-require="['SIGNING']"
|
quay-require="['SIGNING']"
|
||||||
ng-if="repository.trust_enabled">
|
ng-if="repository.trust_enabled">
|
||||||
<tag-signing-display tag="tag" signatures="repoSignatureInfo"></tag-signing-display>
|
<tag-signing-display tag="tag" delegations="repoDelegationsInfo" compact="true"></tag-signing-display>
|
||||||
</td>
|
</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>
|
||||||
|
@ -261,9 +261,15 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="expandedView">
|
<tr ng-if="expandedView">
|
||||||
<td class="checkbox-col"></td>
|
<td class="checkbox-col"></td>
|
||||||
<td class="labels-col" colspan="{{5 + (Features.SECURITY_SCANNER ? 1 : 0)}}">
|
<td class="labels-col" colspan="{{5 + (Features.SECURITY_SCANNER ? 1 : 0) + (repository.trust_enabled ? 1 : 0) }}">
|
||||||
|
<!-- Labels -->
|
||||||
<div class="manifest-label-list" repository="repository"
|
<div class="manifest-label-list" repository="repository"
|
||||||
manifest-digest="tag.manifest_digest" cache="labelCache"></div>
|
manifest-digest="tag.manifest_digest" cache="labelCache"></div>
|
||||||
|
|
||||||
|
<!-- Delegations -->
|
||||||
|
<div class="signing-delegations-list" ng-if="repository.trust_enabled">
|
||||||
|
<tag-signing-display compact="false" tag="tag" delegations="repoDelegationsInfo"></tag-signing-display>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden-xs hidden-sm image-track" ng-repeat="it in imageTracks"
|
<td class="hidden-xs hidden-sm image-track" ng-repeat="it in imageTracks"
|
||||||
ng-if="imageTracks.length <= maxTrackCount" bindonce>
|
ng-if="imageTracks.length <= maxTrackCount" bindonce>
|
||||||
|
|
|
@ -39,7 +39,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
$scope.labelCache = {};
|
$scope.labelCache = {};
|
||||||
|
|
||||||
$scope.imageVulnerabilities = {};
|
$scope.imageVulnerabilities = {};
|
||||||
$scope.repoSignatureInfo = null;
|
$scope.repoDelegationsInfo = null;
|
||||||
|
|
||||||
$scope.defcon1 = {};
|
$scope.defcon1 = {};
|
||||||
$scope.hasDefcon1 = false;
|
$scope.hasDefcon1 = false;
|
||||||
|
@ -50,16 +50,16 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.repoSignatureError = false;
|
$scope.repoSignatureError = false;
|
||||||
$scope.repoSignatureInfo = null;
|
$scope.repoDelegationsInfo = null;
|
||||||
|
|
||||||
var params = {
|
var params = {
|
||||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
||||||
};
|
};
|
||||||
|
|
||||||
ApiService.getRepoSignatures(null, params).then(function(resp) {
|
ApiService.getRepoSignatures(null, params).then(function(resp) {
|
||||||
$scope.repoSignatureInfo = resp;
|
$scope.repoDelegationsInfo = resp;
|
||||||
}, function() {
|
}, function() {
|
||||||
$scope.repoSignatureInfo = {'error': true};
|
$scope.repoDelegationsInfo = {'error': true};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<span class="tag-signing-display-element">
|
<span class="tag-signing-display-element" ng-class="{'extended': !$ctrl.compact}">
|
||||||
<span ng-switch on="$ctrl.signingStatus($ctrl.tag, $ctrl.signatures)">
|
<span class="indicator" ng-switch on="$ctrl.signingStatus($ctrl.tag, $ctrl.delegations)">
|
||||||
<!-- Loading -->
|
<!-- Loading -->
|
||||||
<span ng-switch-when="loading">
|
<span ng-switch-when="loading">
|
||||||
<span class="cor-loader-inline"></span>
|
<span class="cor-loader-inline"></span>
|
||||||
|
@ -17,33 +17,80 @@
|
||||||
<i class="fa shield-icon ci-shield-none"></i>
|
<i class="fa shield-icon ci-shield-none"></i>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Signature Valid -->
|
<!-- Releases + all other targets signed -->
|
||||||
<span class="signing-valid" ng-switch-when="valid-signature">
|
<span class="signing-valid okay-release" ng-switch-when="all-signed"
|
||||||
<span ng-switch on="$ctrl.expirationStatus($ctrl.tag, $ctrl.signatures)">
|
data-title="The tag has valid and matching signatures" bs-tooltip>
|
||||||
<!-- But expired -->
|
<i class="fa shield-icon ci-shield-check-full"></i>
|
||||||
<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 ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasExpiringSoon" class="expiring-soon"></span>
|
||||||
<span class="expires-soon" ng-switch-when="expires-soon"
|
<span ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasExpired" class="expired"></span>
|
||||||
data-title="This tag has a valid and matching signature, but it is expiring soon on {{ $ctrl.signatures.expiration }}" bs-tooltip>
|
<span ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasInvalid" class="invalid"></span>
|
||||||
<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>
|
</span>
|
||||||
|
|
||||||
<!-- Signature Invalid -->
|
<!-- Releases target signed -->
|
||||||
<span class="signing-invalid" ng-switch-when="invalid-signature"
|
<span class="signing-valid okay-release" ng-switch-when="release-signed"
|
||||||
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>
|
data-title="The tag has a valid and matching default signature" bs-tooltip>
|
||||||
|
<i class="fa shield-icon ci-shield-check-outline"></i>
|
||||||
|
|
||||||
|
<span ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasExpiringSoon" class="expiring-soon"></span>
|
||||||
|
<span ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasExpired" class="expired"></span>
|
||||||
|
<span ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasInvalid" class="invalid"></span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Non-releases target signed -->
|
||||||
|
<span class="signing-valid okay" ng-switch-when="non-release-signed"
|
||||||
|
data-title="The tag has valid and matching non-default signatures" bs-tooltip>
|
||||||
|
<i class="fa shield-icon ci-shield-check-outline"></i>
|
||||||
|
|
||||||
|
<span ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasExpiringSoon" class="expiring-soon"></span>
|
||||||
|
<span ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasExpired" class="expired"></span>
|
||||||
|
<span ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasInvalid" class="invalid"></span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Non-releases target signed -->
|
||||||
|
<span class="signing-valid partial-okay" ng-switch-when="one-valid-signed"
|
||||||
|
data-title="The tag has at least one valid and matching non-default signature, but some are invalid or have expired" bs-tooltip>
|
||||||
|
<i class="fa shield-icon ci-shield-check-outline"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- All signatures are invalid -->
|
||||||
|
<span class="signing-invalid" ng-switch-when="invalid-signed"
|
||||||
|
data-title="There are no valid or non-expired signatures for this tag" bs-tooltip>
|
||||||
<i class="fa shield-icon ci-shield-invalid-outline"></i>
|
<i class="fa shield-icon ci-shield-invalid-outline"></i>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<!-- Delegations -->
|
||||||
|
<div class="delegations" ng-if="!$ctrl.compact && !$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).delegations.length">
|
||||||
|
This tag has not been signed
|
||||||
|
</div>
|
||||||
|
<table class="delegations" ng-if="!$ctrl.compact && $ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).delegations.length">
|
||||||
|
<tr ng-repeat="delegation in $ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).delegations">
|
||||||
|
<td>
|
||||||
|
<span class="delegation" ng-class="{'okay': delegation.hasMatchingHash && !delegation.isExpired && !delegation.isExpiringSoon, 'warning': delegation.hasMatchingHash && delegation.isExpiringSoon, 'error': !delegation.hasMatchingHash || delegation.isExpired}">
|
||||||
|
<span class="delegation-name">{{ delegation.delegationName.substr('targets/'.length) }}</span>
|
||||||
|
</span>
|
||||||
|
<div class="delegation-info visible-xs" ng-if="!delegation.hasMatchingHash || delegation.isExpiringSoon || delegation.isExpired">
|
||||||
|
<span class="failure-reason" ng-if="delegation.hasMatchingHash && delegation.isExpiringSoon">
|
||||||
|
Expiring soon: {{ delegation.expiration.format("YYYY-MM-DD HH:mm:ss") }}
|
||||||
|
</span>
|
||||||
|
<span class="failure-reason" ng-if="delegation.hasMatchingHash && delegation.isExpired">Signature expired</span>
|
||||||
|
<span class="failure-reason" ng-if="!delegation.hasMatchingHash">
|
||||||
|
Signature has different hash: {{ delegation.delegationHash.substr(0, 12) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="hidden-xs">
|
||||||
|
<span class="delegation-info" ng-if="!delegation.hasMatchingHash || delegation.isExpiringSoon || delegation.isExpired">
|
||||||
|
<span class="failure-reason" ng-if="delegation.hasMatchingHash && delegation.isExpiringSoon">
|
||||||
|
Expiring soon: {{ delegation.expiration.format("YYYY-MM-DD HH:mm:ss") }}
|
||||||
|
</span>
|
||||||
|
<span class="failure-reason" ng-if="delegation.hasMatchingHash && delegation.isExpired">Signature expired</span>
|
||||||
|
<span class="failure-reason" ng-if="!delegation.hasMatchingHash">
|
||||||
|
Signature has different hash: {{ delegation.delegationHash.substr(0, 12) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</span>
|
</span>
|
|
@ -1,7 +1,27 @@
|
||||||
import { Input, Component, Inject } from 'ng-metadata/core';
|
import { Input, Component, Inject } from 'ng-metadata/core';
|
||||||
import { ApostilleSignatureDocument, ApostilleTagDocument } from '../../../types/common.types';
|
import { ApostilleDelegationsSet, ApostilleSignatureDocument, ApostilleTagDocument } from '../../../types/common.types';
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
|
|
||||||
|
type tagSigningInfo = {
|
||||||
|
delegations: delegationInfo[];
|
||||||
|
delegationsByName: {[delegationName: string]: delegationInfo};
|
||||||
|
|
||||||
|
hasExpiringSoon: boolean;
|
||||||
|
hasExpired: boolean;
|
||||||
|
hasInvalid: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type delegationInfo = {
|
||||||
|
delegationName: string;
|
||||||
|
delegationHash: string;
|
||||||
|
expiration: moment.Moment;
|
||||||
|
hasMatchingHash: boolean;
|
||||||
|
isExpired: boolean;
|
||||||
|
isExpiringSoon: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
var RELEASES = 'targets/releases';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A component that displays the signing status of a tag in the repository view.
|
* A component that displays the signing status of a tag in the repository view.
|
||||||
*/
|
*/
|
||||||
|
@ -10,11 +30,11 @@ import * as moment from "moment";
|
||||||
templateUrl: '/static/js/directives/ui/tag-signing-display/tag-signing-display.component.html',
|
templateUrl: '/static/js/directives/ui/tag-signing-display/tag-signing-display.component.html',
|
||||||
})
|
})
|
||||||
export class TagSigningDisplayComponent {
|
export class TagSigningDisplayComponent {
|
||||||
|
@Input('<') public compact: boolean;
|
||||||
@Input('<') public tag: any;
|
@Input('<') public tag: any;
|
||||||
@Input('<') public signatures: ApostilleSignatureDocument;
|
@Input('<') public delegations: ApostilleDelegationsSet;
|
||||||
|
|
||||||
private signedDigest: string;
|
private cachedSigningInfo: tagSigningInfo | null = null;
|
||||||
private pushedDigest: string;
|
|
||||||
|
|
||||||
constructor(@Inject("$sanitize") private $sanitize: ng.sanitize.ISanitizeService) {}
|
constructor(@Inject("$sanitize") private $sanitize: ng.sanitize.ISanitizeService) {}
|
||||||
|
|
||||||
|
@ -30,49 +50,115 @@ export class TagSigningDisplayComponent {
|
||||||
return hexString;
|
return hexString;
|
||||||
}
|
}
|
||||||
|
|
||||||
private expirationStatus(tag: any, signatures: ApostilleSignatureDocument): string {
|
private buildDelegationInfo(tag: any, delegationName: string, delegation: ApostilleSignatureDocument): delegationInfo {
|
||||||
if (!signatures || !signatures.expiration) {
|
var digest_without_prefix = tag.manifest_digest.substr('sha256:'.length);
|
||||||
return 'unknown';
|
var hex_signature = this.base64ToHex(delegation.targets[tag.name].hashes['sha256']);
|
||||||
}
|
|
||||||
|
|
||||||
var expires = moment(signatures.expiration);
|
var expires = moment(delegation.expiration);
|
||||||
var now = moment();
|
var now = moment();
|
||||||
|
|
||||||
if (expires.isSameOrBefore(now)) {
|
|
||||||
return 'expired';
|
|
||||||
}
|
|
||||||
|
|
||||||
var withOneWeek = moment().add('1', 'w');
|
var withOneWeek = moment().add('1', 'w');
|
||||||
if (expires.isSameOrBefore(withOneWeek)) {
|
|
||||||
return 'expires-soon';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'okay';
|
return {
|
||||||
|
'delegationName': delegationName,
|
||||||
|
'hasMatchingHash': digest_without_prefix == hex_signature,
|
||||||
|
'expiration': expires,
|
||||||
|
'delegationHash': hex_signature,
|
||||||
|
'isExpired': expires.isSameOrBefore(now),
|
||||||
|
'isExpiringSoon': !expires.isSameOrBefore(now) && expires.isSameOrBefore(withOneWeek),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private signingStatus(tag: any, signatures: ApostilleSignatureDocument): string {
|
private buildTagSigningInfo(tag: any, delegationSet: ApostilleDelegationsSet): tagSigningInfo {
|
||||||
if (!tag || !signatures) {
|
var info = {
|
||||||
|
'delegations': [],
|
||||||
|
'delegationsByName': {},
|
||||||
|
'hasExpired': false,
|
||||||
|
'hasExpiringSoon': false,
|
||||||
|
'hasInvalid': false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all delegations containing the tag as a target.
|
||||||
|
Object.keys(delegationSet.delegations).forEach((delegationName) => {
|
||||||
|
var delegation = delegationSet.delegations[delegationName];
|
||||||
|
if (delegation.targets[tag.name]) {
|
||||||
|
var delegationInfo = this.buildDelegationInfo(tag, delegationName, delegation)
|
||||||
|
info.delegations.push(delegationInfo);
|
||||||
|
info.delegationsByName[delegationName] = delegationInfo;
|
||||||
|
|
||||||
|
if (delegationInfo.isExpired) {
|
||||||
|
info.hasExpired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delegationInfo.isExpiringSoon) {
|
||||||
|
info.hasExpiringSoon = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!delegationInfo.hasMatchingHash) {
|
||||||
|
info.hasInvalid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSigningInfo(tag: any, delegationSet: ApostilleDelegationsSet): tagSigningInfo {
|
||||||
|
if (!this.cachedSigningInfo) {
|
||||||
|
this.cachedSigningInfo = this.buildTagSigningInfo(tag, delegationSet);
|
||||||
|
}
|
||||||
|
return this.cachedSigningInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private signingStatus(tag: any, delegationSet: ApostilleDelegationsSet): string {
|
||||||
|
if (!tag || !delegationSet) {
|
||||||
return 'loading';
|
return 'loading';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signatures.error || !signatures.tags) {
|
if (!tag.manifest_digest) {
|
||||||
return 'error';
|
|
||||||
}
|
|
||||||
|
|
||||||
var tag_info = signatures.tags[tag.name];
|
|
||||||
if (!tag_info || !tag.manifest_digest) {
|
|
||||||
return 'not-signed';
|
return 'not-signed';
|
||||||
}
|
}
|
||||||
|
|
||||||
var digest_without_prefix = tag.manifest_digest.substr('sha256:'.length);
|
if (delegationSet.error) {
|
||||||
var hex_signature = this.base64ToHex(tag_info.hashes['sha256']);
|
return 'error';
|
||||||
|
|
||||||
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';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if signed at all.
|
||||||
|
var signingInfo = this.getSigningInfo(tag, delegationSet);
|
||||||
|
if (!signingInfo.delegations.length) {
|
||||||
|
return 'not-signed';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all delegations are signed and valid.
|
||||||
|
var allReleasesValid = true;
|
||||||
|
var oneReleaseValid = false;
|
||||||
|
|
||||||
|
this.cachedSigningInfo.delegations.forEach(function(delegation) {
|
||||||
|
var isValid = delegation.hasMatchingHash && !delegation.isExpired;
|
||||||
|
if (isValid) {
|
||||||
|
oneReleaseValid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
allReleasesValid = allReleasesValid && isValid;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if the special RELEASES target is signed and valid.
|
||||||
|
var releasesDelegation = this.cachedSigningInfo.delegationsByName[RELEASES];
|
||||||
|
if (releasesDelegation && releasesDelegation.hasMatchingHash && !releasesDelegation.isExpired) {
|
||||||
|
if (allReleasesValid && this.cachedSigningInfo.delegations.length > 1) {
|
||||||
|
return 'all-signed';
|
||||||
|
} else {
|
||||||
|
return 'release-signed';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allReleasesValid) {
|
||||||
|
return 'non-release-signed';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oneReleaseValid) {
|
||||||
|
return 'one-valid-signed';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'invalid-signed';
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -105,6 +105,16 @@ export type Trigger = {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a set of apostille delegations.
|
||||||
|
*/
|
||||||
|
export type ApostilleDelegationsSet = {
|
||||||
|
delegations: [delegationName: string]: ApostilleSignatureDocument
|
||||||
|
|
||||||
|
// The error that occurred, if any.
|
||||||
|
error: string | null
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an apostille signature document, with extra expiration information.
|
* Represents an apostille signature document, with extra expiration information.
|
||||||
*/
|
*/
|
||||||
|
@ -113,10 +123,7 @@ export type ApostilleSignatureDocument = {
|
||||||
expiration: string
|
expiration: string
|
||||||
|
|
||||||
// Object of information for each tag.
|
// Object of information for each tag.
|
||||||
tags: {string: ApostilleTagDocument}
|
targets: {string: ApostilleTagDocument}
|
||||||
|
|
||||||
// If true, an error occurred while trying to load this document.
|
|
||||||
error: boolean
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
Reference in a new issue