parent
53ce4de6aa
commit
2cbdecb043
23 changed files with 584 additions and 116 deletions
|
@ -545,6 +545,18 @@ a:focus {
|
|||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.config-service-key-field-element {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.config-service-key-field-element .co-modify-link {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.config-service-key-field-element .fa-check {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.co-checkbox {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -1457,4 +1469,11 @@ a:focus {
|
|||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.co-option-table .help-text {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
|
||||
|
|
11
static/css/directives/ui/request-service-key-dialog.css
Normal file
11
static/css/directives/ui/request-service-key-dialog.css
Normal file
|
@ -0,0 +1,11 @@
|
|||
.request-service-key-dialog-element .co-option-table {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.request-service-key-dialog-element .key-display {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
font-family: Menlo,Monaco,Consolas,"Courier New",monospace;
|
||||
background: white;
|
||||
min-height: 500px;
|
||||
}
|
29
static/directives/config/config-service-key-field.html
Normal file
29
static/directives/config/config-service-key-field.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
<div class="config-service-key-field-element">
|
||||
<!-- Loading -->
|
||||
<div class="cor-loader" ng-if="loading"></div>
|
||||
|
||||
<!-- Loading error -->
|
||||
<div class="co-alert co-alert-warning" ng-if="loadError">
|
||||
Could not load service keys
|
||||
</div>
|
||||
|
||||
<!-- Key config -->
|
||||
<div ng-show="!loading && !loadError">
|
||||
<div ng-show="hasValidKey">
|
||||
<i class="fa fa-check"></i>
|
||||
Valid key for service <code>{{ serviceName }}</code> exists
|
||||
</div>
|
||||
<div ng-show="!hasValidKey">
|
||||
No valid key found for service <code>{{ serviceName }}</code>
|
||||
<a class="co-modify-link" ng-click="showRequestServiceKey()">Create Key</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note: This field is a hidden text field that binds to a model that is set to non-empty
|
||||
when there is a valid key. This allows us to use the existing Angular form validation
|
||||
code.
|
||||
-->
|
||||
<input type="text" ng-model="hasValidKeyStr" ng-required="true" style="position: absolute; top: 0px; left: 0px; visibility: hidden; width: 0px; height: 0px;">
|
||||
|
||||
<div class="request-service-key-dialog" request-key-info="requestKeyInfo" key-created="handleKeyCreated(key)"></div>
|
||||
</div>
|
|
@ -286,6 +286,53 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Security Scanner -->
|
||||
<div class="co-panel">
|
||||
<div class="co-panel-heading">
|
||||
<i class="fa fa-bug"></i> Security Scanner
|
||||
</div>
|
||||
<div class="co-panel-body">
|
||||
<div class="description">
|
||||
<p>If enabled, all images pushed to Quay will be scanned via the external security scanning service, with vulnerability information available in the UI and API, as well
|
||||
as async notification support.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="co-checkbox">
|
||||
<input id="ftsecurity" type="checkbox" ng-model="config.FEATURE_SECURITY_SCANNER">
|
||||
<label for="ftsecurity">Enable Security Scanning</label>
|
||||
</div>
|
||||
|
||||
<div class="co-alert co-alert-info" ng-if="config.FEATURE_SECURITY_SCANNER" style="margin-top: 20px;">
|
||||
A scanner compliant with the Quay Security Scanning API must be running to use this feature. Documentation on running <a href="https://github.com/coreos/clair" ng-safenewtab>Clair</a> can be found at <a href="https://tectonic.com/quay-enterprise/docs/latest/clair.html" ng-safenewtab>Running Clair Security Scanner</a>.
|
||||
</div>
|
||||
|
||||
<table class="config-table" ng-if="config.FEATURE_SECURITY_SCANNER">
|
||||
<tr>
|
||||
<td>Security Scanner Endpoint:</td>
|
||||
<td>
|
||||
<span class="config-string-field" binding="config.SECURITY_SCANNER_ENDPOINT"
|
||||
placeholder="Security Scanner API endpoint (Example: http://myhost:6060)"
|
||||
pattern="http(s)?://.+"></span>
|
||||
<div class="help-text">
|
||||
The HTTP URL at which the security scanner is running.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Authentication Key:</td>
|
||||
<td>
|
||||
<span class="config-service-key-field" service-name="{{ config.SECURITY_SCANNER_ISSUER_NAME }}"></span>
|
||||
<div class="help-text">
|
||||
The security scanning service requires an authorized service key to speak to Quay. Once setup, the key
|
||||
can be managed in the Service Keys panel under the Super User Admin Panel.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ACI Conversion -->
|
||||
<div class="co-panel">
|
||||
<div class="co-panel-heading">
|
||||
|
|
134
static/directives/request-service-key-dialog.html
Normal file
134
static/directives/request-service-key-dialog.html
Normal file
|
@ -0,0 +1,134 @@
|
|||
<div class="request-service-key-dialog-element">
|
||||
<!-- Modal message dialog -->
|
||||
<div class="co-dialog modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true" ng-show="!working">×</button>
|
||||
<h4 class="modal-title">Create key for service {{ requestKeyInfo.service }}</h4>
|
||||
</div>
|
||||
|
||||
<div class="modal-body" ng-show="working">
|
||||
<div class="cor-loader"></div>
|
||||
</div>
|
||||
|
||||
<div class="modal-body" ng-show="!working">
|
||||
<!-- Step 0 -->
|
||||
<div ng-show="step == 0">
|
||||
<table class="co-option-table">
|
||||
<tr>
|
||||
<td><input type="radio" id="automaticKey" ng-model="requestKind" value="automatic"></td>
|
||||
<td>
|
||||
<label for="automaticKey">Have the service provide a key</label>
|
||||
<div class="help-text">Recommended for <code>{{ requestKeyInfo.service }}</code> installations where the single instance is setup now.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><input type="radio" id="presharedKey" ng-model="requestKind" value="preshared"></td>
|
||||
<td>
|
||||
<label for="presharedKey">Generate shared key</label>
|
||||
<div class="help-text">Recommended for <code>{{ requestKeyInfo.service }}</code> installations where the instances are dynamically started.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Step 1 (automatic) -->
|
||||
<div ng-show="step == 1 && requestKind == 'automatic'" style="text-align: center">
|
||||
<div style="margin-top: 20px;">
|
||||
Please start the <code>{{ requestKeyInfo.service }}</code> service now, configured for <a href="https://github.com/coreos/jwtproxy#autogenerated-private-key" ng-safenewtab>autogenerated private key</a>. The key approval process will continue automatically once the service connects to Quay.
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
Waiting for service to connect
|
||||
</div>
|
||||
<div style="margin-top: 10px; margin-bottom: 20px;">
|
||||
<div class="cor-loader-inline"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 (automatic) -->
|
||||
<div ng-show="step == 2 && requestKind == 'automatic'" style="text-align: center">
|
||||
A key for service <code>{{ requestKeyInfo.service }}</code> has been automatically generated, approved and saved in the service's keystore.
|
||||
</div>
|
||||
|
||||
<!-- Step 1 (generate) -->
|
||||
<div ng-show="step == 1 && requestKind == 'preshared'">
|
||||
<form name="createForm" ng-submit="createPresharedKey()">
|
||||
<table class="co-form-table">
|
||||
<tr>
|
||||
<td><label for="create-key-name">Key Name:</label></td>
|
||||
<td>
|
||||
<input class="form-control" name="create-key-name" type="text" ng-model="preshared.name" placeholder="Friendly Key Name">
|
||||
<span class="co-help-text">
|
||||
A friendly name for the key for later reference.
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="create-key-expiration">Expiration date (optional):</label></td>
|
||||
<td>
|
||||
<span class="datetime-picker" datetime="preshared.expiration"></span>
|
||||
<span class="co-help-text">
|
||||
The date and time that the key expires. If left blank, the key will never expire.
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="create-key-notes">Approval Notes (optional):</label></td>
|
||||
<td>
|
||||
<div class="markdown-editor" content="preshared.notes"></div>
|
||||
<span class="co-help-text">
|
||||
Optional notes for additional human-readable information about why the key was created.
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 (generate) -->
|
||||
<div ng-show="step == 2 && requestKind == 'preshared'">
|
||||
<div class="co-alert co-alert-warning">
|
||||
The following key has been generated for service <code>{{ requestKeyInfo.service }}</code>.
|
||||
<br><br>
|
||||
Please copy the key's ID and copy/download the key's private contents and place it in the directory with the service's configuration.
|
||||
<br><br>
|
||||
<strong>Once this dialog is closed this private key will not be accessible anywhere else!</strong>
|
||||
</div>
|
||||
|
||||
<label>Key ID:</label>
|
||||
<div class="copy-box" value="createdKey.kid"></div>
|
||||
|
||||
<label>Private Key (PEM):</label>
|
||||
<textarea class="key-display form-control" onclick="this.focus();this.select()" readonly>{{ createdKey.private_key }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" ng-show="!working">
|
||||
<button type="button" class="btn btn-primary" ng-show="step == 1 && requestKind == 'preshared'"
|
||||
ng-disabled="createForm.$invalid"
|
||||
ng-click="createPresharedKey()">
|
||||
Generate Key
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-primary" ng-show="step == 0 && requestKind == 'preshared'"
|
||||
ng-click="showGenerate()">
|
||||
Continue
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-primary" ng-show="step == 0 && requestKind == 'automatic'"
|
||||
ng-click="startApproval()">
|
||||
Start Approval
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-primary" ng-click="downloadPrivateKey(createdKey)" ng-if="createdKey && isDownloadSupported()">
|
||||
<i class="fa fa-download"></i> Download Private Key
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" ng-show="step == 2">Close</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" ng-show="step != 2">Cancel</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
</div>
|
||||
</div>
|
|
@ -61,6 +61,10 @@ angular.module("core-config-setup", ['angularFileUpload'])
|
|||
|
||||
{'id': 'gitlab-trigger', 'title': 'GitLab Build Triggers', 'condition': function(config) {
|
||||
return config.FEATURE_GITLAB_BUILD;
|
||||
}},
|
||||
|
||||
{'id': 'security-scanner', 'title': 'Quay Security Scanner', 'condition': function(config) {
|
||||
return config.FEATURE_SECURITY_SCANNER;
|
||||
}}
|
||||
];
|
||||
|
||||
|
@ -1029,6 +1033,87 @@ angular.module("core-config-setup", ['angularFileUpload'])
|
|||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('configServiceKeyField', function (ApiService) {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/config/config-service-key-field.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'serviceName': '@serviceName',
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.foundKeys = [];
|
||||
$scope.loading = false;
|
||||
$scope.loadError = false;
|
||||
$scope.hasValidKey = false;
|
||||
$scope.hasValidKeyStr = null;
|
||||
|
||||
$scope.updateKeys = function() {
|
||||
$scope.foundKeys = [];
|
||||
$scope.loading = true;
|
||||
|
||||
ApiService.listServiceKeys().then(function(resp) {
|
||||
$scope.loading = false;
|
||||
$scope.loadError = false;
|
||||
|
||||
resp['keys'].forEach(function(key) {
|
||||
if (key['service'] == $scope.serviceName) {
|
||||
$scope.foundKeys.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.hasValidKey = checkValidKey($scope.foundKeys);
|
||||
$scope.hasValidKeyStr = $scope.hasValidKey ? 'true' : '';
|
||||
}, function() {
|
||||
$scope.loading = false;
|
||||
$scope.loadError = true;
|
||||
});
|
||||
};
|
||||
|
||||
// Perform initial loading of the keys.
|
||||
$scope.updateKeys();
|
||||
|
||||
$scope.isKeyExpired = function(key) {
|
||||
if (key.expiration_date != null) {
|
||||
var expiration_date = moment(key.expiration_date);
|
||||
return moment().isAfter(expiration_date);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.showRequestServiceKey = function() {
|
||||
$scope.requestKeyInfo = {
|
||||
'service': $scope.serviceName
|
||||
};
|
||||
};
|
||||
|
||||
$scope.handleKeyCreated = function() {
|
||||
$scope.updateKeys();
|
||||
};
|
||||
|
||||
var checkValidKey = function(keys) {
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
var key = keys[i];
|
||||
if (!key.approval) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($scope.isKeyExpired(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('configStringField', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
|
120
static/js/directives/ui/request-service-key-dialog.js
Normal file
120
static/js/directives/ui/request-service-key-dialog.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
/**
|
||||
* An element which displays a dialog for requesting or creating a service key.
|
||||
*/
|
||||
angular.module('quay').directive('requestServiceKeyDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/request-service-key-dialog.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'requestKeyInfo': '=requestKeyInfo',
|
||||
'keyCreated': '&keyCreated'
|
||||
},
|
||||
controller: function($scope, $element, $timeout, ApiService) {
|
||||
var handleNewKey = function(key) {
|
||||
var data = {
|
||||
'notes': 'Approved during setup of service ' + key.service
|
||||
};
|
||||
|
||||
var params = {
|
||||
'kid': key.kid
|
||||
};
|
||||
|
||||
ApiService.approveServiceKey(data, params).then(function(resp) {
|
||||
$scope.keyCreated({'key': key});
|
||||
$scope.step = 2;
|
||||
}, ApiService.errorDisplay('Could not approve service key'));
|
||||
};
|
||||
|
||||
var checkKeys = function() {
|
||||
var isShown = ($element.find('.modal').data('bs.modal') || {}).isShown;
|
||||
if (!isShown) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: filter by service.
|
||||
ApiService.listServiceKeys().then(function(resp) {
|
||||
var keys = resp['keys'];
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
var key = keys[i];
|
||||
if (key.service == $scope.requestKeyInfo.service && !key.approval && key.rotation_duration) {
|
||||
handleNewKey(key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$timeout(checkKeys, 1000);
|
||||
}, ApiService.errorDisplay('Could not list service keys'));
|
||||
};
|
||||
|
||||
$scope.show = function() {
|
||||
$scope.working = false;
|
||||
$scope.step = 0;
|
||||
$scope.requestKind = null;
|
||||
$scope.preshared = {
|
||||
'name': $scope.requestKeyInfo.service + ' Service Key',
|
||||
'notes': 'Created during setup for service `' + $scope.requestKeyInfo.service + '`'
|
||||
};
|
||||
|
||||
$element.find('.modal').modal({});
|
||||
};
|
||||
|
||||
$scope.hide = function() {
|
||||
$scope.loading = false;
|
||||
$element.find('.modal').modal('hide');
|
||||
};
|
||||
|
||||
$scope.showGenerate = function() {
|
||||
$scope.step = 1;
|
||||
};
|
||||
|
||||
$scope.startApproval = function() {
|
||||
$scope.step = 1;
|
||||
checkKeys();
|
||||
};
|
||||
|
||||
$scope.isDownloadSupported = function() {
|
||||
var isSafari = /^((?!chrome).)*safari/i.test(navigator.userAgent);
|
||||
if (isSafari) {
|
||||
// Doesn't work properly in Safari, sadly.
|
||||
return false;
|
||||
}
|
||||
|
||||
try { return !!new Blob(); } catch(e) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.downloadPrivateKey = function(key) {
|
||||
var blob = new Blob([key.private_key]);
|
||||
saveAs(blob, key.service + '.pem');
|
||||
};
|
||||
|
||||
$scope.createPresharedKey = function() {
|
||||
$scope.working = true;
|
||||
|
||||
var data = {
|
||||
'name': $scope.preshared.name,
|
||||
'service': $scope.requestKeyInfo.service,
|
||||
'expiration': $scope.preshared.expiration || null,
|
||||
'notes': $scope.preshared.notes
|
||||
};
|
||||
|
||||
ApiService.createServiceKey(data).then(function(resp) {
|
||||
$scope.working = false;
|
||||
$scope.step = 2;
|
||||
$scope.createdKey = resp;
|
||||
$scope.keyCreated({'key': resp});
|
||||
}, ApiService.errorDisplay('Could not create service key'));
|
||||
};
|
||||
|
||||
$scope.$watch('requestKeyInfo', function(info) {
|
||||
if (info && info.service) {
|
||||
$scope.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -1,13 +1,12 @@
|
|||
(function() {
|
||||
/**
|
||||
* The Setup page provides a nice GUI walkthrough experience for setting up the Enterprise
|
||||
* Registry.
|
||||
* The Setup page provides a nice GUI walkthrough experience for setting up Quay Enterprise.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('setup', 'setup.html', SetupCtrl,
|
||||
{
|
||||
'newLayout': true,
|
||||
'title': 'Enterprise Registry Setup'
|
||||
'title': 'Quay Enterprise Setup'
|
||||
})
|
||||
}]);
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
(function() {
|
||||
/**
|
||||
* The superuser admin page provides a new management UI for the Enterprise Registry.
|
||||
* The superuser admin page provides a new management UI for Quay Enterprise.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('superuser', 'super-user.html', SuperuserCtrl,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="page-content" quay-show="Features.SUPER_USERS && currentStep == States.CONFIG">
|
||||
<div class="cor-title">
|
||||
<span class="cor-title-link"></span>
|
||||
<span class="cor-title-content">Enterprise Registry Setup</span>
|
||||
<span class="cor-title-content">Quay Enterprise Setup</span>
|
||||
</div>
|
||||
|
||||
<div class="cor-tab-panel" style="padding: 20px;">
|
||||
|
|
Reference in a new issue