Merge pull request #2550 from coreos-inc/signing-ui

Basic signing support UI
This commit is contained in:
josephschorr 2017-04-20 18:03:08 -04:00 committed by GitHub
commit c8136f2fe5
26 changed files with 478 additions and 22 deletions

View file

@ -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]) {

View file

@ -0,0 +1,65 @@
<div class="repository-signing-config-element" quay-require="['SIGNING']">
<div class="repository-events-table-element">
<div class="co-panel">
<div class="co-panel-heading">
<i class="fa ci-shield-check-full"></i> Trust and Signing
</div>
<div class="panel-body">
<table>
<tr>
<td>
<i class="fa status-icon"
ng-class="{'ci-shield-check-outline': $ctrl.repository.trust_enabled, 'ci-shield-none': !$ctrl.repository.trust_enabled}"></i>
</td>
<td>
<div ng-if="$ctrl.repository.trust_enabled">
<h4>Content Trust Enabled</h4>
<p>
Content Trust and Signing is enabled on this repository and all tag operations must be signed via Docker Content Trust.
</p>
<p>
Note that due to this feature being enabled, all UI-based tag operations and all build support is <strong>disabled on this repository</strong>.
</p>
<button class="btn btn-danger" ng-click="$ctrl.askChangeTrust(false)">Disable Content Trust</button>
</div>
<div ng-if="!$ctrl.repository.trust_enabled">
<h4>Content Trust Disabled</h4>
<p>
Content Trust and Signing is disabled on this repository.
</p>
<button class="btn btn-default" ng-click="$ctrl.askChangeTrust(true)">Enable Content Trust</button>
</div>
</td>
</tr>
</table>
</div>
</div>
<!-- Change trust dialogs -->
<div class="cor-confirm-dialog"
dialog-context="$ctrl.enableTrustInfo"
dialog-action="$ctrl.changeTrust(true, callback)"
dialog-title="Enable Content Trust"
dialog-action-title="Enable Trust">
<p>Click "Enable Trust" to enable content trust on this repository.</p>
<p>Please note that at this time, having content trust will <strong>disable</strong> the following
features under the repository:
<ul>
<li>Any tag operations in the UI (Add Tag, Delete Tag, Restore Tag)
<li>All build triggers and ability to invoke builds
</ul>
</p>
</div>
<div class="cor-confirm-dialog"
dialog-context="$ctrl.disableTrustInfo"
dialog-action="$ctrl.changeTrust(false, callback)"
dialog-title="Disable Content Trust"
dialog-action-title="Disable Trust and Delete Data">
<div class="co-alert co-alert-warning">
<strong>Warning:</strong> Disabling content trust will prevent users from pushing signed
manifests to this repository and will <strong>delete all existing signing and trust data</strong>.
</div>
</div>
</div>

View file

@ -0,0 +1,44 @@
import { Input, Component, Inject } from 'ng-metadata/core';
import { Repository } from '../../../types/common.types';
/**
* A component that displays the configuration and options for repository signing.
*/
@Component({
selector: 'repository-signing-config',
templateUrl: '/static/js/directives/ui/repository-signing-config/repository-signing-config.component.html',
})
export class RepositorySigningConfigComponent {
@Input('<') public repository: Repository;
private enableTrustInfo: {[key: string]: string} = null;
private disableTrustInfo: {[key: string]: string} = null;
constructor (@Inject("ApiService") private ApiService: any) {
}
private askChangeTrust(newState: boolean) {
if (newState) {
this.enableTrustInfo = {};
} else {
this.disableTrustInfo = {};
}
}
private changeTrust(newState: boolean, callback: (success: boolean) => void) {
var params = {
'repository': this.repository.namespace + '/' + this.repository.name,
};
var data = {
'trust_enabled': newState,
};
var errorDisplay = this.ApiService.errorDisplay('Could not just change trust', callback);
this.ApiService.changeRepoTrust(data, params).then((resp) => {
this.repository.trust_enabled = newState;
callback(true);
}, errorDisplay);
}
}

View file

@ -35,6 +35,15 @@ angular.module('quay').directive('tagOperationsDialog', function () {
});
};
$scope.alertOnTrust = function() {
if ($scope.repository.trust_enabled) {
$('#trustEnabledModal').modal('show');
return true;
}
return false;
};
$scope.isAnotherImageTag = function(image, tag) {
if (!$scope.repository) { return; }
@ -53,6 +62,9 @@ angular.module('quay').directive('tagOperationsDialog', function () {
$scope.createOrMoveTag = function(image, tag) {
if (!$scope.repository.can_write) { return; }
if ($scope.alertOnTrust()) {
return;
}
$scope.addingTag = true;
@ -77,6 +89,8 @@ angular.module('quay').directive('tagOperationsDialog', function () {
};
$scope.deleteMultipleTags = function(tags, callback) {
if (!$scope.repository.can_write) { return; }
var count = tags.length;
var perform = function(index) {
if (index >= count) {
@ -221,18 +235,30 @@ angular.module('quay').directive('tagOperationsDialog', function () {
$scope.actionHandler = {
'askDeleteTag': function(tag) {
if ($scope.alertOnTrust()) {
return;
}
$scope.deleteTagInfo = {
'tag': tag
};
},
'askDeleteMultipleTags': function(tags) {
if ($scope.alertOnTrust()) {
return;
}
$scope.deleteMultipleTagsInfo = {
'tags': tags
};
},
'askAddTag': function(image) {
if ($scope.alertOnTrust()) {
return;
}
$scope.tagToCreate = '';
$scope.toTagImage = image;
$scope.addingTag = false;
@ -264,6 +290,10 @@ angular.module('quay').directive('tagOperationsDialog', function () {
},
'askRestoreTag': function(tag, image_id, opt_manifest_digest) {
if ($scope.alertOnTrust()) {
return;
}
if (tag.image_id == image_id) {
bootbox.alert('This is the current image for the tag');
return;

View file

@ -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>

View file

@ -0,0 +1,78 @@
import { Input, Component, Inject } from 'ng-metadata/core';
import { ApostilleSignatureDocument, ApostilleTagDocument } from '../../../types/common.types';
import * as moment from "moment";
/**
* 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';
}
}
}