Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Thomas <nick@gitlab.com>2018-11-12 13:52:48 +0300
committerNick Thomas <nick@gitlab.com>2018-11-19 14:46:39 +0300
commitf1bc7b6eb5cb9beab55e4edac87cc5e0b7ceb069 (patch)
tree2e5aedd22e2fd05909c1e97c5bc52602feadc825 /app/assets/javascripts/mirrors
parentb1b4c94484b0613a6a457e32218d4f62b9eb2029 (diff)
SSH public-key authentication for push mirroring
Diffstat (limited to 'app/assets/javascripts/mirrors')
-rw-r--r--app/assets/javascripts/mirrors/constants.js4
-rw-r--r--app/assets/javascripts/mirrors/mirror_repos.js108
-rw-r--r--app/assets/javascripts/mirrors/ssh_mirror.js299
3 files changed, 411 insertions, 0 deletions
diff --git a/app/assets/javascripts/mirrors/constants.js b/app/assets/javascripts/mirrors/constants.js
new file mode 100644
index 00000000000..8dd6a726425
--- /dev/null
+++ b/app/assets/javascripts/mirrors/constants.js
@@ -0,0 +1,4 @@
+export default {
+ PASSWORD: 'password',
+ SSH: 'ssh_public_key',
+};
diff --git a/app/assets/javascripts/mirrors/mirror_repos.js b/app/assets/javascripts/mirrors/mirror_repos.js
new file mode 100644
index 00000000000..0d8f31d6bfc
--- /dev/null
+++ b/app/assets/javascripts/mirrors/mirror_repos.js
@@ -0,0 +1,108 @@
+import $ from 'jquery';
+import _ from 'underscore';
+import { __ } from '~/locale';
+import Flash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import SSHMirror from './ssh_mirror';
+
+export default class MirrorRepos {
+ constructor(container) {
+ this.$container = $(container);
+ this.$password = null;
+ this.$form = $('.js-mirror-form', this.$container);
+ this.$urlInput = $('.js-mirror-url', this.$form);
+ this.$protectedBranchesInput = $('.js-mirror-protected', this.$form);
+ this.$table = $('.js-mirrors-table-body', this.$container);
+ this.mirrorEndpoint = this.$form.data('projectMirrorEndpoint');
+ }
+
+ init() {
+ this.initMirrorPush();
+ this.registerUpdateListeners();
+ }
+
+ initMirrorPush() {
+ this.$passwordGroup = $('.js-password-group', this.$container);
+ this.$password = $('.js-password', this.$passwordGroup);
+ this.$authMethod = $('.js-auth-method', this.$form);
+
+ this.$authMethod.on('change', () => this.togglePassword());
+ this.$password.on('input.updateUrl', () => this.debouncedUpdateUrl());
+
+ this.initMirrorSSH();
+ }
+
+ initMirrorSSH() {
+ if (this.$password) {
+ this.$password.off('input.updateUrl');
+ }
+ this.$password = undefined;
+
+ this.sshMirror = new SSHMirror('.js-mirror-form');
+ this.sshMirror.init();
+ }
+
+ updateUrl() {
+ let val = this.$urlInput.val();
+
+ if (this.$password) {
+ const password = this.$password.val();
+ if (password) val = val.replace('@', `:${password}@`);
+ }
+
+ $('.js-mirror-url-hidden', this.$form).val(val);
+ }
+
+ updateProtectedBranches() {
+ const val = this.$protectedBranchesInput.get(0).checked
+ ? this.$protectedBranchesInput.val()
+ : '0';
+ $('.js-mirror-protected-hidden', this.$form).val(val);
+ }
+
+ registerUpdateListeners() {
+ this.debouncedUpdateUrl = _.debounce(() => this.updateUrl(), 200);
+ this.$urlInput.on('input', () => this.debouncedUpdateUrl());
+ this.$protectedBranchesInput.on('change', () => this.updateProtectedBranches());
+ this.$table.on('click', '.js-delete-mirror', event => this.deleteMirror(event));
+ }
+
+ togglePassword() {
+ const isPassword = this.$authMethod.val() === 'password';
+
+ if (!isPassword) {
+ this.$password.val('');
+ this.updateUrl();
+ }
+ this.$passwordGroup.collapse(isPassword ? 'show' : 'hide');
+ }
+
+ deleteMirror(event, existingPayload) {
+ const $target = $(event.currentTarget);
+ let payload = existingPayload;
+
+ if (!payload) {
+ payload = {
+ project: {
+ remote_mirrors_attributes: {
+ id: $target.data('mirrorId'),
+ enabled: 0,
+ },
+ },
+ };
+ }
+
+ return axios
+ .put(this.mirrorEndpoint, payload)
+ .then(() => this.removeRow($target))
+ .catch(() => Flash(__('Failed to remove mirror.')));
+ }
+
+ /* eslint-disable class-methods-use-this */
+ removeRow($target) {
+ const row = $target.closest('tr');
+ $('.js-delete-mirror', row).tooltip('hide');
+ row.remove();
+ }
+ /* eslint-enable class-methods-use-this */
+}
diff --git a/app/assets/javascripts/mirrors/ssh_mirror.js b/app/assets/javascripts/mirrors/ssh_mirror.js
new file mode 100644
index 00000000000..5bdf5d6277a
--- /dev/null
+++ b/app/assets/javascripts/mirrors/ssh_mirror.js
@@ -0,0 +1,299 @@
+import $ from 'jquery';
+import _ from 'underscore';
+import { __ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+import Flash from '~/flash';
+import { backOff } from '~/lib/utils/common_utils';
+import AUTH_METHOD from './constants';
+
+export default class SSHMirror {
+ constructor(formSelector) {
+ this.backOffRequestCounter = 0;
+
+ this.$form = $(formSelector);
+
+ this.$repositoryUrl = this.$form.find('.js-repo-url');
+ this.$knownHosts = this.$form.find('.js-known-hosts');
+
+ this.$sectionSSHHostKeys = this.$form.find('.js-ssh-host-keys-section');
+ this.$hostKeysInformation = this.$form.find('.js-fingerprint-ssh-info');
+ this.$btnDetectHostKeys = this.$form.find('.js-detect-host-keys');
+ this.$btnSSHHostsShowAdvanced = this.$form.find('.btn-show-advanced');
+ this.$dropdownAuthType = this.$form.find('.js-mirror-auth-type');
+
+ this.$wellAuthTypeChanging = this.$form.find('.js-well-changing-auth');
+ this.$wellPasswordAuth = this.$form.find('.js-well-password-auth');
+ this.$wellSSHAuth = this.$form.find('.js-well-ssh-auth');
+ this.$sshPublicKeyWrap = this.$form.find('.js-ssh-public-key-wrap');
+ this.$regeneratePublicSshKeyButton = this.$wellSSHAuth.find('.js-btn-regenerate-ssh-key');
+ this.$regeneratePublicSshKeyModal = this.$wellSSHAuth.find(
+ '.js-regenerate-public-ssh-key-confirm-modal',
+ );
+ }
+
+ init() {
+ this.handleRepositoryUrlInput(true);
+
+ this.$repositoryUrl.on('keyup', () => this.handleRepositoryUrlInput());
+ this.$knownHosts.on('keyup', e => this.handleSSHKnownHostsInput(e));
+ this.$dropdownAuthType.on('change', e => this.handleAuthTypeChange(e));
+ this.$btnDetectHostKeys.on('click', e => this.handleDetectHostKeys(e));
+ this.$btnSSHHostsShowAdvanced.on('click', e => this.handleSSHHostsAdvanced(e));
+ this.$regeneratePublicSshKeyButton.on('click', () =>
+ this.$regeneratePublicSshKeyModal.toggle(true),
+ );
+ $('.js-confirm', this.$regeneratePublicSshKeyModal).on('click', e =>
+ this.regeneratePublicSshKey(e),
+ );
+ $('.js-cancel', this.$regeneratePublicSshKeyModal).on('click', () =>
+ this.$regeneratePublicSshKeyModal.toggle(false),
+ );
+ }
+
+ /**
+ * Method to monitor Git Repository URL input
+ */
+ handleRepositoryUrlInput(forceMatch) {
+ const protocol = this.$repositoryUrl.val().split('://')[0];
+ const protRegEx = /http|git/;
+
+ // Validate URL and verify if it consists only supported protocols
+ if (forceMatch || this.$form.get(0).checkValidity()) {
+ const isSsh = protocol === 'ssh';
+ // Hide/Show SSH Host keys section only for SSH URLs
+ this.$sectionSSHHostKeys.collapse(isSsh ? 'show' : 'hide');
+ this.$btnDetectHostKeys.enable();
+
+ // Verify if URL is http, https or git and hide/show Auth type dropdown
+ // as we don't support auth type SSH for non-SSH URLs
+ const matchesProtocol = protRegEx.test(protocol);
+ this.$dropdownAuthType.attr('disabled', matchesProtocol);
+
+ if (forceMatch && isSsh) {
+ this.$dropdownAuthType.val(AUTH_METHOD.SSH);
+ this.toggleAuthWell(AUTH_METHOD.SSH);
+ } else {
+ this.$dropdownAuthType.val(AUTH_METHOD.PASSWORD);
+ this.toggleAuthWell(AUTH_METHOD.PASSWORD);
+ }
+ }
+ }
+
+ /**
+ * Click event handler to detect SSH Host key and fingerprints from
+ * provided Git Repository URL.
+ */
+ handleDetectHostKeys() {
+ const projectMirrorSSHEndpoint = this.$form.data('project-mirror-ssh-endpoint');
+ const repositoryUrl = this.$repositoryUrl.val();
+ const currentKnownHosts = this.$knownHosts.val();
+ const $btnLoadSpinner = this.$btnDetectHostKeys.find('.js-spinner');
+
+ // Disable button while we make request
+ this.$btnDetectHostKeys.disable();
+ $btnLoadSpinner.removeClass('d-none');
+
+ // Make backOff polling to get data
+ backOff((next, stop) => {
+ axios
+ .get(
+ `${projectMirrorSSHEndpoint}?ssh_url=${repositoryUrl}&compare_host_keys=${encodeURIComponent(
+ currentKnownHosts,
+ )}`,
+ )
+ .then(({ data, status }) => {
+ if (status === 204) {
+ this.backOffRequestCounter += 1;
+ if (this.backOffRequestCounter < 3) {
+ next();
+ } else {
+ stop(data);
+ }
+ } else {
+ stop(data);
+ }
+ })
+ .catch(stop);
+ })
+ .then(res => {
+ $btnLoadSpinner.addClass('d-none');
+ // Once data is received, we show verification info along with Host keys and fingerprints
+ this.$hostKeysInformation
+ .find('.js-fingerprint-verification')
+ .collapse(res.host_keys_changed ? 'hide' : 'show');
+ if (res.known_hosts && res.fingerprints) {
+ this.showSSHInformation(res);
+ }
+ })
+ .catch(({ response }) => {
+ // Show failure message when there's an error and re-enable Detect host keys button
+ const failureMessage = response.data
+ ? response.data.message
+ : __('An error occurred while detecting host keys');
+ Flash(failureMessage);
+
+ $btnLoadSpinner.addClass('hidden');
+ this.$btnDetectHostKeys.enable();
+ });
+ }
+
+ /**
+ * Method to monitor known hosts textarea input
+ */
+ handleSSHKnownHostsInput() {
+ // Strike-out fingerprints and remove verification info if `known hosts` value is altered
+ this.$hostKeysInformation.find('.js-fingerprints-list').addClass('invalidate');
+ this.$hostKeysInformation.find('.js-fingerprint-verification').collapse('hide');
+ }
+
+ /**
+ * Click event handler for `Show advanced` button under SSH Host keys section
+ */
+ handleSSHHostsAdvanced() {
+ const $knownHost = this.$sectionSSHHostKeys.find('.js-ssh-known-hosts');
+ const toggleShowAdvanced = $knownHost.hasClass('show');
+
+ $knownHost.collapse('toggle');
+ this.$btnSSHHostsShowAdvanced.toggleClass('show-advanced', toggleShowAdvanced);
+ }
+
+ /**
+ * Authentication method dropdown change event listener
+ */
+ handleAuthTypeChange() {
+ const projectMirrorAuthTypeEndpoint = `${this.$form.attr('action')}.json`;
+ const $sshPublicKey = this.$sshPublicKeyWrap.find('.ssh-public-key');
+ const selectedAuthType = this.$dropdownAuthType.val();
+
+ this.$wellPasswordAuth.collapse('hide');
+ this.$wellSSHAuth.collapse('hide');
+
+ // This request should happen only if selected Auth type was SSH
+ // and SSH Public key was not present on page load
+ if (selectedAuthType === AUTH_METHOD.SSH && !$sshPublicKey.text().trim()) {
+ if (!this.$wellSSHAuth.length) return;
+
+ // Construct request body
+ const authTypeData = {
+ project: {
+ ...this.$regeneratePublicSshKeyButton.data().projectData,
+ },
+ };
+
+ this.$wellAuthTypeChanging.collapse('show');
+ this.$dropdownAuthType.disable();
+
+ axios
+ .put(projectMirrorAuthTypeEndpoint, JSON.stringify(authTypeData), {
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ },
+ })
+ .then(({ data }) => {
+ // Show SSH public key container and fill in public key
+ this.toggleAuthWell(selectedAuthType);
+ this.toggleSSHAuthWellMessage(true);
+ this.setSSHPublicKey(data.import_data_attributes.ssh_public_key);
+
+ this.$wellAuthTypeChanging.collapse('hide');
+ this.$dropdownAuthType.enable();
+ })
+ .catch(() => {
+ Flash(__('Something went wrong on our end.'));
+
+ this.$wellAuthTypeChanging.collapse('hide');
+ this.$dropdownAuthType.enable();
+ });
+ } else {
+ this.toggleAuthWell(selectedAuthType);
+ this.$wellSSHAuth.find('.js-ssh-public-key-present').collapse('show');
+ }
+ }
+
+ /**
+ * Method to parse SSH Host keys data and render it
+ * under SSH host keys section
+ */
+ showSSHInformation(sshHostKeys) {
+ const $fingerprintsList = this.$hostKeysInformation.find('.js-fingerprints-list');
+ let fingerprints = '';
+ sshHostKeys.fingerprints.forEach(fingerprint => {
+ const escFingerprints = _.escape(fingerprint.fingerprint);
+ fingerprints += `<code>${escFingerprints}</code>`;
+ });
+
+ this.$hostKeysInformation.collapse('show');
+ $fingerprintsList.removeClass('invalidate');
+ $fingerprintsList.html(fingerprints);
+ this.$sectionSSHHostKeys.find('.js-known-hosts').val(sshHostKeys.known_hosts);
+ }
+
+ /**
+ * Toggle Auth type information container based on provided `authType`
+ */
+ toggleAuthWell(authType) {
+ this.$wellPasswordAuth.collapse(authType === AUTH_METHOD.PASSWORD ? 'show' : 'hide');
+ this.$wellSSHAuth.collapse(authType === AUTH_METHOD.SSH ? 'show' : 'hide');
+ }
+
+ /**
+ * Toggle SSH auth information message
+ */
+ toggleSSHAuthWellMessage(sshKeyPresent) {
+ this.$sshPublicKeyWrap.collapse(sshKeyPresent ? 'show' : 'hide');
+ this.$wellSSHAuth.find('.js-ssh-public-key-present').collapse(sshKeyPresent ? 'show' : 'hide');
+ this.$regeneratePublicSshKeyButton.collapse(sshKeyPresent ? 'show' : 'hide');
+ this.$wellSSHAuth.find('.js-ssh-public-key-pending').collapse(sshKeyPresent ? 'hide' : 'show');
+ }
+
+ /**
+ * Sets SSH Public key to Clipboard button and shows it on UI.
+ */
+ setSSHPublicKey(sshPublicKey) {
+ this.$sshPublicKeyWrap.find('.ssh-public-key').text(sshPublicKey);
+ this.$sshPublicKeyWrap
+ .find('.btn-copy-ssh-public-key')
+ .attr('data-clipboard-text', sshPublicKey);
+ }
+
+ regeneratePublicSshKey(event) {
+ event.preventDefault();
+
+ this.$regeneratePublicSshKeyModal.toggle(false);
+
+ const button = this.$regeneratePublicSshKeyButton;
+ const spinner = $('.js-spinner', button);
+ const endpoint = button.data('endpoint');
+ const authTypeData = {
+ project: {
+ ...this.$regeneratePublicSshKeyButton.data().projectData,
+ },
+ };
+
+ button.attr('disabled', 'disabled');
+ spinner.removeClass('d-none');
+
+ axios
+ .patch(endpoint, authTypeData)
+ .then(({ data }) => {
+ button.removeAttr('disabled');
+ spinner.addClass('d-none');
+
+ this.setSSHPublicKey(data.import_data_attributes.ssh_public_key);
+ })
+ .catch(() => {
+ Flash(_('Unable to regenerate public ssh key.'));
+ });
+ }
+
+ destroy() {
+ this.$repositoryUrl.off('keyup');
+ this.$form.find('.js-known-hosts').off('keyup');
+ this.$dropdownAuthType.off('change');
+ this.$btnDetectHostKeys.off('click');
+ this.$btnSSHHostsShowAdvanced.off('click');
+ this.$regeneratePublicSshKeyButton.off('click');
+ $('.js-confirm', this.$regeneratePublicSshKeyModal).off('click');
+ $('.js-cancel', this.$regeneratePublicSshKeyModal).off('click');
+ }
+}