Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
444f662b8d
commit
a865379008
|
|
@ -45,6 +45,7 @@ docs lint:
|
||||||
image: "registry.gitlab.com/gitlab-org/gitlab-docs/lint:vale-2.3.3-markdownlint-0.23.2"
|
image: "registry.gitlab.com/gitlab-org/gitlab-docs/lint:vale-2.3.3-markdownlint-0.23.2"
|
||||||
stage: test
|
stage: test
|
||||||
needs: []
|
needs: []
|
||||||
|
allow_failure: true
|
||||||
script:
|
script:
|
||||||
- scripts/lint-doc.sh
|
- scripts/lint-doc.sh
|
||||||
# Prepare docs for build
|
# Prepare docs for build
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,8 @@ linters:
|
||||||
- "app/views/admin/users/new.html.haml"
|
- "app/views/admin/users/new.html.haml"
|
||||||
- "app/views/admin/users/projects.html.haml"
|
- "app/views/admin/users/projects.html.haml"
|
||||||
- "app/views/admin/users/show.html.haml"
|
- "app/views/admin/users/show.html.haml"
|
||||||
|
- 'app/views/authentication/_authenticate.html.haml'
|
||||||
|
- 'app/views/authentication/_register.html.haml'
|
||||||
- "app/views/clusters/clusters/_cluster.html.haml"
|
- "app/views/clusters/clusters/_cluster.html.haml"
|
||||||
- "app/views/clusters/clusters/new.html.haml"
|
- "app/views/clusters/clusters/new.html.haml"
|
||||||
- "app/views/dashboard/milestones/index.html.haml"
|
- "app/views/dashboard/milestones/index.html.haml"
|
||||||
|
|
@ -311,8 +313,6 @@ linters:
|
||||||
- "app/views/shared/web_hooks/_form.html.haml"
|
- "app/views/shared/web_hooks/_form.html.haml"
|
||||||
- "app/views/shared/web_hooks/_hook.html.haml"
|
- "app/views/shared/web_hooks/_hook.html.haml"
|
||||||
- "app/views/shared/wikis/_pages_wiki_page.html.haml"
|
- "app/views/shared/wikis/_pages_wiki_page.html.haml"
|
||||||
- "app/views/u2f/_authenticate.html.haml"
|
|
||||||
- "app/views/u2f/_register.html.haml"
|
|
||||||
- "app/views/users/_deletion_guidance.html.haml"
|
- "app/views/users/_deletion_guidance.html.haml"
|
||||||
- "ee/app/views/admin/_namespace_plan_info.html.haml"
|
- "ee/app/views/admin/_namespace_plan_info.html.haml"
|
||||||
- "ee/app/views/admin/application_settings/_templates.html.haml"
|
- "ee/app/views/admin/application_settings/_templates.html.haml"
|
||||||
|
|
|
||||||
2
Gemfile
2
Gemfile
|
|
@ -512,3 +512,5 @@ gem 'json_schemer', '~> 0.2.12'
|
||||||
gem 'oj', '~> 3.10.6'
|
gem 'oj', '~> 3.10.6'
|
||||||
gem 'multi_json', '~> 1.14.1'
|
gem 'multi_json', '~> 1.14.1'
|
||||||
gem 'yajl-ruby', '~> 1.4.1', require: 'yajl'
|
gem 'yajl-ruby', '~> 1.4.1', require: 'yajl'
|
||||||
|
|
||||||
|
gem 'webauthn', '~> 2.3'
|
||||||
|
|
|
||||||
25
Gemfile.lock
25
Gemfile.lock
|
|
@ -73,6 +73,7 @@ GEM
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
aes_key_wrap (1.0.1)
|
aes_key_wrap (1.0.1)
|
||||||
akismet (3.0.0)
|
akismet (3.0.0)
|
||||||
|
android_key_attestation (0.3.0)
|
||||||
apollo_upload_server (2.0.2)
|
apollo_upload_server (2.0.2)
|
||||||
graphql (>= 1.8)
|
graphql (>= 1.8)
|
||||||
rails (>= 4.2)
|
rails (>= 4.2)
|
||||||
|
|
@ -93,6 +94,7 @@ GEM
|
||||||
encryptor (~> 3.0.0)
|
encryptor (~> 3.0.0)
|
||||||
attr_required (1.0.1)
|
attr_required (1.0.1)
|
||||||
awesome_print (1.8.0)
|
awesome_print (1.8.0)
|
||||||
|
awrence (1.1.1)
|
||||||
aws-eventstream (1.1.0)
|
aws-eventstream (1.1.0)
|
||||||
aws-partitions (1.345.0)
|
aws-partitions (1.345.0)
|
||||||
aws-sdk-cloudformation (1.41.0)
|
aws-sdk-cloudformation (1.41.0)
|
||||||
|
|
@ -167,6 +169,7 @@ GEM
|
||||||
activemodel (>= 4.0.0)
|
activemodel (>= 4.0.0)
|
||||||
activesupport (>= 4.0.0)
|
activesupport (>= 4.0.0)
|
||||||
mime-types (>= 1.16)
|
mime-types (>= 1.16)
|
||||||
|
cbor (0.5.9.6)
|
||||||
character_set (1.4.0)
|
character_set (1.4.0)
|
||||||
charlock_holmes (0.7.6)
|
charlock_holmes (0.7.6)
|
||||||
childprocess (3.0.0)
|
childprocess (3.0.0)
|
||||||
|
|
@ -189,6 +192,9 @@ GEM
|
||||||
contracts (0.11.0)
|
contracts (0.11.0)
|
||||||
cork (0.3.0)
|
cork (0.3.0)
|
||||||
colored2 (~> 3.1)
|
colored2 (~> 3.1)
|
||||||
|
cose (1.0.0)
|
||||||
|
cbor (~> 0.5.9)
|
||||||
|
openssl-signature_algorithm (~> 0.4.0)
|
||||||
countries (3.0.0)
|
countries (3.0.0)
|
||||||
i18n_data (~> 0.8.0)
|
i18n_data (~> 0.8.0)
|
||||||
sixarm_ruby_unaccent (~> 1.1)
|
sixarm_ruby_unaccent (~> 1.1)
|
||||||
|
|
@ -802,6 +808,8 @@ GEM
|
||||||
validate_email
|
validate_email
|
||||||
validate_url
|
validate_url
|
||||||
webfinger (>= 1.0.1)
|
webfinger (>= 1.0.1)
|
||||||
|
openssl (2.2.0)
|
||||||
|
openssl-signature_algorithm (0.4.0)
|
||||||
opentracing (0.5.0)
|
opentracing (0.5.0)
|
||||||
optimist (3.0.1)
|
optimist (3.0.1)
|
||||||
org-ruby (0.9.12)
|
org-ruby (0.9.12)
|
||||||
|
|
@ -1026,6 +1034,8 @@ GEM
|
||||||
rubyzip (2.0.0)
|
rubyzip (2.0.0)
|
||||||
rugged (0.28.4.1)
|
rugged (0.28.4.1)
|
||||||
safe_yaml (1.0.4)
|
safe_yaml (1.0.4)
|
||||||
|
safety_net_attestation (0.4.0)
|
||||||
|
jwt (~> 2.0)
|
||||||
sanitize (5.2.1)
|
sanitize (5.2.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.8.0)
|
nokogiri (>= 1.8.0)
|
||||||
|
|
@ -1050,6 +1060,7 @@ GEM
|
||||||
scss_lint (0.56.0)
|
scss_lint (0.56.0)
|
||||||
rake (>= 0.9, < 13)
|
rake (>= 0.9, < 13)
|
||||||
sass (~> 3.5.3)
|
sass (~> 3.5.3)
|
||||||
|
securecompare (1.0.0)
|
||||||
seed-fu (2.3.7)
|
seed-fu (2.3.7)
|
||||||
activerecord (>= 3.1)
|
activerecord (>= 3.1)
|
||||||
activesupport (>= 3.1)
|
activesupport (>= 3.1)
|
||||||
|
|
@ -1135,6 +1146,9 @@ GEM
|
||||||
parslet (~> 1.8.0)
|
parslet (~> 1.8.0)
|
||||||
toml-rb (1.0.0)
|
toml-rb (1.0.0)
|
||||||
citrus (~> 3.0, > 3.0)
|
citrus (~> 3.0, > 3.0)
|
||||||
|
tpm-key_attestation (0.9.0)
|
||||||
|
bindata (~> 2.4)
|
||||||
|
openssl-signature_algorithm (~> 0.4.0)
|
||||||
truncato (0.7.11)
|
truncato (0.7.11)
|
||||||
htmlentities (~> 4.3.1)
|
htmlentities (~> 4.3.1)
|
||||||
nokogiri (>= 1.7.0, <= 2.0)
|
nokogiri (>= 1.7.0, <= 2.0)
|
||||||
|
|
@ -1186,6 +1200,16 @@ GEM
|
||||||
vmstat (2.3.0)
|
vmstat (2.3.0)
|
||||||
warden (1.2.8)
|
warden (1.2.8)
|
||||||
rack (>= 2.0.6)
|
rack (>= 2.0.6)
|
||||||
|
webauthn (2.3.0)
|
||||||
|
android_key_attestation (~> 0.3.0)
|
||||||
|
awrence (~> 1.1)
|
||||||
|
bindata (~> 2.4)
|
||||||
|
cbor (~> 0.5.9)
|
||||||
|
cose (~> 1.0)
|
||||||
|
openssl (~> 2.0)
|
||||||
|
safety_net_attestation (~> 0.4.0)
|
||||||
|
securecompare (~> 1.0)
|
||||||
|
tpm-key_attestation (~> 0.9.0)
|
||||||
webfinger (1.1.0)
|
webfinger (1.1.0)
|
||||||
activesupport
|
activesupport
|
||||||
httpclient (>= 2.4)
|
httpclient (>= 2.4)
|
||||||
|
|
@ -1472,6 +1496,7 @@ DEPENDENCIES
|
||||||
validates_hostname (~> 1.0.10)
|
validates_hostname (~> 1.0.10)
|
||||||
version_sorter (~> 2.2.4)
|
version_sorter (~> 2.2.4)
|
||||||
vmstat (~> 2.3.0)
|
vmstat (~> 2.3.0)
|
||||||
|
webauthn (~> 2.3)
|
||||||
webmock (~> 3.5.1)
|
webmock (~> 3.5.1)
|
||||||
webpack-rails (~> 0.9.10)
|
webpack-rails (~> 0.9.10)
|
||||||
wikicloth (= 0.8.1)
|
wikicloth (= 0.8.1)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,23 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import initU2F from './u2f';
|
import initU2F from './u2f';
|
||||||
|
import initWebauthn from './webauthn';
|
||||||
import U2FRegister from './u2f/register';
|
import U2FRegister from './u2f/register';
|
||||||
|
import WebAuthnRegister from './webauthn/register';
|
||||||
|
|
||||||
export const mount2faAuthentication = () => {
|
export const mount2faAuthentication = () => {
|
||||||
// Soon this will conditionally mount a webauthn app (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26692)
|
if (gon.webauthn) {
|
||||||
initU2F();
|
initWebauthn();
|
||||||
|
} else {
|
||||||
|
initU2F();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mount2faRegistration = () => {
|
export const mount2faRegistration = () => {
|
||||||
// Soon this will conditionally mount a webauthn app (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26692)
|
if (gon.webauthn) {
|
||||||
const u2fRegister = new U2FRegister($('#js-register-u2f'), gon.u2f);
|
const webauthnRegister = new WebAuthnRegister($('#js-register-token-2fa'), gon.webauthn);
|
||||||
u2fRegister.start();
|
webauthnRegister.start();
|
||||||
|
} else {
|
||||||
|
const u2fRegister = new U2FRegister($('#js-register-token-2fa'), gon.u2f);
|
||||||
|
u2fRegister.start();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ export default class U2FAuthenticate {
|
||||||
this.signRequests = u2fParams.sign_requests.map(request => omit(request, 'challenge'));
|
this.signRequests = u2fParams.sign_requests.map(request => omit(request, 'challenge'));
|
||||||
|
|
||||||
this.templates = {
|
this.templates = {
|
||||||
setup: '#js-authenticate-token-2fa-setup',
|
|
||||||
inProgress: '#js-authenticate-token-2fa-in-progress',
|
inProgress: '#js-authenticate-token-2fa-in-progress',
|
||||||
error: '#js-authenticate-token-2fa-error',
|
error: '#js-authenticate-token-2fa-error',
|
||||||
authenticated: '#js-authenticate-token-2fa-authenticated',
|
authenticated: '#js-authenticate-token-2fa-authenticated',
|
||||||
|
|
@ -86,7 +85,7 @@ export default class U2FAuthenticate {
|
||||||
renderError(error) {
|
renderError(error) {
|
||||||
this.renderTemplate('error', {
|
this.renderTemplate('error', {
|
||||||
error_message: error.message(),
|
error_message: error.message(),
|
||||||
error_code: error.errorCode,
|
error_name: error.errorCode,
|
||||||
});
|
});
|
||||||
return this.container.find('#js-token-2fa-try-again').on('click', this.renderInProgress);
|
return this.container.find('#js-token-2fa-try-again').on('click', this.renderInProgress);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { template as lodashTemplate } from 'lodash';
|
import { template as lodashTemplate } from 'lodash';
|
||||||
|
import { __ } from '~/locale';
|
||||||
import importU2FLibrary from './util';
|
import importU2FLibrary from './util';
|
||||||
import U2FError from './error';
|
import U2FError from './error';
|
||||||
|
|
||||||
|
|
@ -24,11 +25,10 @@ export default class U2FRegister {
|
||||||
this.signRequests = u2fParams.sign_requests;
|
this.signRequests = u2fParams.sign_requests;
|
||||||
|
|
||||||
this.templates = {
|
this.templates = {
|
||||||
notSupported: '#js-register-u2f-not-supported',
|
message: '#js-register-2fa-message',
|
||||||
setup: '#js-register-u2f-setup',
|
setup: '#js-register-token-2fa-setup',
|
||||||
inProgress: '#js-register-u2f-in-progress',
|
error: '#js-register-token-2fa-error',
|
||||||
error: '#js-register-u2f-error',
|
registered: '#js-register-token-2fa-registered',
|
||||||
registered: '#js-register-u2f-registered',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,18 +65,22 @@ export default class U2FRegister {
|
||||||
|
|
||||||
renderSetup() {
|
renderSetup() {
|
||||||
this.renderTemplate('setup');
|
this.renderTemplate('setup');
|
||||||
return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress);
|
return this.container.find('#js-setup-token-2fa-device').on('click', this.renderInProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderInProgress() {
|
renderInProgress() {
|
||||||
this.renderTemplate('inProgress');
|
this.renderTemplate('message', {
|
||||||
|
message: __(
|
||||||
|
'Trying to communicate with your device. Plug it in (if needed) and press the button on the device now.',
|
||||||
|
),
|
||||||
|
});
|
||||||
return this.register();
|
return this.register();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderError(error) {
|
renderError(error) {
|
||||||
this.renderTemplate('error', {
|
this.renderTemplate('error', {
|
||||||
error_message: error.message(),
|
error_message: error.message(),
|
||||||
error_code: error.errorCode,
|
error_name: error.errorCode,
|
||||||
});
|
});
|
||||||
return this.container.find('#js-token-2fa-try-again').on('click', this.renderSetup);
|
return this.container.find('#js-token-2fa-try-again').on('click', this.renderSetup);
|
||||||
}
|
}
|
||||||
|
|
@ -89,6 +93,10 @@ export default class U2FRegister {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNotSupported() {
|
renderNotSupported() {
|
||||||
return this.renderTemplate('notSupported');
|
return this.renderTemplate('message', {
|
||||||
|
message: __(
|
||||||
|
"Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).",
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
import WebAuthnError from './error';
|
||||||
|
import WebAuthnFlow from './flow';
|
||||||
|
import { supported, convertGetParams, convertGetResponse } from './util';
|
||||||
|
|
||||||
|
// Authenticate WebAuthn devices for users to authenticate with.
|
||||||
|
//
|
||||||
|
// State Flow #1: setup -> in_progress -> authenticated -> POST to server
|
||||||
|
// State Flow #2: setup -> in_progress -> error -> setup
|
||||||
|
export default class WebAuthnAuthenticate {
|
||||||
|
constructor(container, form, webauthnParams, fallbackButton, fallbackUI) {
|
||||||
|
this.container = container;
|
||||||
|
this.webauthnParams = convertGetParams(JSON.parse(webauthnParams.options));
|
||||||
|
this.renderInProgress = this.renderInProgress.bind(this);
|
||||||
|
|
||||||
|
this.form = form;
|
||||||
|
this.fallbackButton = fallbackButton;
|
||||||
|
this.fallbackUI = fallbackUI;
|
||||||
|
if (this.fallbackButton) {
|
||||||
|
this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.flow = new WebAuthnFlow(container, {
|
||||||
|
inProgress: '#js-authenticate-token-2fa-in-progress',
|
||||||
|
error: '#js-authenticate-token-2fa-error',
|
||||||
|
authenticated: '#js-authenticate-token-2fa-authenticated',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.container.on('click', '#js-token-2fa-try-again', this.renderInProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (!supported()) {
|
||||||
|
this.switchToFallbackUI();
|
||||||
|
} else {
|
||||||
|
this.renderInProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticate() {
|
||||||
|
navigator.credentials
|
||||||
|
.get({ publicKey: this.webauthnParams })
|
||||||
|
.then(resp => {
|
||||||
|
const convertedResponse = convertGetResponse(resp);
|
||||||
|
this.renderAuthenticated(JSON.stringify(convertedResponse));
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.flow.renderError(new WebAuthnError(err, 'authenticate'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInProgress() {
|
||||||
|
this.flow.renderTemplate('inProgress');
|
||||||
|
this.authenticate();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAuthenticated(deviceResponse) {
|
||||||
|
this.flow.renderTemplate('authenticated');
|
||||||
|
const container = this.container[0];
|
||||||
|
container.querySelector('#js-device-response').value = deviceResponse;
|
||||||
|
container.querySelector(this.form).submit();
|
||||||
|
this.fallbackButton.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
switchToFallbackUI() {
|
||||||
|
this.fallbackButton.classList.add('hidden');
|
||||||
|
this.container[0].classList.add('hidden');
|
||||||
|
this.fallbackUI.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { __ } from '~/locale';
|
||||||
|
import { isHTTPS, FLOW_AUTHENTICATE, FLOW_REGISTER } from './util';
|
||||||
|
|
||||||
|
export default class WebAuthnError {
|
||||||
|
constructor(error, flowType) {
|
||||||
|
this.error = error;
|
||||||
|
this.errorName = error.name || 'UnknownError';
|
||||||
|
this.message = this.message.bind(this);
|
||||||
|
this.httpsDisabled = !isHTTPS();
|
||||||
|
this.flowType = flowType;
|
||||||
|
}
|
||||||
|
|
||||||
|
message() {
|
||||||
|
if (this.errorName === 'NotSupportedError') {
|
||||||
|
return __('Your device is not compatible with GitLab. Please try another device');
|
||||||
|
} else if (this.errorName === 'InvalidStateError' && this.flowType === FLOW_AUTHENTICATE) {
|
||||||
|
return __('This device has not been registered with us.');
|
||||||
|
} else if (this.errorName === 'InvalidStateError' && this.flowType === FLOW_REGISTER) {
|
||||||
|
return __('This device has already been registered with us.');
|
||||||
|
} else if (this.errorName === 'SecurityError' && this.httpsDisabled) {
|
||||||
|
return __(
|
||||||
|
'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return __('There was a problem communicating with your device.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { template } from 'lodash';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic abstraction for WebAuthnFlows, especially for register / authenticate
|
||||||
|
*/
|
||||||
|
export default class WebAuthnFlow {
|
||||||
|
constructor(container, templates) {
|
||||||
|
this.container = container;
|
||||||
|
this.templates = templates;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTemplate(name, params) {
|
||||||
|
const templateString = document.querySelector(this.templates[name]).innerHTML;
|
||||||
|
const compiledTemplate = template(templateString);
|
||||||
|
this.container.html(compiledTemplate(params));
|
||||||
|
}
|
||||||
|
|
||||||
|
renderError(error) {
|
||||||
|
this.renderTemplate('error', {
|
||||||
|
error_message: error.message(),
|
||||||
|
error_name: error.errorName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import $ from 'jquery';
|
||||||
|
import WebAuthnAuthenticate from './authenticate';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const webauthnAuthenticate = new WebAuthnAuthenticate(
|
||||||
|
$('#js-authenticate-token-2fa'),
|
||||||
|
'#js-login-token-2fa-form',
|
||||||
|
gon.webauthn,
|
||||||
|
document.querySelector('#js-login-2fa-device'),
|
||||||
|
document.querySelector('.js-2fa-form'),
|
||||||
|
);
|
||||||
|
webauthnAuthenticate.start();
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { __ } from '~/locale';
|
||||||
|
import WebAuthnError from './error';
|
||||||
|
import WebAuthnFlow from './flow';
|
||||||
|
import { supported, isHTTPS, convertCreateParams, convertCreateResponse } from './util';
|
||||||
|
|
||||||
|
// Register WebAuthn devices for users to authenticate with.
|
||||||
|
//
|
||||||
|
// State Flow #1: setup -> in_progress -> registered -> POST to server
|
||||||
|
// State Flow #2: setup -> in_progress -> error -> setup
|
||||||
|
export default class WebAuthnRegister {
|
||||||
|
constructor(container, webauthnParams) {
|
||||||
|
this.container = container;
|
||||||
|
this.renderInProgress = this.renderInProgress.bind(this);
|
||||||
|
this.webauthnOptions = convertCreateParams(webauthnParams.options);
|
||||||
|
|
||||||
|
this.flow = new WebAuthnFlow(container, {
|
||||||
|
message: '#js-register-2fa-message',
|
||||||
|
setup: '#js-register-token-2fa-setup',
|
||||||
|
error: '#js-register-token-2fa-error',
|
||||||
|
registered: '#js-register-token-2fa-registered',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.container.on('click', '#js-token-2fa-try-again', this.renderInProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (!supported()) {
|
||||||
|
// we show a special error message when the user visits the site
|
||||||
|
// using a non-ssl connection as this makes WebAuthn unavailable in
|
||||||
|
// any case, regardless of the used browser
|
||||||
|
this.renderNotSupported(!isHTTPS());
|
||||||
|
} else {
|
||||||
|
this.renderSetup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
register() {
|
||||||
|
navigator.credentials
|
||||||
|
.create({
|
||||||
|
publicKey: this.webauthnOptions,
|
||||||
|
})
|
||||||
|
.then(cred => this.renderRegistered(JSON.stringify(convertCreateResponse(cred))))
|
||||||
|
.catch(err => this.flow.renderError(new WebAuthnError(err, 'register')));
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSetup() {
|
||||||
|
this.flow.renderTemplate('setup');
|
||||||
|
this.container.find('#js-setup-token-2fa-device').on('click', this.renderInProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInProgress() {
|
||||||
|
this.flow.renderTemplate('message', {
|
||||||
|
message: __(
|
||||||
|
'Trying to communicate with your device. Plug it in (if needed) and press the button on the device now.',
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return this.register();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRegistered(deviceResponse) {
|
||||||
|
this.flow.renderTemplate('registered');
|
||||||
|
// Prefer to do this instead of interpolating using Underscore templates
|
||||||
|
// because of JSON escaping issues.
|
||||||
|
this.container.find('#js-device-response').val(deviceResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNotSupported(noHttps) {
|
||||||
|
const message = noHttps
|
||||||
|
? __(
|
||||||
|
'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.',
|
||||||
|
)
|
||||||
|
: __(
|
||||||
|
"Your browser doesn't support WebAuthn. Please use a supported browser, e.g. Chrome (67+) or Firefox (60+).",
|
||||||
|
);
|
||||||
|
|
||||||
|
this.flow.renderTemplate('message', { message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
export function supported() {
|
||||||
|
return Boolean(
|
||||||
|
navigator.credentials &&
|
||||||
|
navigator.credentials.create &&
|
||||||
|
navigator.credentials.get &&
|
||||||
|
window.PublicKeyCredential,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isHTTPS() {
|
||||||
|
return window.location.protocol.startsWith('https');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FLOW_AUTHENTICATE = 'authenticate';
|
||||||
|
export const FLOW_REGISTER = 'register';
|
||||||
|
|
||||||
|
// adapted from https://stackoverflow.com/a/21797381/8204697
|
||||||
|
function base64ToBuffer(base64) {
|
||||||
|
const binaryString = window.atob(base64);
|
||||||
|
const len = binaryString.length;
|
||||||
|
const bytes = new Uint8Array(len);
|
||||||
|
for (let i = 0; i < len; i += 1) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// adapted from https://stackoverflow.com/a/9458996/8204697
|
||||||
|
function bufferToBase64(buffer) {
|
||||||
|
if (typeof buffer === 'string') {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
let binary = '';
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
const len = bytes.byteLength;
|
||||||
|
for (let i = 0; i < len; i += 1) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return window.btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the given object with the id property converted to buffer
|
||||||
|
*
|
||||||
|
* @param {Object} param
|
||||||
|
*/
|
||||||
|
function convertIdToBuffer({ id, ...rest }) {
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
id: base64ToBuffer(id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the given array with all `id`s of the items converted to buffer
|
||||||
|
*
|
||||||
|
* @param {Array} items
|
||||||
|
*/
|
||||||
|
function convertIdsToBuffer(items) {
|
||||||
|
return items.map(convertIdToBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an object with keys of the given props, and values from the given object converted to base64
|
||||||
|
*
|
||||||
|
* @param {String} obj
|
||||||
|
* @param {Array} props
|
||||||
|
*/
|
||||||
|
function convertPropertiesToBase64(obj, props) {
|
||||||
|
return props.reduce(
|
||||||
|
(acc, property) => Object.assign(acc, { [property]: bufferToBase64(obj[property]) }),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertGetParams({ allowCredentials, challenge, ...rest }) {
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
...(allowCredentials ? { allowCredentials: convertIdsToBuffer(allowCredentials) } : {}),
|
||||||
|
challenge: base64ToBuffer(challenge),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertGetResponse(webauthnResponse) {
|
||||||
|
return {
|
||||||
|
type: webauthnResponse.type,
|
||||||
|
id: webauthnResponse.id,
|
||||||
|
rawId: bufferToBase64(webauthnResponse.rawId),
|
||||||
|
response: convertPropertiesToBase64(webauthnResponse.response, [
|
||||||
|
'clientDataJSON',
|
||||||
|
'authenticatorData',
|
||||||
|
'signature',
|
||||||
|
'userHandle',
|
||||||
|
]),
|
||||||
|
clientExtensionResults: webauthnResponse.getClientExtensionResults(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertCreateParams({ challenge, user, excludeCredentials, ...rest }) {
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
challenge: base64ToBuffer(challenge),
|
||||||
|
user: convertIdToBuffer(user),
|
||||||
|
...(excludeCredentials ? { excludeCredentials: convertIdsToBuffer(excludeCredentials) } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertCreateResponse(webauthnResponse) {
|
||||||
|
return {
|
||||||
|
type: webauthnResponse.type,
|
||||||
|
id: webauthnResponse.id,
|
||||||
|
rawId: bufferToBase64(webauthnResponse.rawId),
|
||||||
|
clientExtensionResults: webauthnResponse.getClientExtensionResults(),
|
||||||
|
response: convertPropertiesToBase64(webauthnResponse.response, [
|
||||||
|
'clientDataJSON',
|
||||||
|
'attestationObject',
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,14 @@
|
||||||
/* eslint-disable vue/no-v-html */
|
/* eslint-disable vue/no-v-html */
|
||||||
import { escape } from 'lodash';
|
import { escape } from 'lodash';
|
||||||
import { mapActions, mapGetters } from 'vuex';
|
import { mapActions, mapGetters } from 'vuex';
|
||||||
import { GlDeprecatedButton, GlTooltipDirective, GlLoadingIcon, GlIcon } from '@gitlab/ui';
|
import {
|
||||||
|
GlDeprecatedButton,
|
||||||
|
GlTooltipDirective,
|
||||||
|
GlSafeHtmlDirective,
|
||||||
|
GlLoadingIcon,
|
||||||
|
GlIcon,
|
||||||
|
GlButton,
|
||||||
|
} from '@gitlab/ui';
|
||||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||||
import { truncateSha } from '~/lib/utils/text_utility';
|
import { truncateSha } from '~/lib/utils/text_utility';
|
||||||
|
|
@ -21,9 +28,11 @@ export default {
|
||||||
GlIcon,
|
GlIcon,
|
||||||
FileIcon,
|
FileIcon,
|
||||||
DiffStats,
|
DiffStats,
|
||||||
|
GlButton,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
|
SafeHtml: GlSafeHtmlDirective,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
discussionPath: {
|
discussionPath: {
|
||||||
|
|
@ -77,6 +86,21 @@ export default {
|
||||||
|
|
||||||
return this.discussionPath;
|
return this.discussionPath;
|
||||||
},
|
},
|
||||||
|
submoduleDiffCompareLinkText() {
|
||||||
|
if (this.diffFile.submodule_compare) {
|
||||||
|
const truncatedOldSha = escape(truncateSha(this.diffFile.submodule_compare.old_sha));
|
||||||
|
const truncatedNewSha = escape(truncateSha(this.diffFile.submodule_compare.new_sha));
|
||||||
|
return sprintf(
|
||||||
|
s__('Compare %{oldCommitId}...%{newCommitId}'),
|
||||||
|
{
|
||||||
|
oldCommitId: `<span class="commit-sha">${truncatedOldSha}</span>`,
|
||||||
|
newCommitId: `<span class="commit-sha">${truncatedNewSha}</span>`,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
filePath() {
|
filePath() {
|
||||||
if (this.diffFile.submodule) {
|
if (this.diffFile.submodule) {
|
||||||
return `${this.diffFile.file_path} @ ${truncateSha(this.diffFile.blob.id)}`;
|
return `${this.diffFile.file_path} @ ${truncateSha(this.diffFile.blob.id)}`;
|
||||||
|
|
@ -311,5 +335,18 @@ export default {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="diffFile.submodule_compare"
|
||||||
|
class="file-actions d-none d-sm-flex align-items-center flex-wrap"
|
||||||
|
>
|
||||||
|
<gl-button
|
||||||
|
v-gl-tooltip.hover
|
||||||
|
v-safe-html="submoduleDiffCompareLinkText"
|
||||||
|
class="submodule-compare"
|
||||||
|
:title="s__('Compare submodule commit revisions')"
|
||||||
|
:href="diffFile.submodule_compare.url"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export default {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="showVersion"
|
v-if="showVersion"
|
||||||
class="table-section section-50 gl-display-flex gl-justify-content-md-end"
|
class="table-section section-50 gl-display-flex gl-md-justify-content-end"
|
||||||
data-testid="version-pattern"
|
data-testid="version-pattern"
|
||||||
>
|
>
|
||||||
<span class="gl-text-body">{{ dependency.version_pattern }}</span>
|
<span class="gl-text-body">{{ dependency.version_pattern }}</span>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
/* eslint-disable vue/no-v-html */
|
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
|
|
||||||
import { deprecatedCreateFlash as Flash } from '~/flash';
|
import { deprecatedCreateFlash as Flash } from '~/flash';
|
||||||
|
|
@ -12,6 +12,9 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
DeprecatedModal,
|
DeprecatedModal,
|
||||||
},
|
},
|
||||||
|
directives: {
|
||||||
|
SafeHtml,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
issueCount: {
|
issueCount: {
|
||||||
type: Number,
|
type: Number,
|
||||||
|
|
@ -125,7 +128,7 @@ Once deleted, it cannot be undone or recovered.`),
|
||||||
@submit="onSubmit"
|
@submit="onSubmit"
|
||||||
>
|
>
|
||||||
<template #body="props">
|
<template #body="props">
|
||||||
<p v-html="props.text"></p>
|
<p v-safe-html="props.text"></p>
|
||||||
</template>
|
</template>
|
||||||
</deprecated-modal>
|
</deprecated-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,20 @@ import createTestReportsStore from './stores/test_reports';
|
||||||
|
|
||||||
Vue.use(Translate);
|
Vue.use(Translate);
|
||||||
|
|
||||||
|
const SELECTORS = {
|
||||||
|
PIPELINE_DETAILS: '.js-pipeline-details-vue',
|
||||||
|
PIPELINE_GRAPH: '#js-pipeline-graph-vue',
|
||||||
|
PIPELINE_HEADER: '#js-pipeline-header-vue',
|
||||||
|
PIPELINE_TESTS: '#js-pipeline-tests-detail',
|
||||||
|
};
|
||||||
|
|
||||||
const createPipelinesDetailApp = mediator => {
|
const createPipelinesDetailApp = mediator => {
|
||||||
|
if (!document.querySelector(SELECTORS.PIPELINE_GRAPH)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// eslint-disable-next-line no-new
|
// eslint-disable-next-line no-new
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#js-pipeline-graph-vue',
|
el: SELECTORS.PIPELINE_GRAPH,
|
||||||
components: {
|
components: {
|
||||||
pipelineGraph,
|
pipelineGraph,
|
||||||
},
|
},
|
||||||
|
|
@ -47,9 +57,12 @@ const createPipelinesDetailApp = mediator => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPipelineHeaderApp = mediator => {
|
const createPipelineHeaderApp = mediator => {
|
||||||
|
if (!document.querySelector(SELECTORS.PIPELINE_HEADER)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// eslint-disable-next-line no-new
|
// eslint-disable-next-line no-new
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#js-pipeline-header-vue',
|
el: SELECTORS.PIPELINE_HEADER,
|
||||||
components: {
|
components: {
|
||||||
pipelineHeader,
|
pipelineHeader,
|
||||||
},
|
},
|
||||||
|
|
@ -93,9 +106,8 @@ const createPipelineHeaderApp = mediator => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createTestDetails = () => {
|
const createTestDetails = () => {
|
||||||
const el = document.querySelector('#js-pipeline-tests-detail');
|
const el = document.querySelector(SELECTORS.PIPELINE_TESTS);
|
||||||
const { summaryEndpoint, suiteEndpoint } = el?.dataset || {};
|
const { summaryEndpoint, suiteEndpoint } = el?.dataset || {};
|
||||||
|
|
||||||
const testReportsStore = createTestReportsStore({
|
const testReportsStore = createTestReportsStore({
|
||||||
summaryEndpoint,
|
summaryEndpoint,
|
||||||
suiteEndpoint,
|
suiteEndpoint,
|
||||||
|
|
@ -115,7 +127,7 @@ const createTestDetails = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { dataset } = document.querySelector('.js-pipeline-details-vue');
|
const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
|
||||||
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
|
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
|
||||||
mediator.fetchPipeline();
|
mediator.fetchPipeline();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export default function deviseState() {
|
||||||
return stateKey.pipelineFailed;
|
return stateKey.pipelineFailed;
|
||||||
} else if (this.workInProgress) {
|
} else if (this.workInProgress) {
|
||||||
return stateKey.workInProgress;
|
return stateKey.workInProgress;
|
||||||
} else if (this.hasMergeableDiscussionsState) {
|
} else if (this.hasMergeableDiscussionsState && !this.autoMergeEnabled) {
|
||||||
return stateKey.unresolvedDiscussions;
|
return stateKey.unresolvedDiscussions;
|
||||||
} else if (this.isPipelineBlocked) {
|
} else if (this.isPipelineBlocked) {
|
||||||
return stateKey.pipelineBlocked;
|
return stateKey.pipelineBlocked;
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,10 @@ table {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detected {
|
||||||
|
width: 9%;
|
||||||
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
width: 8%;
|
width: 8%;
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +206,7 @@ table {
|
||||||
}
|
}
|
||||||
|
|
||||||
.identifier {
|
.identifier {
|
||||||
width: 12%;
|
width: 16%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scanner {
|
.scanner {
|
||||||
|
|
|
||||||
|
|
@ -151,18 +151,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-down(sm) {
|
|
||||||
.todos-filters {
|
|
||||||
.dropdown-menu-toggle {
|
|
||||||
width: 130px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu-toggle-sort {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include media-breakpoint-down(lg) {
|
@include media-breakpoint-down(lg) {
|
||||||
.todos-filters {
|
.todos-filters {
|
||||||
.filter-categories {
|
.filter-categories {
|
||||||
|
|
@ -206,6 +194,10 @@
|
||||||
.dropdown-menu-toggle {
|
.dropdown-menu-toggle {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-toggle-sort {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -257,7 +257,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table.u2f-registrations {
|
table.u2f-registrations,
|
||||||
|
.webauthn-registrations {
|
||||||
th:not(:last-child),
|
th:not(:last-child),
|
||||||
td:not(:last-child) {
|
td:not(:last-child) {
|
||||||
border-right: solid 1px transparent;
|
border-right: solid 1px transparent;
|
||||||
|
|
|
||||||
|
|
@ -112,27 +112,10 @@
|
||||||
top: 66vh;
|
top: 66vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove when https://gitlab.com/gitlab-org/gitlab-ui/-/issues/871
|
|
||||||
// gets fixed on GitLab UI
|
|
||||||
.gl-sm-w-auto\! {
|
|
||||||
@media (min-width: $breakpoint-sm) {
|
|
||||||
width: auto !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.gl-shadow-x0-y0-b3-s1-blue-500 {
|
.gl-shadow-x0-y0-b3-s1-blue-500 {
|
||||||
box-shadow: inset 0 0 3px $gl-border-size-1 $blue-500;
|
box-shadow: inset 0 0 3px $gl-border-size-1 $blue-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove when https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1692 is merged
|
|
||||||
.gl-border-t-transparent {
|
|
||||||
border-top-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gl-align-items-flex-end {
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gl-sm-align-items-flex-end {
|
.gl-sm-align-items-flex-end {
|
||||||
@media (min-width: $breakpoint-sm) {
|
@media (min-width: $breakpoint-sm) {
|
||||||
|
|
@ -152,15 +135,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gl-align-items-stretch {
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gl-min-h-6 {
|
.gl-min-h-6 {
|
||||||
min-height: $gl-spacing-scale-6;
|
min-height: $gl-spacing-scale-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gl-justify-content-md-end {
|
.gl-md-justify-content-end {
|
||||||
@media (min-width: $breakpoint-md) {
|
@media (min-width: $breakpoint-md) {
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,13 @@ module Authenticates2FAForAdminMode
|
||||||
return handle_locked_user(user) unless user.can?(:log_in)
|
return handle_locked_user(user) unless user.can?(:log_in)
|
||||||
|
|
||||||
session[:otp_user_id] = user.id
|
session[:otp_user_id] = user.id
|
||||||
setup_u2f_authentication(user)
|
push_frontend_feature_flag(:webauthn)
|
||||||
|
|
||||||
|
if user.two_factor_webauthn_enabled?
|
||||||
|
setup_webauthn_authentication(user)
|
||||||
|
else
|
||||||
|
setup_u2f_authentication(user)
|
||||||
|
end
|
||||||
|
|
||||||
render 'admin/sessions/two_factor', layout: 'application'
|
render 'admin/sessions/two_factor', layout: 'application'
|
||||||
end
|
end
|
||||||
|
|
@ -24,7 +30,11 @@ module Authenticates2FAForAdminMode
|
||||||
if user_params[:otp_attempt].present? && session[:otp_user_id]
|
if user_params[:otp_attempt].present? && session[:otp_user_id]
|
||||||
admin_mode_authenticate_with_two_factor_via_otp(user)
|
admin_mode_authenticate_with_two_factor_via_otp(user)
|
||||||
elsif user_params[:device_response].present? && session[:otp_user_id]
|
elsif user_params[:device_response].present? && session[:otp_user_id]
|
||||||
admin_mode_authenticate_with_two_factor_via_u2f(user)
|
if user.two_factor_webauthn_enabled?
|
||||||
|
admin_mode_authenticate_with_two_factor_via_webauthn(user)
|
||||||
|
else
|
||||||
|
admin_mode_authenticate_with_two_factor_via_u2f(user)
|
||||||
|
end
|
||||||
elsif user && user.valid_password?(user_params[:password])
|
elsif user && user.valid_password?(user_params[:password])
|
||||||
admin_mode_prompt_for_two_factor(user)
|
admin_mode_prompt_for_two_factor(user)
|
||||||
else
|
else
|
||||||
|
|
@ -52,18 +62,17 @@ module Authenticates2FAForAdminMode
|
||||||
|
|
||||||
def admin_mode_authenticate_with_two_factor_via_u2f(user)
|
def admin_mode_authenticate_with_two_factor_via_u2f(user)
|
||||||
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge])
|
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge])
|
||||||
# Remove any lingering user data from login
|
admin_handle_two_factor_success
|
||||||
session.delete(:otp_user_id)
|
|
||||||
session.delete(:challenge)
|
|
||||||
|
|
||||||
# The admin user has successfully passed 2fa, enable admin mode ignoring password
|
|
||||||
enable_admin_mode
|
|
||||||
else
|
else
|
||||||
user.increment_failed_attempts!
|
admin_handle_two_factor_failure(user, 'U2F')
|
||||||
Gitlab::AppLogger.info("Failed Admin Mode Login: user=#{user.username} ip=#{request.remote_ip} method=U2F")
|
end
|
||||||
flash.now[:alert] = _('Authentication via U2F device failed.')
|
end
|
||||||
|
|
||||||
admin_mode_prompt_for_two_factor(user)
|
def admin_mode_authenticate_with_two_factor_via_webauthn(user)
|
||||||
|
if Webauthn::AuthenticateService.new(user, user_params[:device_response], session[:challenge]).execute
|
||||||
|
admin_handle_two_factor_success
|
||||||
|
else
|
||||||
|
admin_handle_two_factor_failure(user, 'WebAuthn')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -81,4 +90,21 @@ module Authenticates2FAForAdminMode
|
||||||
flash.now[:alert] = _('Invalid login or password')
|
flash.now[:alert] = _('Invalid login or password')
|
||||||
render :new
|
render :new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def admin_handle_two_factor_success
|
||||||
|
# Remove any lingering user data from login
|
||||||
|
session.delete(:otp_user_id)
|
||||||
|
session.delete(:challenge)
|
||||||
|
|
||||||
|
# The admin user has successfully passed 2fa, enable admin mode ignoring password
|
||||||
|
enable_admin_mode
|
||||||
|
end
|
||||||
|
|
||||||
|
def admin_handle_two_factor_failure(user, method)
|
||||||
|
user.increment_failed_attempts!
|
||||||
|
Gitlab::AppLogger.info("Failed Admin Mode Login: user=#{user.username} ip=#{request.remote_ip} method=#{method}")
|
||||||
|
flash.now[:alert] = _('Authentication via %{method} device failed.') % { method: method }
|
||||||
|
|
||||||
|
admin_mode_prompt_for_two_factor(user)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,14 @@ module AuthenticatesWithTwoFactor
|
||||||
|
|
||||||
session[:otp_user_id] = user.id
|
session[:otp_user_id] = user.id
|
||||||
session[:user_updated_at] = user.updated_at
|
session[:user_updated_at] = user.updated_at
|
||||||
|
push_frontend_feature_flag(:webauthn)
|
||||||
|
|
||||||
|
if user.two_factor_webauthn_enabled?
|
||||||
|
setup_webauthn_authentication(user)
|
||||||
|
else
|
||||||
|
setup_u2f_authentication(user)
|
||||||
|
end
|
||||||
|
|
||||||
setup_u2f_authentication(user)
|
|
||||||
render 'devise/sessions/two_factor'
|
render 'devise/sessions/two_factor'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -46,7 +52,11 @@ module AuthenticatesWithTwoFactor
|
||||||
if user_params[:otp_attempt].present? && session[:otp_user_id]
|
if user_params[:otp_attempt].present? && session[:otp_user_id]
|
||||||
authenticate_with_two_factor_via_otp(user)
|
authenticate_with_two_factor_via_otp(user)
|
||||||
elsif user_params[:device_response].present? && session[:otp_user_id]
|
elsif user_params[:device_response].present? && session[:otp_user_id]
|
||||||
authenticate_with_two_factor_via_u2f(user)
|
if user.two_factor_webauthn_enabled?
|
||||||
|
authenticate_with_two_factor_via_webauthn(user)
|
||||||
|
else
|
||||||
|
authenticate_with_two_factor_via_u2f(user)
|
||||||
|
end
|
||||||
elsif user && user.valid_password?(user_params[:password])
|
elsif user && user.valid_password?(user_params[:password])
|
||||||
prompt_for_two_factor(user)
|
prompt_for_two_factor(user)
|
||||||
end
|
end
|
||||||
|
|
@ -89,16 +99,17 @@ module AuthenticatesWithTwoFactor
|
||||||
# Authenticate using the response from a U2F (universal 2nd factor) device
|
# Authenticate using the response from a U2F (universal 2nd factor) device
|
||||||
def authenticate_with_two_factor_via_u2f(user)
|
def authenticate_with_two_factor_via_u2f(user)
|
||||||
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge])
|
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge])
|
||||||
# Remove any lingering user data from login
|
handle_two_factor_success(user)
|
||||||
clear_two_factor_attempt!
|
|
||||||
|
|
||||||
remember_me(user) if user_params[:remember_me] == '1'
|
|
||||||
sign_in(user, message: :two_factor_authenticated, event: :authentication)
|
|
||||||
else
|
else
|
||||||
user.increment_failed_attempts!
|
handle_two_factor_failure(user, 'U2F')
|
||||||
Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=U2F")
|
end
|
||||||
flash.now[:alert] = _('Authentication via U2F device failed.')
|
end
|
||||||
prompt_for_two_factor(user)
|
|
||||||
|
def authenticate_with_two_factor_via_webauthn(user)
|
||||||
|
if Webauthn::AuthenticateService.new(user, user_params[:device_response], session[:challenge]).execute
|
||||||
|
handle_two_factor_success(user)
|
||||||
|
else
|
||||||
|
handle_two_factor_failure(user, 'WebAuthn')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -116,8 +127,39 @@ module AuthenticatesWithTwoFactor
|
||||||
sign_requests: sign_requests })
|
sign_requests: sign_requests })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def setup_webauthn_authentication(user)
|
||||||
|
if user.webauthn_registrations.present?
|
||||||
|
|
||||||
|
webauthn_registration_ids = user.webauthn_registrations.pluck(:credential_xid)
|
||||||
|
|
||||||
|
get_options = WebAuthn::Credential.options_for_get(allow: webauthn_registration_ids,
|
||||||
|
user_verification: 'discouraged',
|
||||||
|
extensions: { appid: WebAuthn.configuration.origin })
|
||||||
|
|
||||||
|
session[:credentialRequestOptions] = get_options
|
||||||
|
session[:challenge] = get_options.challenge
|
||||||
|
gon.push(webauthn: { options: get_options.to_json })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
|
def handle_two_factor_success(user)
|
||||||
|
# Remove any lingering user data from login
|
||||||
|
clear_two_factor_attempt!
|
||||||
|
|
||||||
|
remember_me(user) if user_params[:remember_me] == '1'
|
||||||
|
sign_in(user, message: :two_factor_authenticated, event: :authentication)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_two_factor_failure(user, method)
|
||||||
|
user.increment_failed_attempts!
|
||||||
|
Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=#{method}")
|
||||||
|
flash.now[:alert] = _('Authentication via %{method} device failed.') % { method: method }
|
||||||
|
prompt_for_two_factor(user)
|
||||||
|
end
|
||||||
|
|
||||||
def handle_changed_user(user)
|
def handle_changed_user(user)
|
||||||
clear_two_factor_attempt!
|
clear_two_factor_attempt!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
||||||
skip_before_action :check_two_factor_requirement
|
skip_before_action :check_two_factor_requirement
|
||||||
|
before_action do
|
||||||
|
push_frontend_feature_flag(:webauthn)
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
unless current_user.two_factor_enabled?
|
unless current_user.two_factor_enabled?
|
||||||
|
|
@ -33,7 +36,12 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
||||||
|
|
||||||
@qr_code = build_qr_code
|
@qr_code = build_qr_code
|
||||||
@account_string = account_string
|
@account_string = account_string
|
||||||
setup_u2f_registration
|
|
||||||
|
if Feature.enabled?(:webauthn)
|
||||||
|
setup_webauthn_registration
|
||||||
|
else
|
||||||
|
setup_u2f_registration
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|
@ -48,7 +56,13 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
||||||
else
|
else
|
||||||
@error = _('Invalid pin code')
|
@error = _('Invalid pin code')
|
||||||
@qr_code = build_qr_code
|
@qr_code = build_qr_code
|
||||||
setup_u2f_registration
|
|
||||||
|
if Feature.enabled?(:webauthn)
|
||||||
|
setup_webauthn_registration
|
||||||
|
else
|
||||||
|
setup_u2f_registration
|
||||||
|
end
|
||||||
|
|
||||||
render 'show'
|
render 'show'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -56,7 +70,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
||||||
# A U2F (universal 2nd factor) device's information is stored after successful
|
# A U2F (universal 2nd factor) device's information is stored after successful
|
||||||
# registration, which is then used while 2FA authentication is taking place.
|
# registration, which is then used while 2FA authentication is taking place.
|
||||||
def create_u2f
|
def create_u2f
|
||||||
@u2f_registration = U2fRegistration.register(current_user, u2f_app_id, u2f_registration_params, session[:challenges])
|
@u2f_registration = U2fRegistration.register(current_user, u2f_app_id, device_registration_params, session[:challenges])
|
||||||
|
|
||||||
if @u2f_registration.persisted?
|
if @u2f_registration.persisted?
|
||||||
session.delete(:challenges)
|
session.delete(:challenges)
|
||||||
|
|
@ -68,6 +82,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_webauthn
|
||||||
|
@webauthn_registration = Webauthn::RegisterService.new(current_user, device_registration_params, session[:challenge]).execute
|
||||||
|
if @webauthn_registration.persisted?
|
||||||
|
session.delete(:challenge)
|
||||||
|
|
||||||
|
redirect_to profile_two_factor_auth_path, notice: s_("Your WebAuthn device was registered!")
|
||||||
|
else
|
||||||
|
@qr_code = build_qr_code
|
||||||
|
|
||||||
|
setup_webauthn_registration
|
||||||
|
|
||||||
|
render :show
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def codes
|
def codes
|
||||||
Users::UpdateService.new(current_user, user: current_user).execute! do |user|
|
Users::UpdateService.new(current_user, user: current_user).execute! do |user|
|
||||||
@codes = user.generate_otp_backup_codes!
|
@codes = user.generate_otp_backup_codes!
|
||||||
|
|
@ -112,11 +141,11 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
||||||
# Actual communication is performed using a Javascript API
|
# Actual communication is performed using a Javascript API
|
||||||
def setup_u2f_registration
|
def setup_u2f_registration
|
||||||
@u2f_registration ||= U2fRegistration.new
|
@u2f_registration ||= U2fRegistration.new
|
||||||
@u2f_registrations = current_user.u2f_registrations
|
@registrations = u2f_registrations
|
||||||
u2f = U2F::U2F.new(u2f_app_id)
|
u2f = U2F::U2F.new(u2f_app_id)
|
||||||
|
|
||||||
registration_requests = u2f.registration_requests
|
registration_requests = u2f.registration_requests
|
||||||
sign_requests = u2f.authentication_requests(@u2f_registrations.map(&:key_handle))
|
sign_requests = u2f.authentication_requests(current_user.u2f_registrations.map(&:key_handle))
|
||||||
session[:challenges] = registration_requests.map(&:challenge)
|
session[:challenges] = registration_requests.map(&:challenge)
|
||||||
|
|
||||||
gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
|
gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
|
||||||
|
|
@ -124,8 +153,53 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
||||||
sign_requests: sign_requests })
|
sign_requests: sign_requests })
|
||||||
end
|
end
|
||||||
|
|
||||||
def u2f_registration_params
|
def device_registration_params
|
||||||
params.require(:u2f_registration).permit(:device_response, :name)
|
params.require(:device_registration).permit(:device_response, :name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_webauthn_registration
|
||||||
|
@registrations = webauthn_registrations
|
||||||
|
@webauthn_registration ||= WebauthnRegistration.new
|
||||||
|
|
||||||
|
unless current_user.webauthn_xid
|
||||||
|
current_user.user_detail.update!(webauthn_xid: WebAuthn.generate_user_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
options = webauthn_options
|
||||||
|
session[:challenge] = options.challenge
|
||||||
|
|
||||||
|
gon.push(webauthn: { options: options, app_id: u2f_app_id })
|
||||||
|
end
|
||||||
|
|
||||||
|
# Adds delete path to u2f registrations
|
||||||
|
# to reduce logic in view template
|
||||||
|
def u2f_registrations
|
||||||
|
current_user.u2f_registrations.map do |u2f_registration|
|
||||||
|
{
|
||||||
|
name: u2f_registration.name,
|
||||||
|
created_at: u2f_registration.created_at,
|
||||||
|
delete_path: profile_u2f_registration_path(u2f_registration)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def webauthn_registrations
|
||||||
|
current_user.webauthn_registrations.map do |webauthn_registration|
|
||||||
|
{
|
||||||
|
name: webauthn_registration.name,
|
||||||
|
created_at: webauthn_registration.created_at,
|
||||||
|
delete_path: profile_webauthn_registration_path(webauthn_registration)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def webauthn_options
|
||||||
|
WebAuthn::Credential.options_for_create(
|
||||||
|
user: { id: current_user.webauthn_xid, name: current_user.username },
|
||||||
|
exclude: current_user.webauthn_registrations.map { |c| c.credential_xid },
|
||||||
|
authenticator_selection: { user_verification: 'discouraged' },
|
||||||
|
rp: { name: 'GitLab' }
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def groups_notification(groups)
|
def groups_notification(groups)
|
||||||
|
|
@ -133,6 +207,6 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
||||||
leave_group_links = groups.map { |group| view_context.link_to (s_("leave %{group_name}") % { group_name: group.full_name }), leave_group_members_path(group), remote: false, method: :delete}.to_sentence
|
leave_group_links = groups.map { |group| view_context.link_to (s_("leave %{group_name}") % { group_name: group.full_name }), leave_group_members_path(group), remote: false, method: :delete}.to_sentence
|
||||||
|
|
||||||
s_(%{The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}.})
|
s_(%{The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}.})
|
||||||
.html_safe % { group_links: group_links.html_safe, leave_group_links: leave_group_links.html_safe }
|
.html_safe % { group_links: group_links.html_safe, leave_group_links: leave_group_links.html_safe }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Profiles::WebauthnRegistrationsController < Profiles::ApplicationController
|
||||||
|
def destroy
|
||||||
|
webauthn_registration = current_user.webauthn_registrations.find(params[:id])
|
||||||
|
webauthn_registration.destroy
|
||||||
|
|
||||||
|
redirect_to profile_two_factor_auth_path, status: :found, notice: _("Successfully deleted WebAuthn device.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -6,6 +6,9 @@ class ProfilesController < Profiles::ApplicationController
|
||||||
before_action :user
|
before_action :user
|
||||||
before_action :authorize_change_username!, only: :update_username
|
before_action :authorize_change_username!, only: :update_username
|
||||||
skip_before_action :require_email, only: [:show, :update]
|
skip_before_action :require_email, only: [:show, :update]
|
||||||
|
before_action do
|
||||||
|
push_frontend_feature_flag(:webauthn)
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
|
||||||
:discussion_locked,
|
:discussion_locked,
|
||||||
label_ids: [],
|
label_ids: [],
|
||||||
assignee_ids: [],
|
assignee_ids: [],
|
||||||
|
reviewer_ids: [],
|
||||||
update_task: [:index, :checked, :line_number, :line_source]
|
update_task: [:index, :checked, :line_number, :line_source]
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class ProjectsController < Projects::ApplicationController
|
||||||
|
|
||||||
before_action only: [:edit] do
|
before_action only: [:edit] do
|
||||||
push_frontend_feature_flag(:service_desk_custom_address, @project)
|
push_frontend_feature_flag(:service_desk_custom_address, @project)
|
||||||
push_frontend_feature_flag(:approval_suggestions, @project)
|
push_frontend_feature_flag(:approval_suggestions, @project, default_enabled: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
layout :determine_layout
|
layout :determine_layout
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@ class SessionsController < Devise::SessionsController
|
||||||
before_action :save_failed_login, if: :action_new_and_failed_login?
|
before_action :save_failed_login, if: :action_new_and_failed_login?
|
||||||
before_action :load_recaptcha
|
before_action :load_recaptcha
|
||||||
before_action :set_invite_params, only: [:new]
|
before_action :set_invite_params, only: [:new]
|
||||||
|
before_action do
|
||||||
|
push_frontend_feature_flag(:webauthn)
|
||||||
|
end
|
||||||
|
|
||||||
after_action :log_failed_login, if: :action_new_and_failed_login?
|
after_action :log_failed_login, if: :action_new_and_failed_login?
|
||||||
after_action :verify_known_sign_in, only: [:create]
|
after_action :verify_known_sign_in, only: [:create]
|
||||||
|
|
@ -293,7 +296,9 @@ class SessionsController < Devise::SessionsController
|
||||||
def authentication_method
|
def authentication_method
|
||||||
if user_params[:otp_attempt]
|
if user_params[:otp_attempt]
|
||||||
"two-factor"
|
"two-factor"
|
||||||
elsif user_params[:device_response]
|
elsif user_params[:device_response] && Feature.enabled?(:webauthn)
|
||||||
|
"two-factor-via-webauthn-device"
|
||||||
|
elsif user_params[:device_response] && !Feature.enabled?(:webauthn)
|
||||||
"two-factor-via-u2f-device"
|
"two-factor-via-u2f-device"
|
||||||
else
|
else
|
||||||
"standard"
|
"standard"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Types
|
||||||
|
class IssuableSeverityEnum < BaseEnum
|
||||||
|
graphql_name 'IssuableSeverity'
|
||||||
|
description 'Incident severity'
|
||||||
|
|
||||||
|
::IssuableSeverity.severities.keys.each do |severity|
|
||||||
|
value severity.upcase, value: severity, description: "#{severity.titleize} severity"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -105,6 +105,9 @@ module Types
|
||||||
Types::AlertManagement::AlertType,
|
Types::AlertManagement::AlertType,
|
||||||
null: true,
|
null: true,
|
||||||
description: 'Alert associated to this issue'
|
description: 'Alert associated to this issue'
|
||||||
|
|
||||||
|
field :severity, Types::IssuableSeverityEnum, null: true,
|
||||||
|
description: 'Severity level of the incident'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,20 +100,43 @@ module DiffHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def submodule_link(blob, ref, repository = @repository)
|
def submodule_link(blob, ref, repository = @repository)
|
||||||
project_url, tree_url = submodule_links(blob, ref, repository)
|
urls = submodule_links(blob, ref, repository)
|
||||||
commit_id = if tree_url.nil?
|
|
||||||
Commit.truncate_sha(blob.id)
|
folder_name = truncate(blob.name, length: 40)
|
||||||
else
|
folder_name = link_to(folder_name, urls.web) if urls&.web
|
||||||
link_to Commit.truncate_sha(blob.id), tree_url
|
|
||||||
end
|
commit_id = Commit.truncate_sha(blob.id)
|
||||||
|
commit_id = link_to(commit_id, urls.tree) if urls&.tree
|
||||||
|
|
||||||
[
|
[
|
||||||
content_tag(:span, link_to(truncate(blob.name, length: 40), project_url)),
|
content_tag(:span, folder_name),
|
||||||
'@',
|
'@',
|
||||||
content_tag(:span, commit_id, class: 'commit-sha')
|
content_tag(:span, commit_id, class: 'commit-sha')
|
||||||
].join(' ').html_safe
|
].join(' ').html_safe
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def submodule_diff_compare_link(diff_file)
|
||||||
|
compare_url = submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository, diff_file)&.compare
|
||||||
|
|
||||||
|
link = ""
|
||||||
|
|
||||||
|
if compare_url
|
||||||
|
|
||||||
|
link_text = [
|
||||||
|
_('Compare'),
|
||||||
|
' ',
|
||||||
|
content_tag(:span, Commit.truncate_sha(diff_file.old_blob.id), class: 'commit-sha'),
|
||||||
|
'...',
|
||||||
|
content_tag(:span, Commit.truncate_sha(diff_file.blob.id), class: 'commit-sha')
|
||||||
|
].join('').html_safe
|
||||||
|
|
||||||
|
tooltip = _('Compare submodule commit revisions')
|
||||||
|
link = content_tag(:span, link_to(link_text, compare_url, class: 'btn has-tooltip', title: tooltip), class: 'submodule-compare')
|
||||||
|
end
|
||||||
|
|
||||||
|
link
|
||||||
|
end
|
||||||
|
|
||||||
def diff_file_blob_raw_url(diff_file, only_path: false)
|
def diff_file_blob_raw_url(diff_file, only_path: false)
|
||||||
project_raw_url(@project, tree_join(diff_file.content_sha, diff_file.file_path), only_path: only_path)
|
project_raw_url(@project, tree_join(diff_file.content_sha, diff_file.file_path), only_path: only_path)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ module SubmoduleHelper
|
||||||
VALID_SUBMODULE_PROTOCOLS = %w[http https git ssh].freeze
|
VALID_SUBMODULE_PROTOCOLS = %w[http https git ssh].freeze
|
||||||
|
|
||||||
# links to files listing for submodule if submodule is a project on this server
|
# links to files listing for submodule if submodule is a project on this server
|
||||||
def submodule_links(submodule_item, ref = nil, repository = @repository)
|
def submodule_links(submodule_item, ref = nil, repository = @repository, diff_file = nil)
|
||||||
repository.submodule_links.for(submodule_item, ref)
|
repository.submodule_links.for(submodule_item, ref, diff_file)
|
||||||
end
|
end
|
||||||
|
|
||||||
def submodule_links_for_url(submodule_item_id, url, repository)
|
def submodule_links_for_url(submodule_item_id, url, repository, old_submodule_item_id = nil)
|
||||||
return [nil, nil] unless url
|
return [nil, nil, nil] unless url
|
||||||
|
|
||||||
if url == '.' || url == './'
|
if url == '.' || url == './'
|
||||||
url = File.join(Gitlab.config.gitlab.url, repository.project.full_path)
|
url = File.join(Gitlab.config.gitlab.url, repository.project.full_path)
|
||||||
|
|
@ -34,21 +34,24 @@ module SubmoduleHelper
|
||||||
project.sub!(/\.git\z/, '')
|
project.sub!(/\.git\z/, '')
|
||||||
|
|
||||||
if self_url?(url, namespace, project)
|
if self_url?(url, namespace, project)
|
||||||
[url_helpers.namespace_project_path(namespace, project),
|
[
|
||||||
url_helpers.namespace_project_tree_path(namespace, project, submodule_item_id)]
|
url_helpers.namespace_project_path(namespace, project),
|
||||||
|
url_helpers.namespace_project_tree_path(namespace, project, submodule_item_id),
|
||||||
|
(url_helpers.namespace_project_compare_path(namespace, project, to: submodule_item_id, from: old_submodule_item_id) if old_submodule_item_id)
|
||||||
|
]
|
||||||
elsif relative_self_url?(url)
|
elsif relative_self_url?(url)
|
||||||
relative_self_links(url, submodule_item_id, repository.project)
|
relative_self_links(url, submodule_item_id, old_submodule_item_id, repository.project)
|
||||||
elsif gist_github_dot_com_url?(url)
|
elsif gist_github_dot_com_url?(url)
|
||||||
gist_github_com_tree_links(namespace, project, submodule_item_id)
|
gist_github_com_tree_links(namespace, project, submodule_item_id)
|
||||||
elsif github_dot_com_url?(url)
|
elsif github_dot_com_url?(url)
|
||||||
github_com_tree_links(namespace, project, submodule_item_id)
|
github_com_tree_links(namespace, project, submodule_item_id, old_submodule_item_id)
|
||||||
elsif gitlab_dot_com_url?(url)
|
elsif gitlab_dot_com_url?(url)
|
||||||
gitlab_com_tree_links(namespace, project, submodule_item_id)
|
gitlab_com_tree_links(namespace, project, submodule_item_id, old_submodule_item_id)
|
||||||
else
|
else
|
||||||
[sanitize_submodule_url(url), nil]
|
[sanitize_submodule_url(url), nil, nil]
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
[sanitize_submodule_url(url), nil]
|
[sanitize_submodule_url(url), nil, nil]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -79,22 +82,30 @@ module SubmoduleHelper
|
||||||
url.start_with?('../', './')
|
url.start_with?('../', './')
|
||||||
end
|
end
|
||||||
|
|
||||||
def gitlab_com_tree_links(namespace, project, commit)
|
def gitlab_com_tree_links(namespace, project, commit, old_commit)
|
||||||
base = ['https://gitlab.com/', namespace, '/', project].join('')
|
base = ['https://gitlab.com/', namespace, '/', project].join('')
|
||||||
[base, [base, '/-/tree/', commit].join('')]
|
[
|
||||||
|
base,
|
||||||
|
[base, '/-/tree/', commit].join(''),
|
||||||
|
([base, '/-/compare/', old_commit, '...', commit].join('') if old_commit)
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def gist_github_com_tree_links(namespace, project, commit)
|
def gist_github_com_tree_links(namespace, project, commit)
|
||||||
base = ['https://gist.github.com/', namespace, '/', project].join('')
|
base = ['https://gist.github.com/', namespace, '/', project].join('')
|
||||||
[base, [base, commit].join('/')]
|
[base, [base, commit].join('/'), nil]
|
||||||
end
|
end
|
||||||
|
|
||||||
def github_com_tree_links(namespace, project, commit)
|
def github_com_tree_links(namespace, project, commit, old_commit)
|
||||||
base = ['https://github.com/', namespace, '/', project].join('')
|
base = ['https://github.com/', namespace, '/', project].join('')
|
||||||
[base, [base, '/tree/', commit].join('')]
|
[
|
||||||
|
base,
|
||||||
|
[base, '/tree/', commit].join(''),
|
||||||
|
([base, '/compare/', old_commit, '...', commit].join('') if old_commit)
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def relative_self_links(relative_path, commit, project)
|
def relative_self_links(relative_path, commit, old_commit, project)
|
||||||
relative_path = relative_path.rstrip
|
relative_path = relative_path.rstrip
|
||||||
absolute_project_path = "/" + project.full_path
|
absolute_project_path = "/" + project.full_path
|
||||||
|
|
||||||
|
|
@ -107,7 +118,7 @@ module SubmoduleHelper
|
||||||
target_namespace_path = File.dirname(submodule_project_path)
|
target_namespace_path = File.dirname(submodule_project_path)
|
||||||
|
|
||||||
if target_namespace_path == '/' || target_namespace_path.start_with?(absolute_project_path)
|
if target_namespace_path == '/' || target_namespace_path.start_with?(absolute_project_path)
|
||||||
return [nil, nil]
|
return [nil, nil, nil]
|
||||||
end
|
end
|
||||||
|
|
||||||
target_namespace_path.sub!(%r{^/}, '')
|
target_namespace_path.sub!(%r{^/}, '')
|
||||||
|
|
@ -116,10 +127,11 @@ module SubmoduleHelper
|
||||||
begin
|
begin
|
||||||
[
|
[
|
||||||
url_helpers.namespace_project_path(target_namespace_path, submodule_base),
|
url_helpers.namespace_project_path(target_namespace_path, submodule_base),
|
||||||
url_helpers.namespace_project_tree_path(target_namespace_path, submodule_base, commit)
|
url_helpers.namespace_project_tree_path(target_namespace_path, submodule_base, commit),
|
||||||
|
(url_helpers.namespace_project_compare_path(target_namespace_path, submodule_base, to: commit, from: old_commit) if old_commit)
|
||||||
]
|
]
|
||||||
rescue ActionController::UrlGenerationError
|
rescue ActionController::UrlGenerationError
|
||||||
[nil, nil]
|
[nil, nil, nil]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,14 @@ module Ci
|
||||||
Ci::BuildTraceChunkFlushWorker.perform_async(id)
|
Ci::BuildTraceChunkFlushWorker.perform_async(id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def persisted?
|
||||||
|
!redis?
|
||||||
|
end
|
||||||
|
|
||||||
|
def live?
|
||||||
|
redis?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def get_data
|
def get_data
|
||||||
|
|
@ -170,14 +178,6 @@ module Ci
|
||||||
save! if changed?
|
save! if changed?
|
||||||
end
|
end
|
||||||
|
|
||||||
def persisted?
|
|
||||||
!redis?
|
|
||||||
end
|
|
||||||
|
|
||||||
def live?
|
|
||||||
redis?
|
|
||||||
end
|
|
||||||
|
|
||||||
def full?
|
def full?
|
||||||
size == CHUNK_SIZE
|
size == CHUNK_SIZE
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -262,7 +262,7 @@ module Ci
|
||||||
|
|
||||||
scope :internal, -> { where(source: internal_sources) }
|
scope :internal, -> { where(source: internal_sources) }
|
||||||
scope :no_child, -> { where.not(source: :parent_pipeline) }
|
scope :no_child, -> { where.not(source: :parent_pipeline) }
|
||||||
scope :ci_sources, -> { where(config_source: Enums::Ci::Pipeline.ci_config_sources_values) }
|
scope :ci_sources, -> { where(source: Enums::Ci::Pipeline.ci_sources.values) }
|
||||||
scope :for_user, -> (user) { where(user: user) }
|
scope :for_user, -> (user) { where(user: user) }
|
||||||
scope :for_sha, -> (sha) { where(sha: sha) }
|
scope :for_sha, -> (sha) { where(sha: sha) }
|
||||||
scope :for_source_sha, -> (source_sha) { where(source_sha: source_sha) }
|
scope :for_source_sha, -> (source_sha) { where(source_sha: source_sha) }
|
||||||
|
|
@ -1033,7 +1033,11 @@ module Ci
|
||||||
end
|
end
|
||||||
|
|
||||||
def cacheable?
|
def cacheable?
|
||||||
Enums::Ci::Pipeline.ci_config_sources.key?(config_source.to_sym)
|
!dangling?
|
||||||
|
end
|
||||||
|
|
||||||
|
def dangling?
|
||||||
|
Enums::Ci::Pipeline.dangling_sources.key?(source.to_sym)
|
||||||
end
|
end
|
||||||
|
|
||||||
def source_ref_path
|
def source_ref_path
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,23 @@ module Enums
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Dangling sources are those events that generate pipelines for which
|
||||||
|
# we don't want to directly affect the ref CI status.
|
||||||
|
# - when a webide pipeline fails it does not change the ref CI status to failed
|
||||||
|
# - when a child pipeline (from parent_pipeline source) fails it affects its
|
||||||
|
# parent pipeline. It's up to the parent to affect the ref CI status
|
||||||
|
# - when an ondemand_dast_scan pipeline runs it is for testing purpose and should
|
||||||
|
# not affect the ref CI status.
|
||||||
|
def self.dangling_sources
|
||||||
|
sources.slice(:webide, :parent_pipeline, :ondemand_dast_scan)
|
||||||
|
end
|
||||||
|
|
||||||
|
# CI sources are those pipeline events that affect the CI status of the ref
|
||||||
|
# they run for. By definition it excludes dangling pipelines.
|
||||||
|
def self.ci_sources
|
||||||
|
sources.except(*dangling_sources.keys)
|
||||||
|
end
|
||||||
|
|
||||||
# Returns the `Hash` to use for creating the `config_sources` enum for
|
# Returns the `Hash` to use for creating the `config_sources` enum for
|
||||||
# `Ci::Pipeline`.
|
# `Ci::Pipeline`.
|
||||||
def self.config_sources
|
def self.config_sources
|
||||||
|
|
@ -50,24 +67,6 @@ module Enums
|
||||||
parameter_source: 7
|
parameter_source: 7
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.ci_config_sources
|
|
||||||
config_sources.slice(
|
|
||||||
:unknown_source,
|
|
||||||
:repository_source,
|
|
||||||
:auto_devops_source,
|
|
||||||
:remote_source,
|
|
||||||
:external_project_source
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.ci_config_sources_values
|
|
||||||
ci_config_sources.values
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.non_ci_config_source_values
|
|
||||||
config_sources.values - ci_config_sources.values
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,10 @@ module Issuable
|
||||||
assignees.count > 1
|
assignees.count > 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def allows_reviewers?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
def supports_time_tracking?
|
def supports_time_tracking?
|
||||||
is_a?(TimeTrackable) && !incident?
|
is_a?(TimeTrackable) && !incident?
|
||||||
end
|
end
|
||||||
|
|
@ -185,6 +189,12 @@ module Issuable
|
||||||
is_a?(Issue) && super
|
is_a?(Issue) && super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def severity
|
||||||
|
return IssuableSeverity::DEFAULT unless incident?
|
||||||
|
|
||||||
|
issuable_severity&.severity || IssuableSeverity::DEFAULT
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def description_max_length_for_new_records_is_valid
|
def description_max_length_for_new_records_is_valid
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class IssuableSeverity < ApplicationRecord
|
class IssuableSeverity < ApplicationRecord
|
||||||
|
DEFAULT = 'unknown'
|
||||||
|
|
||||||
belongs_to :issue
|
belongs_to :issue
|
||||||
|
|
||||||
validates :issue, presence: true, uniqueness: true
|
validates :issue, presence: true, uniqueness: true
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ class Issue < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
has_one :issuable_severity
|
||||||
has_one :sentry_issue
|
has_one :sentry_issue
|
||||||
has_one :alert_management_alert, class_name: 'AlertManagement::Alert'
|
has_one :alert_management_alert, class_name: 'AlertManagement::Alert'
|
||||||
has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
|
has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ class LfsObjectsProject < ApplicationRecord
|
||||||
}
|
}
|
||||||
|
|
||||||
scope :project_id_in, ->(ids) { where(project_id: ids) }
|
scope :project_id_in, ->(ids) { where(project_id: ids) }
|
||||||
|
scope :lfs_object_in, -> (lfs_objects) { where(lfs_object: lfs_objects) }
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ class MembersPreloader
|
||||||
ActiveRecord::Associations::Preloader.new.preload(members, :source)
|
ActiveRecord::Associations::Preloader.new.preload(members, :source)
|
||||||
ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :status)
|
ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :status)
|
||||||
ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations)
|
ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations)
|
||||||
|
ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :webauthn_registrations)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -955,8 +955,9 @@ class MergeRequest < ApplicationRecord
|
||||||
self.class.wip_title(self.title)
|
self.class.wip_title(self.title)
|
||||||
end
|
end
|
||||||
|
|
||||||
def mergeable?(skip_ci_check: false)
|
def mergeable?(skip_ci_check: false, skip_discussions_check: false)
|
||||||
return false unless mergeable_state?(skip_ci_check: skip_ci_check)
|
return false unless mergeable_state?(skip_ci_check: skip_ci_check,
|
||||||
|
skip_discussions_check: skip_discussions_check)
|
||||||
|
|
||||||
check_mergeability
|
check_mergeability
|
||||||
|
|
||||||
|
|
@ -1658,6 +1659,10 @@ class MergeRequest < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def allows_reviewers?
|
||||||
|
Feature.enabled?(:merge_request_reviewers, project)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def with_rebase_lock
|
def with_rebase_lock
|
||||||
|
|
|
||||||
|
|
@ -280,10 +280,9 @@ class Project < ApplicationRecord
|
||||||
# The relation :all_pipelines is intended to be used when we want to get the
|
# The relation :all_pipelines is intended to be used when we want to get the
|
||||||
# whole list of pipelines associated to the project
|
# whole list of pipelines associated to the project
|
||||||
has_many :all_pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
|
has_many :all_pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
|
||||||
# The relation :ci_pipelines is intended to be used when we want to get only
|
# The relation :ci_pipelines includes all those that directly contribute to the
|
||||||
# those pipeline which are directly related to CI. There are
|
# latest status of a ref. This does not include dangling pipelines such as those
|
||||||
# other pipelines, like webide ones, that we won't retrieve
|
# from webide, child pipelines, etc.
|
||||||
# if we use this relation.
|
|
||||||
has_many :ci_pipelines,
|
has_many :ci_pipelines,
|
||||||
-> { ci_sources },
|
-> { ci_sources },
|
||||||
class_name: 'Ci::Pipeline',
|
class_name: 'Ci::Pipeline',
|
||||||
|
|
@ -2709,9 +2708,11 @@ class Project < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def oids(objects, oids: [])
|
def oids(objects, oids: [])
|
||||||
collection = oids.any? ? objects.where(oid: oids) : objects
|
objects = objects.where(oid: oids) if oids.any?
|
||||||
|
|
||||||
collection.pluck(:oid)
|
[].tap do |out|
|
||||||
|
objects.each_batch { |relation| out.concat(relation.pluck(:oid)) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ class User < ApplicationRecord
|
||||||
has_many :personal_access_tokens, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
has_many :personal_access_tokens, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||||
has_many :identities, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent
|
has_many :identities, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent
|
||||||
has_many :u2f_registrations, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
has_many :u2f_registrations, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||||
|
has_many :webauthn_registrations
|
||||||
has_many :chat_names, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
has_many :chat_names, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||||
has_one :user_synced_attributes_metadata, autosave: true
|
has_one :user_synced_attributes_metadata, autosave: true
|
||||||
has_one :aws_role, class_name: 'Aws::Role'
|
has_one :aws_role, class_name: 'Aws::Role'
|
||||||
|
|
@ -286,6 +287,7 @@ class User < ApplicationRecord
|
||||||
delegate :path, to: :namespace, allow_nil: true, prefix: true
|
delegate :path, to: :namespace, allow_nil: true, prefix: true
|
||||||
delegate :job_title, :job_title=, to: :user_detail, allow_nil: true
|
delegate :job_title, :job_title=, to: :user_detail, allow_nil: true
|
||||||
delegate :bio, :bio=, :bio_html, to: :user_detail, allow_nil: true
|
delegate :bio, :bio=, :bio_html, to: :user_detail, allow_nil: true
|
||||||
|
delegate :webauthn_xid, :webauthn_xid=, to: :user_detail, allow_nil: true
|
||||||
|
|
||||||
accepts_nested_attributes_for :user_preference, update_only: true
|
accepts_nested_attributes_for :user_preference, update_only: true
|
||||||
accepts_nested_attributes_for :user_detail, update_only: true
|
accepts_nested_attributes_for :user_detail, update_only: true
|
||||||
|
|
@ -434,14 +436,21 @@ class User < ApplicationRecord
|
||||||
FROM u2f_registrations AS u2f
|
FROM u2f_registrations AS u2f
|
||||||
WHERE u2f.user_id = users.id
|
WHERE u2f.user_id = users.id
|
||||||
) OR users.otp_required_for_login = ?
|
) OR users.otp_required_for_login = ?
|
||||||
|
OR
|
||||||
|
EXISTS (
|
||||||
|
SELECT *
|
||||||
|
FROM webauthn_registrations AS webauthn
|
||||||
|
WHERE webauthn.user_id = users.id
|
||||||
|
)
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
where(with_u2f_registrations, true)
|
where(with_u2f_registrations, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.without_two_factor
|
def self.without_two_factor
|
||||||
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
|
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id
|
||||||
.where("u2f.id IS NULL AND users.otp_required_for_login = ?", false)
|
LEFT OUTER JOIN webauthn_registrations AS webauthn ON webauthn.user_id = users.id")
|
||||||
|
.where("u2f.id IS NULL AND webauthn.id IS NULL AND users.otp_required_for_login = ?", false)
|
||||||
end
|
end
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
@ -754,11 +763,12 @@ class User < ApplicationRecord
|
||||||
otp_backup_codes: nil
|
otp_backup_codes: nil
|
||||||
)
|
)
|
||||||
self.u2f_registrations.destroy_all # rubocop: disable Cop/DestroyAll
|
self.u2f_registrations.destroy_all # rubocop: disable Cop/DestroyAll
|
||||||
|
self.webauthn_registrations.destroy_all # rubocop: disable Cop/DestroyAll
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def two_factor_enabled?
|
def two_factor_enabled?
|
||||||
two_factor_otp_enabled? || two_factor_u2f_enabled?
|
two_factor_otp_enabled? || two_factor_webauthn_u2f_enabled?
|
||||||
end
|
end
|
||||||
|
|
||||||
def two_factor_otp_enabled?
|
def two_factor_otp_enabled?
|
||||||
|
|
@ -773,6 +783,16 @@ class User < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def two_factor_webauthn_u2f_enabled?
|
||||||
|
two_factor_u2f_enabled? || two_factor_webauthn_enabled?
|
||||||
|
end
|
||||||
|
|
||||||
|
def two_factor_webauthn_enabled?
|
||||||
|
return false unless Feature.enabled?(:webauthn)
|
||||||
|
|
||||||
|
(webauthn_registrations.loaded? && webauthn_registrations.any?) || (!webauthn_registrations.loaded? && webauthn_registrations.exists?)
|
||||||
|
end
|
||||||
|
|
||||||
def namespace_move_dir_allowed
|
def namespace_move_dir_allowed
|
||||||
if namespace&.any_project_has_container_registry_tags?
|
if namespace&.any_project_has_container_registry_tags?
|
||||||
errors.add(:username, _('cannot be changed if a personal project has container registry tags.'))
|
errors.add(:username, _('cannot be changed if a personal project has container registry tags.'))
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,23 @@ class DiffFileBaseEntity < Grape::Entity
|
||||||
expose :submodule?, as: :submodule
|
expose :submodule?, as: :submodule
|
||||||
|
|
||||||
expose :submodule_link do |diff_file, options|
|
expose :submodule_link do |diff_file, options|
|
||||||
memoized_submodule_links(diff_file, options).first
|
memoized_submodule_links(diff_file, options)&.web
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :submodule_tree_url do |diff_file|
|
expose :submodule_tree_url do |diff_file|
|
||||||
memoized_submodule_links(diff_file, options).last
|
memoized_submodule_links(diff_file, options)&.tree
|
||||||
|
end
|
||||||
|
|
||||||
|
expose :submodule_compare do |diff_file|
|
||||||
|
url = memoized_submodule_links(diff_file, options)&.compare
|
||||||
|
|
||||||
|
next unless url
|
||||||
|
|
||||||
|
{
|
||||||
|
url: url,
|
||||||
|
old_sha: diff_file.old_blob&.id,
|
||||||
|
new_sha: diff_file.blob&.id
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
|
expose :edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
|
||||||
|
|
@ -96,11 +108,9 @@ class DiffFileBaseEntity < Grape::Entity
|
||||||
|
|
||||||
def memoized_submodule_links(diff_file, options)
|
def memoized_submodule_links(diff_file, options)
|
||||||
strong_memoize(:submodule_links) do
|
strong_memoize(:submodule_links) do
|
||||||
if diff_file.submodule?
|
next unless diff_file.submodule?
|
||||||
options[:submodule_links].for(diff_file.blob, diff_file.content_sha)
|
|
||||||
else
|
options[:submodule_links].for(diff_file.blob, diff_file.content_sha, diff_file)
|
||||||
[]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ class MergeRequestBasicEntity < Grape::Entity
|
||||||
expose :milestone, using: API::Entities::Milestone
|
expose :milestone, using: API::Entities::Milestone
|
||||||
expose :labels, using: LabelEntity
|
expose :labels, using: LabelEntity
|
||||||
expose :assignees, using: API::Entities::UserBasic
|
expose :assignees, using: API::Entities::UserBasic
|
||||||
|
expose :reviewers, if: -> (m) { m.allows_reviewers? }, using: API::Entities::UserBasic
|
||||||
expose :task_status, :task_status_short
|
expose :task_status, :task_status_short
|
||||||
expose :lock_version, :lock_version
|
expose :lock_version, :lock_version
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MergeRequestReviewerEntity < ::API::Entities::UserBasic
|
||||||
|
expose :can_merge do |reviewer, options|
|
||||||
|
options[:merge_request]&.can_be_merged_by?(reviewer)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
MergeRequestReviewerEntity.prepend_if_ee('EE::MergeRequestReviewerEntity')
|
||||||
|
|
@ -4,4 +4,8 @@ class MergeRequestSidebarExtrasEntity < IssuableSidebarExtrasEntity
|
||||||
expose :assignees do |merge_request|
|
expose :assignees do |merge_request|
|
||||||
MergeRequestAssigneeEntity.represent(merge_request.assignees, merge_request: merge_request)
|
MergeRequestAssigneeEntity.represent(merge_request.assignees, merge_request: merge_request)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
expose :reviewers, if: -> (m) { m.allows_reviewers? } do |merge_request|
|
||||||
|
MergeRequestReviewerEntity.represent(merge_request.reviewers, merge_request: merge_request)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class IssuableBaseService < BaseService
|
||||||
params[:assignee_ids] = params[:assignee_ids].first(1)
|
params[:assignee_ids] = params[:assignee_ids].first(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
|
assignee_ids = params[:assignee_ids].select { |assignee_id| user_can_read?(issuable, assignee_id) }
|
||||||
|
|
||||||
if params[:assignee_ids].map(&:to_s) == [IssuableFinder::Params::NONE]
|
if params[:assignee_ids].map(&:to_s) == [IssuableFinder::Params::NONE]
|
||||||
params[:assignee_ids] = []
|
params[:assignee_ids] = []
|
||||||
|
|
@ -57,15 +57,15 @@ class IssuableBaseService < BaseService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def assignee_can_read?(issuable, assignee_id)
|
def user_can_read?(issuable, user_id)
|
||||||
new_assignee = User.find_by_id(assignee_id)
|
user = User.find_by_id(user_id)
|
||||||
|
|
||||||
return false unless new_assignee
|
return false unless user
|
||||||
|
|
||||||
ability_name = :"read_#{issuable.to_ability_name}"
|
ability_name = :"read_#{issuable.to_ability_name}"
|
||||||
resource = issuable.persisted? ? issuable : project
|
resource = issuable.persisted? ? issuable : project
|
||||||
|
|
||||||
can?(new_assignee, ability_name, resource)
|
can?(user, ability_name, resource)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_milestone
|
def filter_milestone
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,28 @@ module MergeRequests
|
||||||
unless merge_request.can_allow_collaboration?(current_user)
|
unless merge_request.can_allow_collaboration?(current_user)
|
||||||
params.delete(:allow_collaboration)
|
params.delete(:allow_collaboration)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
filter_reviewer(merge_request)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_reviewer(merge_request)
|
||||||
|
return if params[:reviewer_ids].blank?
|
||||||
|
|
||||||
|
unless can_admin_issuable?(merge_request) && merge_request.allows_reviewers?
|
||||||
|
params.delete(:reviewer_ids)
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
reviewer_ids = params[:reviewer_ids].select { |reviewer_id| user_can_read?(merge_request, reviewer_id) }
|
||||||
|
|
||||||
|
if params[:reviewer_ids].map(&:to_s) == [IssuableFinder::Params::NONE]
|
||||||
|
params[:reviewer_ids] = []
|
||||||
|
elsif reviewer_ids.any?
|
||||||
|
params[:reviewer_ids] = reviewer_ids
|
||||||
|
else
|
||||||
|
params.delete(:reviewer_ids)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_request_metrics_service(merge_request)
|
def merge_request_metrics_service(merge_request)
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,14 @@ module MergeRequests
|
||||||
class MergeService < MergeRequests::MergeBaseService
|
class MergeService < MergeRequests::MergeBaseService
|
||||||
delegate :merge_jid, :state, to: :@merge_request
|
delegate :merge_jid, :state, to: :@merge_request
|
||||||
|
|
||||||
def execute(merge_request)
|
def execute(merge_request, options = {})
|
||||||
if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService)
|
if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService)
|
||||||
FfMergeService.new(project, current_user, params).execute(merge_request)
|
FfMergeService.new(project, current_user, params).execute(merge_request)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@merge_request = merge_request
|
@merge_request = merge_request
|
||||||
|
@options = options
|
||||||
|
|
||||||
validate!
|
validate!
|
||||||
|
|
||||||
|
|
@ -55,7 +56,7 @@ module MergeRequests
|
||||||
error =
|
error =
|
||||||
if @merge_request.should_be_rebased?
|
if @merge_request.should_be_rebased?
|
||||||
'Only fast-forward merge is allowed for your project. Please update your source branch'
|
'Only fast-forward merge is allowed for your project. Please update your source branch'
|
||||||
elsif !@merge_request.mergeable?
|
elsif !@merge_request.mergeable?(skip_discussions_check: @options[:skip_discussions_check])
|
||||||
'Merge request is not mergeable'
|
'Merge request is not mergeable'
|
||||||
elsif !@merge_request.squash && project.squash_always?
|
elsif !@merge_request.squash && project.squash_always?
|
||||||
'This project requires squashing commits when merge requests are accepted.'
|
'This project requires squashing commits when merge requests are accepted.'
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Webauthn
|
||||||
|
class AuthenticateService < BaseService
|
||||||
|
def initialize(user, device_response, challenge)
|
||||||
|
@user = user
|
||||||
|
@device_response = device_response
|
||||||
|
@challenge = challenge
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
parsed_device_response = Gitlab::Json.parse(@device_response)
|
||||||
|
|
||||||
|
# appid is set for legacy U2F devices, will be used in a future iteration
|
||||||
|
# rp_id = @app_id
|
||||||
|
# unless parsed_device_response['clientExtensionResults'] && parsed_device_response['clientExtensionResults']['appid']
|
||||||
|
# rp_id = URI(@app_id).host
|
||||||
|
# end
|
||||||
|
|
||||||
|
webauthn_credential = WebAuthn::Credential.from_get(parsed_device_response)
|
||||||
|
encoded_raw_id = Base64.strict_encode64(webauthn_credential.raw_id)
|
||||||
|
stored_webauthn_credential = @user.webauthn_registrations.find_by_credential_xid(encoded_raw_id)
|
||||||
|
|
||||||
|
encoder = WebAuthn.configuration.encoder
|
||||||
|
|
||||||
|
if stored_webauthn_credential &&
|
||||||
|
validate_webauthn_credential(webauthn_credential) &&
|
||||||
|
verify_webauthn_credential(webauthn_credential, stored_webauthn_credential, @challenge, encoder)
|
||||||
|
|
||||||
|
stored_webauthn_credential.update!(counter: webauthn_credential.sign_count)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
false
|
||||||
|
rescue JSON::ParserError, WebAuthn::SignCountVerificationError, WebAuthn::Error
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Validates that webauthn_credential is syntactically valid
|
||||||
|
#
|
||||||
|
# duplicated from WebAuthn::PublicKeyCredential#verify
|
||||||
|
# which can't be used here as we need to call WebAuthn::AuthenticatorAssertionResponse#verify instead
|
||||||
|
# (which is done in #verify_webauthn_credential)
|
||||||
|
def validate_webauthn_credential(webauthn_credential)
|
||||||
|
webauthn_credential.type == WebAuthn::TYPE_PUBLIC_KEY &&
|
||||||
|
webauthn_credential.raw_id && webauthn_credential.id &&
|
||||||
|
webauthn_credential.raw_id == WebAuthn.standard_encoder.decode(webauthn_credential.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Verifies that webauthn_credential matches stored_credential with the given challenge
|
||||||
|
#
|
||||||
|
def verify_webauthn_credential(webauthn_credential, stored_credential, challenge, encoder)
|
||||||
|
webauthn_credential.response.verify(
|
||||||
|
encoder.decode(challenge),
|
||||||
|
public_key: encoder.decode(stored_credential.public_key),
|
||||||
|
sign_count: stored_credential.counter)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Webauthn
|
||||||
|
class RegisterService < BaseService
|
||||||
|
def initialize(user, params, challenge)
|
||||||
|
@user = user
|
||||||
|
@params = params
|
||||||
|
@challenge = challenge
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
registration = WebauthnRegistration.new
|
||||||
|
|
||||||
|
begin
|
||||||
|
webauthn_credential = WebAuthn::Credential.from_create(Gitlab::Json.parse(@params[:device_response]))
|
||||||
|
webauthn_credential.verify(@challenge)
|
||||||
|
|
||||||
|
registration.update(
|
||||||
|
credential_xid: Base64.strict_encode64(webauthn_credential.raw_id),
|
||||||
|
public_key: webauthn_credential.public_key,
|
||||||
|
counter: webauthn_credential.sign_count,
|
||||||
|
name: @params[:name],
|
||||||
|
user: @user
|
||||||
|
)
|
||||||
|
rescue JSON::ParserError
|
||||||
|
registration.errors.add(:base, _('Your WebAuthn device did not send a valid JSON response.'))
|
||||||
|
rescue WebAuthn::Error => e
|
||||||
|
registration.errors.add(:base, e.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
registration
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
= form_tag(admin_session_path, { method: :post, class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if current_user.two_factor_u2f_enabled?}" }) do
|
= form_tag(admin_session_path, { method: :post, class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if current_user.two_factor_webauthn_u2f_enabled?}" }) do
|
||||||
.form-group
|
.form-group
|
||||||
= label_tag :user_otp_attempt, _('Two-Factor Authentication code')
|
= label_tag :user_otp_attempt, _('Two-Factor Authentication code')
|
||||||
= text_field_tag 'user[otp_attempt]', nil, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: _('This field is required.')
|
= text_field_tag 'user[otp_attempt]', nil, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: _('This field is required.')
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,5 @@
|
||||||
.login-body
|
.login-body
|
||||||
- if current_user.two_factor_otp_enabled?
|
- if current_user.two_factor_otp_enabled?
|
||||||
= render 'admin/sessions/two_factor_otp'
|
= render 'admin/sessions/two_factor_otp'
|
||||||
- if current_user.two_factor_u2f_enabled?
|
- if current_user.two_factor_webauthn_u2f_enabled?
|
||||||
= render 'u2f/authenticate', render_remember_me: false, target_path: admin_session_path
|
= render 'authentication/authenticate', render_remember_me: false, target_path: admin_session_path
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
%script#js-authenticate-token-2fa-error{ type: "text/template" }
|
%script#js-authenticate-token-2fa-error{ type: "text/template" }
|
||||||
%div
|
%div
|
||||||
%p <%= error_message %> (#{_("error code:")} <%= error_code %>)
|
%p <%= error_message %> (<%= error_name %>)
|
||||||
%a.btn.btn-block.btn-warning#js-token-2fa-try-again= _("Try again?")
|
%a.btn.btn-block.btn-warning#js-token-2fa-try-again= _("Try again?")
|
||||||
|
|
||||||
%script#js-authenticate-token-2fa-authenticated{ type: "text/template" }
|
%script#js-authenticate-token-2fa-authenticated{ type: "text/template" }
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
#js-register-token-2fa
|
||||||
|
|
||||||
|
-# haml-lint:disable InlineJavaScript
|
||||||
|
%script#js-register-2fa-message{ type: "text/template" }
|
||||||
|
%p <%= message %>
|
||||||
|
|
||||||
|
%script#js-register-token-2fa-setup{ type: "text/template" }
|
||||||
|
- if current_user.two_factor_otp_enabled?
|
||||||
|
.row.gl-mb-3
|
||||||
|
.col-md-5
|
||||||
|
%button#js-setup-token-2fa-device.btn.btn-info= _("Set up new device")
|
||||||
|
.col-md-7
|
||||||
|
%p= _("Your device needs to be set up. Plug it in (if needed) and click the button on the left.")
|
||||||
|
- else
|
||||||
|
.row.gl-mb-3
|
||||||
|
.col-md-4
|
||||||
|
%button#js-setup-token-2fa-device.btn.btn-info.btn-block{ disabled: true }= _("Set up new device")
|
||||||
|
.col-md-8
|
||||||
|
%p= _("You need to register a two-factor authentication app before you can set up a device.")
|
||||||
|
|
||||||
|
%script#js-register-token-2fa-error{ type: "text/template" }
|
||||||
|
%div
|
||||||
|
%p
|
||||||
|
%span <%= error_message %> (<%= error_name %>)
|
||||||
|
%a.btn.btn-warning#js-token-2fa-try-again= _("Try again?")
|
||||||
|
|
||||||
|
%script#js-register-token-2fa-registered{ type: "text/template" }
|
||||||
|
.row.gl-mb-3
|
||||||
|
.col-md-12
|
||||||
|
%p= _("Your device was successfully set up! Give it a name and register it with the GitLab server.")
|
||||||
|
= form_tag(target_path, method: :post) do
|
||||||
|
.row.gl-mb-3
|
||||||
|
.col-md-3
|
||||||
|
= text_field_tag 'device_registration[name]', nil, class: 'form-control', placeholder: _("Pick a name")
|
||||||
|
.col-md-3
|
||||||
|
= hidden_field_tag 'device_registration[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
|
||||||
|
= submit_tag _("Register device"), class: "btn btn-success"
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
.login-box
|
.login-box
|
||||||
.login-body
|
.login-body
|
||||||
- if @user.two_factor_otp_enabled?
|
- if @user.two_factor_otp_enabled?
|
||||||
= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if @user.two_factor_u2f_enabled?}" }) do |f|
|
= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if @user.two_factor_webauthn_u2f_enabled?}" }) do |f|
|
||||||
- resource_params = params[resource_name].presence || params
|
- resource_params = params[resource_name].presence || params
|
||||||
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
|
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
|
||||||
%div
|
%div
|
||||||
|
|
@ -12,6 +12,5 @@
|
||||||
%p.form-text.text-muted.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
|
%p.form-text.text-muted.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
|
||||||
.prepend-top-20
|
.prepend-top-20
|
||||||
= f.submit "Verify code", class: "btn btn-success", data: { qa_selector: 'verify_code_button' }
|
= f.submit "Verify code", class: "btn btn-success", data: { qa_selector: 'verify_code_button' }
|
||||||
|
- if @user.two_factor_webauthn_u2f_enabled?
|
||||||
- if @user.two_factor_u2f_enabled?
|
= render "authentication/authenticate", params: params, resource: resource, resource_name: resource_name, render_remember_me: true, target_path: new_user_session_path
|
||||||
= render "u2f/authenticate", params: params, resource: resource, resource_name: resource_name, render_remember_me: true, target_path: new_user_session_path
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
- page_title _('Two-Factor Authentication'), _('Account')
|
- page_title _('Two-Factor Authentication'), _('Account')
|
||||||
- add_to_breadcrumbs(_('Two-Factor Authentication'), profile_account_path)
|
- add_to_breadcrumbs(_('Two-Factor Authentication'), profile_account_path)
|
||||||
- @content_class = "limit-container-width" unless fluid_layout
|
- @content_class = "limit-container-width" unless fluid_layout
|
||||||
|
- webauthn_enabled = Feature.enabled?(:webauthn)
|
||||||
|
|
||||||
.js-two-factor-auth{ 'data-two-factor-skippable' => "#{two_factor_skippable?}", 'data-two_factor_skip_url' => skip_profile_two_factor_auth_path }
|
.js-two-factor-auth{ 'data-two-factor-skippable' => "#{two_factor_skippable?}", 'data-two_factor_skip_url' => skip_profile_two_factor_auth_path }
|
||||||
.row.gl-mt-3
|
.row.gl-mt-3
|
||||||
|
|
@ -18,7 +19,7 @@
|
||||||
%div
|
%div
|
||||||
= link_to _('Disable two-factor authentication'), profile_two_factor_auth_path,
|
= link_to _('Disable two-factor authentication'), profile_two_factor_auth_path,
|
||||||
method: :delete,
|
method: :delete,
|
||||||
data: { confirm: _('Are you sure? This will invalidate your registered applications and U2F devices.') },
|
data: { confirm: webauthn_enabled ? _('Are you sure? This will invalidate your registered applications and U2F / WebAuthn devices.') : _('Are you sure? This will invalidate your registered applications and U2F devices.') },
|
||||||
class: 'btn btn-danger gl-mr-3'
|
class: 'btn btn-danger gl-mr-3'
|
||||||
= form_tag codes_profile_two_factor_auth_path, {style: 'display: inline-block', method: :post} do |f|
|
= form_tag codes_profile_two_factor_auth_path, {style: 'display: inline-block', method: :post} do |f|
|
||||||
= submit_tag _('Regenerate recovery codes'), class: 'btn'
|
= submit_tag _('Regenerate recovery codes'), class: 'btn'
|
||||||
|
|
@ -58,22 +59,35 @@
|
||||||
.row.gl-mt-3
|
.row.gl-mt-3
|
||||||
.col-lg-4
|
.col-lg-4
|
||||||
%h4.gl-mt-0
|
%h4.gl-mt-0
|
||||||
= _('Register Universal Two-Factor (U2F) Device')
|
- if webauthn_enabled
|
||||||
|
= _('Register WebAuthn Device')
|
||||||
|
- else
|
||||||
|
= _('Register Universal Two-Factor (U2F) Device')
|
||||||
%p
|
%p
|
||||||
= _('Use a hardware device to add the second factor of authentication.')
|
= _('Use a hardware device to add the second factor of authentication.')
|
||||||
%p
|
%p
|
||||||
= _("As U2F devices are only supported by a few browsers, we require that you set up a two-factor authentication app before a U2F device. That way you'll always be able to log in - even when you're using an unsupported browser.")
|
- if webauthn_enabled
|
||||||
|
= _("As WebAuthn devices are only supported by a few browsers, we require that you set up a two-factor authentication app before a WebAuthn device. That way you'll always be able to log in - even when you're using an unsupported browser.")
|
||||||
|
- else
|
||||||
|
= _("As U2F devices are only supported by a few browsers, we require that you set up a two-factor authentication app before a U2F device. That way you'll always be able to log in - even when you're using an unsupported browser.")
|
||||||
.col-lg-8
|
.col-lg-8
|
||||||
- if @u2f_registration.errors.present?
|
- registration = webauthn_enabled ? @webauthn_registration : @u2f_registration
|
||||||
= form_errors(@u2f_registration)
|
- if registration.errors.present?
|
||||||
= render "u2f/register"
|
= form_errors(registration)
|
||||||
|
- if webauthn_enabled
|
||||||
|
= render "authentication/register", target_path: create_webauthn_profile_two_factor_auth_path
|
||||||
|
- else
|
||||||
|
= render "authentication/register", target_path: create_u2f_profile_two_factor_auth_path
|
||||||
|
|
||||||
%hr
|
%hr
|
||||||
|
|
||||||
%h5
|
%h5
|
||||||
= _('U2F Devices (%{length})') % { length: @u2f_registrations.length }
|
- if webauthn_enabled
|
||||||
|
= _('WebAuthn Devices (%{length})') % { length: @registrations.length }
|
||||||
|
- else
|
||||||
|
= _('U2F Devices (%{length})') % { length: @registrations.length }
|
||||||
|
|
||||||
- if @u2f_registrations.present?
|
- if @registrations.present?
|
||||||
.table-responsive
|
.table-responsive
|
||||||
%table.table.table-bordered.u2f-registrations
|
%table.table.table-bordered.u2f-registrations
|
||||||
%colgroup
|
%colgroup
|
||||||
|
|
@ -86,12 +100,15 @@
|
||||||
%th= s_('2FADevice|Registered On')
|
%th= s_('2FADevice|Registered On')
|
||||||
%th
|
%th
|
||||||
%tbody
|
%tbody
|
||||||
- @u2f_registrations.each do |registration|
|
- @registrations.each do |registration|
|
||||||
%tr
|
%tr
|
||||||
%td= registration.name.presence || html_escape_once(_("<no name set>")).html_safe
|
%td= registration[:name].presence || html_escape_once(_("<no name set>")).html_safe
|
||||||
%td= registration.created_at.to_date.to_s(:medium)
|
%td= registration[:created_at].to_date.to_s(:medium)
|
||||||
%td= link_to _('Delete'), profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger float-right", data: { confirm: _('Are you sure you want to delete this device? This action cannot be undone.') }
|
%td= link_to _('Delete'), registration[:delete_path], method: :delete, class: "btn btn-danger float-right", data: { confirm: _('Are you sure you want to delete this device? This action cannot be undone.') }
|
||||||
|
|
||||||
- else
|
- else
|
||||||
.settings-message.text-center
|
.settings-message.text-center
|
||||||
= _("You don't have any U2F devices registered yet.")
|
- if webauthn_enabled
|
||||||
|
= _("You don't have any WebAuthn devices registered yet.")
|
||||||
|
- else
|
||||||
|
= _("You don't have any U2F devices registered yet.")
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@
|
||||||
.file-header-content
|
.file-header-content
|
||||||
= render "projects/diffs/file_header", diff_file: diff_file, url: "##{file_hash}"
|
= render "projects/diffs/file_header", diff_file: diff_file, url: "##{file_hash}"
|
||||||
|
|
||||||
|
- if diff_file.submodule?
|
||||||
|
.file-actions.d-none.d-sm-block
|
||||||
|
= submodule_diff_compare_link(diff_file)
|
||||||
|
|
||||||
- unless diff_file.submodule?
|
- unless diff_file.submodule?
|
||||||
- blob = diff_file.blob
|
- blob = diff_file.blob
|
||||||
.file-actions.d-none.d-sm-block
|
.file-actions.d-none.d-sm-block
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
#js-register-u2f
|
|
||||||
|
|
||||||
-# haml-lint:disable InlineJavaScript
|
|
||||||
%script#js-register-u2f-not-supported{ type: "text/template" }
|
|
||||||
%p= _("Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).")
|
|
||||||
|
|
||||||
%script#js-register-u2f-setup{ type: "text/template" }
|
|
||||||
- if current_user.two_factor_otp_enabled?
|
|
||||||
.row.gl-mb-3
|
|
||||||
.col-md-4
|
|
||||||
%button#js-setup-u2f-device.btn.btn-info.btn-block= _("Set up new U2F device")
|
|
||||||
.col-md-8
|
|
||||||
%p= _("Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left.")
|
|
||||||
- else
|
|
||||||
.row.gl-mb-3
|
|
||||||
.col-md-4
|
|
||||||
%button#js-setup-u2f-device.btn.btn-info.btn-block{ disabled: true }= _("Set up new U2F device")
|
|
||||||
.col-md-8
|
|
||||||
%p= _("You need to register a two-factor authentication app before you can set up a U2F device.")
|
|
||||||
|
|
||||||
%script#js-register-u2f-in-progress{ type: "text/template" }
|
|
||||||
%p= _("Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.")
|
|
||||||
|
|
||||||
%script#js-register-u2f-error{ type: "text/template" }
|
|
||||||
%div
|
|
||||||
%p
|
|
||||||
%span <%= error_message %> (#{_("error code:")} <%= error_code %>)
|
|
||||||
%a.btn.btn-warning#js-token-2fa-try-again= _("Try again?")
|
|
||||||
|
|
||||||
%script#js-register-u2f-registered{ type: "text/template" }
|
|
||||||
.row.gl-mb-3
|
|
||||||
.col-md-12
|
|
||||||
%p= _("Your device was successfully set up! Give it a name and register it with the GitLab server.")
|
|
||||||
= form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do
|
|
||||||
.row.gl-mb-3
|
|
||||||
.col-md-3
|
|
||||||
= text_field_tag 'u2f_registration[name]', nil, class: 'form-control', placeholder: _("Pick a name")
|
|
||||||
.col-md-3
|
|
||||||
= hidden_field_tag 'u2f_registration[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
|
|
||||||
= submit_tag _("Register U2F device"), class: "btn btn-success"
|
|
||||||
|
|
@ -897,7 +897,7 @@
|
||||||
:urgency: :low
|
:urgency: :low
|
||||||
:resource_boundary: :unknown
|
:resource_boundary: :unknown
|
||||||
:weight: 1
|
:weight: 1
|
||||||
:idempotent:
|
:idempotent: true
|
||||||
:tags: []
|
:tags: []
|
||||||
- :name: pipeline_background:ci_daily_build_group_report_results
|
- :name: pipeline_background:ci_daily_build_group_report_results
|
||||||
:feature_category: :continuous_integration
|
:feature_category: :continuous_integration
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Ci
|
module Ci
|
||||||
class BuildTraceChunkFlushWorker # rubocop:disable Scalability/IdempotentWorker
|
class BuildTraceChunkFlushWorker
|
||||||
include ApplicationWorker
|
include ApplicationWorker
|
||||||
include PipelineBackgroundQueue
|
include PipelineBackgroundQueue
|
||||||
|
|
||||||
|
idempotent!
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def perform(chunk_id)
|
def perform(chunk_id)
|
||||||
::Ci::BuildTraceChunk.find_by(id: chunk_id).try do |chunk|
|
::Ci::BuildTraceChunk.find_by(id: chunk_id).try do |chunk|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Optimise index on audit events for CSV export
|
||||||
|
merge_request: 41266
|
||||||
|
author:
|
||||||
|
type: added
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Prevent MRs to be dropped from Merge Trains for open discussions
|
||||||
|
merge_request: 39957
|
||||||
|
author:
|
||||||
|
type: changed
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: WebAuthn support (behind feature flag)
|
||||||
|
merge_request: 26692
|
||||||
|
author: Jan Beckmann
|
||||||
|
type: added
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Replace bootstrap alerts in ee/app/views/groups/push_rules/edit.html.haml
|
||||||
|
merge_request: 41069
|
||||||
|
author: Jacopo Beschi @jacopo-beschi
|
||||||
|
type: changed
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Exposes Incident's severity via GraphQL
|
||||||
|
merge_request: 40945
|
||||||
|
author:
|
||||||
|
type: added
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Correctly preserve LFS objects in design or wiki repositories
|
||||||
|
merge_request: 41352
|
||||||
|
author:
|
||||||
|
type: fixed
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add link to compare changes intoduced by a git submodule update
|
||||||
|
merge_request: 37740
|
||||||
|
author: Daniel Seemer @Phaiax
|
||||||
|
type: added
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Replace v-html to v-safe-html directive
|
||||||
|
merge_request: 41305
|
||||||
|
author: Kazuya Kojima
|
||||||
|
type: other
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
name: merge_request_reviewers
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40488
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/245190
|
||||||
|
group: group::source code
|
||||||
|
type: development
|
||||||
|
default_enabled: false
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
WebAuthn.configure do |config|
|
||||||
|
# This value needs to match `window.location.origin` evaluated by
|
||||||
|
# the User Agent during registration and authentication ceremonies.
|
||||||
|
config.origin = Settings.gitlab['base_url']
|
||||||
|
|
||||||
|
# Relying Party name for display purposes
|
||||||
|
# config.rp_name = "Example Inc."
|
||||||
|
|
||||||
|
# Optionally configure a client timeout hint, in milliseconds.
|
||||||
|
# This hint specifies how long the browser should wait for any
|
||||||
|
# interaction with the user.
|
||||||
|
# This hint may be overridden by the browser.
|
||||||
|
# https://www.w3.org/TR/webauthn/#dom-publickeycredentialcreationoptions-timeout
|
||||||
|
# config.credential_options_timeout = 120_000
|
||||||
|
|
||||||
|
# You can optionally specify a different Relying Party ID
|
||||||
|
# (https://www.w3.org/TR/webauthn/#relying-party-identifier)
|
||||||
|
# if it differs from the default one.
|
||||||
|
#
|
||||||
|
# In this case the default would be "auth.example.com", but you can set it to
|
||||||
|
# the suffix "example.com"
|
||||||
|
#
|
||||||
|
# config.rp_id = "example.com"
|
||||||
|
|
||||||
|
# Configure preferred binary-to-text encoding scheme. This should match the encoding scheme
|
||||||
|
# used in your client-side (user agent) code before sending the credential to the server.
|
||||||
|
# Supported values: `:base64url` (default), `:base64` or `false` to disable all encoding.
|
||||||
|
#
|
||||||
|
config.encoding = :base64
|
||||||
|
|
||||||
|
# Possible values: "ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512", "RS1"
|
||||||
|
# Default: ["ES256", "PS256", "RS256"]
|
||||||
|
#
|
||||||
|
# config.algorithms << "ES384"
|
||||||
|
end
|
||||||
|
|
@ -63,9 +63,11 @@ resource :profile, only: [:show, :update] do
|
||||||
post :create_u2f
|
post :create_u2f
|
||||||
post :codes
|
post :codes
|
||||||
patch :skip
|
patch :skip
|
||||||
|
post :create_webauthn
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :u2f_registrations, only: [:destroy]
|
resources :u2f_registrations, only: [:destroy]
|
||||||
|
resources :webauthn_registrations, only: [:destroy]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddCreatedAtIndexToAuditEvents < ActiveRecord::Migration[6.0]
|
||||||
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
INDEX_NAME = 'idx_audit_events_on_entity_id_desc_author_id_created_at'
|
||||||
|
OLD_INDEX_NAME = 'index_audit_events_on_entity_id_entity_type_id_desc_author_id'
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_concurrent_index(:audit_events, [:entity_id, :entity_type, :id, :author_id, :created_at], order: { id: :desc }, name: INDEX_NAME)
|
||||||
|
remove_concurrent_index_by_name(:audit_events, OLD_INDEX_NAME)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
add_concurrent_index(:audit_events, [:entity_id, :entity_type, :id, :author_id], order: { id: :desc }, name: OLD_INDEX_NAME)
|
||||||
|
remove_concurrent_index_by_name(:audit_events, INDEX_NAME)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
5c065dc7905fd1292e270d2248810d71fa71d6b6996e9d60c463a7eb36042881
|
||||||
|
|
@ -19075,6 +19075,8 @@ CREATE UNIQUE INDEX epic_user_mentions_on_epic_id_and_note_id_index ON public.ep
|
||||||
|
|
||||||
CREATE UNIQUE INDEX epic_user_mentions_on_epic_id_index ON public.epic_user_mentions USING btree (epic_id) WHERE (note_id IS NULL);
|
CREATE UNIQUE INDEX epic_user_mentions_on_epic_id_index ON public.epic_user_mentions USING btree (epic_id) WHERE (note_id IS NULL);
|
||||||
|
|
||||||
|
CREATE INDEX idx_audit_events_on_entity_id_desc_author_id_created_at ON public.audit_events USING btree (entity_id, entity_type, id DESC, author_id, created_at);
|
||||||
|
|
||||||
CREATE INDEX idx_ci_pipelines_artifacts_locked ON public.ci_pipelines USING btree (ci_ref_id, id) WHERE (locked = 1);
|
CREATE INDEX idx_ci_pipelines_artifacts_locked ON public.ci_pipelines USING btree (ci_ref_id, id) WHERE (locked = 1);
|
||||||
|
|
||||||
CREATE INDEX idx_container_scanning_findings ON public.vulnerability_occurrences USING btree (id) WHERE (report_type = 2);
|
CREATE INDEX idx_container_scanning_findings ON public.vulnerability_occurrences USING btree (id) WHERE (report_type = 2);
|
||||||
|
|
@ -19267,8 +19269,6 @@ CREATE INDEX index_approvers_on_user_id ON public.approvers USING btree (user_id
|
||||||
|
|
||||||
CREATE UNIQUE INDEX index_atlassian_identities_on_extern_uid ON public.atlassian_identities USING btree (extern_uid);
|
CREATE UNIQUE INDEX index_atlassian_identities_on_extern_uid ON public.atlassian_identities USING btree (extern_uid);
|
||||||
|
|
||||||
CREATE INDEX index_audit_events_on_entity_id_entity_type_id_desc_author_id ON public.audit_events USING btree (entity_id, entity_type, id DESC, author_id);
|
|
||||||
|
|
||||||
CREATE INDEX index_award_emoji_on_awardable_type_and_awardable_id ON public.award_emoji USING btree (awardable_type, awardable_id);
|
CREATE INDEX index_award_emoji_on_awardable_type_and_awardable_id ON public.award_emoji USING btree (awardable_type, awardable_id);
|
||||||
|
|
||||||
CREATE INDEX index_award_emoji_on_user_id_and_name ON public.award_emoji USING btree (user_id, name);
|
CREATE INDEX index_award_emoji_on_user_id_and_name ON public.award_emoji USING btree (user_id, name);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ See [Geo current limitations](../replication/index.md#current-limitations) for m
|
||||||
|
|
||||||
CAUTION: **Warning:**
|
CAUTION: **Warning:**
|
||||||
Disaster recovery for multi-secondary configurations is in **Alpha**.
|
Disaster recovery for multi-secondary configurations is in **Alpha**.
|
||||||
For the latest updates, check the multi-secondary [Disaster Recovery epic](https://gitlab.com/groups/gitlab-org/-/epics/65).
|
For the latest updates, check the [Disaster Recovery epic for complete maturity](https://gitlab.com/groups/gitlab-org/-/epics/590).
|
||||||
|
Multi-secondary configurations require the complete re-synchronization and re-configuration of all non-promoted secondaries and
|
||||||
|
will cause downtime.
|
||||||
|
|
||||||
## Promoting a **secondary** Geo node in single-secondary configurations
|
## Promoting a **secondary** Geo node in single-secondary configurations
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -588,8 +588,9 @@ database encryption. Proceed with caution.
|
||||||
1. On the **GitLab server**, make the following changes to `/etc/gitlab/gitlab.rb`:
|
1. On the **GitLab server**, make the following changes to `/etc/gitlab/gitlab.rb`:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
gitlab_pages['enable'] = false
|
|
||||||
pages_external_url "http://<pages_server_URL>"
|
pages_external_url "http://<pages_server_URL>"
|
||||||
|
gitlab_pages['enable'] = false
|
||||||
|
gitlab_rails['pages_enabled']=false
|
||||||
gitlab_rails['pages_path'] = "/mnt/pages"
|
gitlab_rails['pages_path'] = "/mnt/pages"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ If you use a cloud-managed service, or provide your own PostgreSQL instance:
|
||||||
|
|
||||||
1. Set up PostgreSQL according to the
|
1. Set up PostgreSQL according to the
|
||||||
[database requirements document](../../install/requirements.md#database).
|
[database requirements document](../../install/requirements.md#database).
|
||||||
1. Set up a `gitlab` username with a password of your choice. The `gitlab` user
|
1. Set up a `gitlab` user with a password of your choice, create the `gitlabhq_production` database, and make the user an owner of the database. You can see an example of this setup in the [installation from source documentation](../../install/installation.md#6-database).
|
||||||
needs privileges to create the `gitlabhq_production` database.
|
|
||||||
1. If you are using a cloud-managed service, you may need to grant additional
|
1. If you are using a cloud-managed service, you may need to grant additional
|
||||||
roles to your `gitlab` user:
|
roles to your `gitlab` user:
|
||||||
- Amazon RDS requires the [`rds_superuser`](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Appendix.PostgreSQL.CommonDBATasks.html#Appendix.PostgreSQL.CommonDBATasks.Roles) role.
|
- Amazon RDS requires the [`rds_superuser`](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Appendix.PostgreSQL.CommonDBATasks.html#Appendix.PostgreSQL.CommonDBATasks.Roles) role.
|
||||||
|
|
|
||||||
|
|
@ -5679,6 +5679,11 @@ type EpicIssue implements Noteable {
|
||||||
"""
|
"""
|
||||||
relativePosition: Int
|
relativePosition: Int
|
||||||
|
|
||||||
|
"""
|
||||||
|
Severity level of the incident
|
||||||
|
"""
|
||||||
|
severity: IssuableSeverity
|
||||||
|
|
||||||
"""
|
"""
|
||||||
State of the issue
|
State of the issue
|
||||||
"""
|
"""
|
||||||
|
|
@ -6927,6 +6932,11 @@ type Group {
|
||||||
"""
|
"""
|
||||||
severity: [VulnerabilitySeverity!]
|
severity: [VulnerabilitySeverity!]
|
||||||
|
|
||||||
|
"""
|
||||||
|
List vulnerabilities by sort order
|
||||||
|
"""
|
||||||
|
sort: VulnerabilitySort = severity_desc
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Filter vulnerabilities by state
|
Filter vulnerabilities by state
|
||||||
"""
|
"""
|
||||||
|
|
@ -7263,6 +7273,36 @@ type InstanceSecurityDashboard {
|
||||||
): VulnerabilitySeveritiesCount
|
): VulnerabilitySeveritiesCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Incident severity
|
||||||
|
"""
|
||||||
|
enum IssuableSeverity {
|
||||||
|
"""
|
||||||
|
Critical severity
|
||||||
|
"""
|
||||||
|
CRITICAL
|
||||||
|
|
||||||
|
"""
|
||||||
|
High severity
|
||||||
|
"""
|
||||||
|
HIGH
|
||||||
|
|
||||||
|
"""
|
||||||
|
Low severity
|
||||||
|
"""
|
||||||
|
LOW
|
||||||
|
|
||||||
|
"""
|
||||||
|
Medium severity
|
||||||
|
"""
|
||||||
|
MEDIUM
|
||||||
|
|
||||||
|
"""
|
||||||
|
Unknown severity
|
||||||
|
"""
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
State of a GitLab issue or merge request
|
State of a GitLab issue or merge request
|
||||||
"""
|
"""
|
||||||
|
|
@ -7509,6 +7549,11 @@ type Issue implements Noteable {
|
||||||
"""
|
"""
|
||||||
relativePosition: Int
|
relativePosition: Int
|
||||||
|
|
||||||
|
"""
|
||||||
|
Severity level of the incident
|
||||||
|
"""
|
||||||
|
severity: IssuableSeverity
|
||||||
|
|
||||||
"""
|
"""
|
||||||
State of the issue
|
State of the issue
|
||||||
"""
|
"""
|
||||||
|
|
@ -12689,6 +12734,11 @@ type Project {
|
||||||
"""
|
"""
|
||||||
severity: [VulnerabilitySeverity!]
|
severity: [VulnerabilitySeverity!]
|
||||||
|
|
||||||
|
"""
|
||||||
|
List vulnerabilities by sort order
|
||||||
|
"""
|
||||||
|
sort: VulnerabilitySort = severity_desc
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Filter vulnerabilities by state
|
Filter vulnerabilities by state
|
||||||
"""
|
"""
|
||||||
|
|
@ -13451,6 +13501,11 @@ type Query {
|
||||||
"""
|
"""
|
||||||
severity: [VulnerabilitySeverity!]
|
severity: [VulnerabilitySeverity!]
|
||||||
|
|
||||||
|
"""
|
||||||
|
List vulnerabilities by sort order
|
||||||
|
"""
|
||||||
|
sort: VulnerabilitySort = severity_desc
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Filter vulnerabilities by state
|
Filter vulnerabilities by state
|
||||||
"""
|
"""
|
||||||
|
|
@ -18389,6 +18444,21 @@ enum VulnerabilitySeverity {
|
||||||
UNKNOWN
|
UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Vulnerability sort values
|
||||||
|
"""
|
||||||
|
enum VulnerabilitySort {
|
||||||
|
"""
|
||||||
|
Severity in ascending order
|
||||||
|
"""
|
||||||
|
severity_asc
|
||||||
|
|
||||||
|
"""
|
||||||
|
Severity in descending order
|
||||||
|
"""
|
||||||
|
severity_desc
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
The state of the vulnerability.
|
The state of the vulnerability.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -15844,6 +15844,20 @@
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"deprecationReason": null
|
"deprecationReason": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "severity",
|
||||||
|
"description": "Severity level of the incident",
|
||||||
|
"args": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"type": {
|
||||||
|
"kind": "ENUM",
|
||||||
|
"name": "IssuableSeverity",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "state",
|
"name": "state",
|
||||||
"description": "State of the issue",
|
"description": "State of the issue",
|
||||||
|
|
@ -19047,6 +19061,16 @@
|
||||||
},
|
},
|
||||||
"defaultValue": null
|
"defaultValue": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "sort",
|
||||||
|
"description": "List vulnerabilities by sort order",
|
||||||
|
"type": {
|
||||||
|
"kind": "ENUM",
|
||||||
|
"name": "VulnerabilitySort",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"defaultValue": "severity_desc"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "after",
|
"name": "after",
|
||||||
"description": "Returns the elements in the list that come after the specified cursor.",
|
"description": "Returns the elements in the list that come after the specified cursor.",
|
||||||
|
|
@ -20075,6 +20099,47 @@
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "ENUM",
|
||||||
|
"name": "IssuableSeverity",
|
||||||
|
"description": "Incident severity",
|
||||||
|
"fields": null,
|
||||||
|
"inputFields": null,
|
||||||
|
"interfaces": null,
|
||||||
|
"enumValues": [
|
||||||
|
{
|
||||||
|
"name": "UNKNOWN",
|
||||||
|
"description": "Unknown severity",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LOW",
|
||||||
|
"description": "Low severity",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MEDIUM",
|
||||||
|
"description": "Medium severity",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "HIGH",
|
||||||
|
"description": "High severity",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CRITICAL",
|
||||||
|
"description": "Critical severity",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"possibleTypes": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "ENUM",
|
"kind": "ENUM",
|
||||||
"name": "IssuableState",
|
"name": "IssuableState",
|
||||||
|
|
@ -20727,6 +20792,20 @@
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"deprecationReason": null
|
"deprecationReason": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "severity",
|
||||||
|
"description": "Severity level of the incident",
|
||||||
|
"args": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"type": {
|
||||||
|
"kind": "ENUM",
|
||||||
|
"name": "IssuableSeverity",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "state",
|
"name": "state",
|
||||||
"description": "State of the issue",
|
"description": "State of the issue",
|
||||||
|
|
@ -37132,6 +37211,16 @@
|
||||||
},
|
},
|
||||||
"defaultValue": null
|
"defaultValue": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "sort",
|
||||||
|
"description": "List vulnerabilities by sort order",
|
||||||
|
"type": {
|
||||||
|
"kind": "ENUM",
|
||||||
|
"name": "VulnerabilitySort",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"defaultValue": "severity_desc"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "after",
|
"name": "after",
|
||||||
"description": "Returns the elements in the list that come after the specified cursor.",
|
"description": "Returns the elements in the list that come after the specified cursor.",
|
||||||
|
|
@ -39461,6 +39550,16 @@
|
||||||
},
|
},
|
||||||
"defaultValue": null
|
"defaultValue": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "sort",
|
||||||
|
"description": "List vulnerabilities by sort order",
|
||||||
|
"type": {
|
||||||
|
"kind": "ENUM",
|
||||||
|
"name": "VulnerabilitySort",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"defaultValue": "severity_desc"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "after",
|
"name": "after",
|
||||||
"description": "Returns the elements in the list that come after the specified cursor.",
|
"description": "Returns the elements in the list that come after the specified cursor.",
|
||||||
|
|
@ -54034,6 +54133,29 @@
|
||||||
],
|
],
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "ENUM",
|
||||||
|
"name": "VulnerabilitySort",
|
||||||
|
"description": "Vulnerability sort values",
|
||||||
|
"fields": null,
|
||||||
|
"inputFields": null,
|
||||||
|
"interfaces": null,
|
||||||
|
"enumValues": [
|
||||||
|
{
|
||||||
|
"name": "severity_desc",
|
||||||
|
"description": "Severity in descending order",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "severity_asc",
|
||||||
|
"description": "Severity in ascending order",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"possibleTypes": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "ENUM",
|
"kind": "ENUM",
|
||||||
"name": "VulnerabilityState",
|
"name": "VulnerabilityState",
|
||||||
|
|
|
||||||
|
|
@ -950,6 +950,7 @@ Relationship between an epic and an issue
|
||||||
| `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
|
| `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
|
||||||
| `relationPath` | String | URI path of the epic-issue relation |
|
| `relationPath` | String | URI path of the epic-issue relation |
|
||||||
| `relativePosition` | Int | Relative position of the issue (used for positioning in epic tree and issue boards) |
|
| `relativePosition` | Int | Relative position of the issue (used for positioning in epic tree and issue boards) |
|
||||||
|
| `severity` | IssuableSeverity | Severity level of the incident |
|
||||||
| `state` | IssueState! | State of the issue |
|
| `state` | IssueState! | State of the issue |
|
||||||
| `statusPagePublishedIncident` | Boolean | Indicates whether an issue is published to the status page |
|
| `statusPagePublishedIncident` | Boolean | Indicates whether an issue is published to the status page |
|
||||||
| `subscribed` | Boolean! | Indicates the currently logged in user is subscribed to the issue |
|
| `subscribed` | Boolean! | Indicates the currently logged in user is subscribed to the issue |
|
||||||
|
|
@ -1123,6 +1124,7 @@ Represents a Group Membership
|
||||||
| `milestone` | Milestone | Milestone of the issue |
|
| `milestone` | Milestone | Milestone of the issue |
|
||||||
| `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
|
| `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
|
||||||
| `relativePosition` | Int | Relative position of the issue (used for positioning in epic tree and issue boards) |
|
| `relativePosition` | Int | Relative position of the issue (used for positioning in epic tree and issue boards) |
|
||||||
|
| `severity` | IssuableSeverity | Severity level of the incident |
|
||||||
| `state` | IssueState! | State of the issue |
|
| `state` | IssueState! | State of the issue |
|
||||||
| `statusPagePublishedIncident` | Boolean | Indicates whether an issue is published to the status page |
|
| `statusPagePublishedIncident` | Boolean | Indicates whether an issue is published to the status page |
|
||||||
| `subscribed` | Boolean! | Indicates the currently logged in user is subscribed to the issue |
|
| `subscribed` | Boolean! | Indicates the currently logged in user is subscribed to the issue |
|
||||||
|
|
|
||||||
|
|
@ -680,7 +680,7 @@ Example response:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
|
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) can also see
|
||||||
the `weight` parameter:
|
the `weight` parameter:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
@ -692,7 +692,7 @@ the `weight` parameter:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Users on GitLab [Ultimate](https://about.gitlab.com/pricing/) will additionally see
|
Users on GitLab [Ultimate](https://about.gitlab.com/pricing/) can also see
|
||||||
the `epic` property:
|
the `epic` property:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
|
@ -712,171 +712,20 @@ the `epic` property:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
|
NOTE: **Note:**
|
||||||
|
The `assignee` column is deprecated. We now show it as a single-sized array `assignees` to conform
|
||||||
|
to the GitLab EE API.
|
||||||
|
|
||||||
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17042). This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
|
NOTE: **Note:**
|
||||||
|
The `closed_by` attribute was [introduced in GitLab 10.6](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17042).
|
||||||
|
This value is only present for issues closed after GitLab 10.6 and if the user account
|
||||||
|
that closed the issue still exists.
|
||||||
|
|
||||||
**Note**: The `epic_iid` attribute is deprecated and [will be removed in version 5](https://gitlab.com/gitlab-org/gitlab/-/issues/35157).
|
NOTE: **Note:**
|
||||||
|
The `epic_iid` attribute is deprecated, and [will be removed in version 5](https://gitlab.com/gitlab-org/gitlab/-/issues/35157).
|
||||||
Please use `iid` of the `epic` attribute instead.
|
Please use `iid` of the `epic` attribute instead.
|
||||||
|
|
||||||
## Single Issue
|
## Single project issue
|
||||||
|
|
||||||
Only for administrators. Get a single issue.
|
|
||||||
|
|
||||||
The preferred way to do this is by using [personal access tokens](../user/profile/personal_access_tokens.md).
|
|
||||||
|
|
||||||
```plaintext
|
|
||||||
GET /issues/:id
|
|
||||||
```
|
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
|
||||||
|-------------|---------|----------|--------------------------------------|
|
|
||||||
| `id` | integer | yes | The ID of the issue |
|
|
||||||
|
|
||||||
```shell
|
|
||||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/issues/41"
|
|
||||||
```
|
|
||||||
|
|
||||||
Example response:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id" : 1,
|
|
||||||
"milestone" : {
|
|
||||||
"due_date" : null,
|
|
||||||
"project_id" : 4,
|
|
||||||
"state" : "closed",
|
|
||||||
"description" : "Rerum est voluptatem provident consequuntur molestias similique ipsum dolor.",
|
|
||||||
"iid" : 3,
|
|
||||||
"id" : 11,
|
|
||||||
"title" : "v3.0",
|
|
||||||
"created_at" : "2016-01-04T15:31:39.788Z",
|
|
||||||
"updated_at" : "2016-01-04T15:31:39.788Z",
|
|
||||||
"closed_at" : "2016-01-05T15:31:46.176Z"
|
|
||||||
},
|
|
||||||
"author" : {
|
|
||||||
"state" : "active",
|
|
||||||
"web_url" : "https://gitlab.example.com/root",
|
|
||||||
"avatar_url" : null,
|
|
||||||
"username" : "root",
|
|
||||||
"id" : 1,
|
|
||||||
"name" : "Administrator"
|
|
||||||
},
|
|
||||||
"description" : "Omnis vero earum sunt corporis dolor et placeat.",
|
|
||||||
"state" : "closed",
|
|
||||||
"iid" : 1,
|
|
||||||
"assignees" : [{
|
|
||||||
"avatar_url" : null,
|
|
||||||
"web_url" : "https://gitlab.example.com/lennie",
|
|
||||||
"state" : "active",
|
|
||||||
"username" : "lennie",
|
|
||||||
"id" : 9,
|
|
||||||
"name" : "Dr. Luella Kovacek"
|
|
||||||
}],
|
|
||||||
"assignee" : {
|
|
||||||
"avatar_url" : null,
|
|
||||||
"web_url" : "https://gitlab.example.com/lennie",
|
|
||||||
"state" : "active",
|
|
||||||
"username" : "lennie",
|
|
||||||
"id" : 9,
|
|
||||||
"name" : "Dr. Luella Kovacek"
|
|
||||||
},
|
|
||||||
"labels" : [],
|
|
||||||
"upvotes": 4,
|
|
||||||
"downvotes": 0,
|
|
||||||
"merge_requests_count": 0,
|
|
||||||
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
|
|
||||||
"updated_at" : "2016-01-04T15:31:46.176Z",
|
|
||||||
"created_at" : "2016-01-04T15:31:46.176Z",
|
|
||||||
"closed_at" : null,
|
|
||||||
"closed_by" : null,
|
|
||||||
"subscribed": false,
|
|
||||||
"user_notes_count": 1,
|
|
||||||
"due_date": null,
|
|
||||||
"web_url": "http://example.com/my-group/my-project/issues/1",
|
|
||||||
"references": {
|
|
||||||
"short": "#1",
|
|
||||||
"relative": "#1",
|
|
||||||
"full": "my-group/my-project#1"
|
|
||||||
},
|
|
||||||
"time_stats": {
|
|
||||||
"time_estimate": 0,
|
|
||||||
"total_time_spent": 0,
|
|
||||||
"human_time_estimate": null,
|
|
||||||
"human_total_time_spent": null
|
|
||||||
},
|
|
||||||
"confidential": false,
|
|
||||||
"discussion_locked": false,
|
|
||||||
"_links": {
|
|
||||||
"self": "http://example.com/api/v4/projects/1/issues/2",
|
|
||||||
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
|
|
||||||
"award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
|
|
||||||
"project": "http://example.com/api/v4/projects/1"
|
|
||||||
},
|
|
||||||
"task_completion_status":{
|
|
||||||
"count":0,
|
|
||||||
"completed_count":0
|
|
||||||
},
|
|
||||||
"weight": null,
|
|
||||||
"has_tasks": false,
|
|
||||||
"_links": {
|
|
||||||
"self": "http://gitlab.dummy:3000/api/v4/projects/1/issues/1",
|
|
||||||
"notes": "http://gitlab.dummy:3000/api/v4/projects/1/issues/1/notes",
|
|
||||||
"award_emoji": "http://gitlab.dummy:3000/api/v4/projects/1/issues/1/award_emoji",
|
|
||||||
"project": "http://gitlab.dummy:3000/api/v4/projects/1"
|
|
||||||
},
|
|
||||||
"references": {
|
|
||||||
"short": "#1",
|
|
||||||
"relative": "#1",
|
|
||||||
"full": "gitlab-org/gitlab-test#1"
|
|
||||||
},
|
|
||||||
"subscribed": true,
|
|
||||||
"moved_to_id": null,
|
|
||||||
"epic_iid": null,
|
|
||||||
"epic": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
|
|
||||||
the `weight` parameter:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"project_id" : 4,
|
|
||||||
"description" : "Omnis vero earum sunt corporis dolor et placeat.",
|
|
||||||
"weight": null,
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Users on GitLab [Ultimate](https://about.gitlab.com/pricing/) will additionally see
|
|
||||||
the `epic` property:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
"project_id" : 4,
|
|
||||||
"description" : "Omnis vero earum sunt corporis dolor et placeat.",
|
|
||||||
"epic": {
|
|
||||||
"epic_iid" : 5, //deprecated, use `iid` of the `epic` attribute
|
|
||||||
"epic": {
|
|
||||||
"id" : 42,
|
|
||||||
"iid" : 5,
|
|
||||||
"title": "My epic epic",
|
|
||||||
"url" : "/groups/h5bp/-/epics/5",
|
|
||||||
"group_id": 8
|
|
||||||
},
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
|
|
||||||
|
|
||||||
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17042). This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
|
|
||||||
|
|
||||||
**Note**: The `epic_iid` attribute is deprecated and [will be removed in version 5](https://gitlab.com/gitlab-org/gitlab/-/issues/35157).
|
|
||||||
Please use `iid` of the `epic` attribute instead.
|
|
||||||
|
|
||||||
## Single Project Issue
|
|
||||||
|
|
||||||
Get a single project issue.
|
Get a single project issue.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -288,6 +288,29 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
1. Track event in API using `increment_unique_values(event_name, values)` helper method.
|
||||||
|
|
||||||
|
In order to be able to track the event, Usage Ping must be enabled and the event feature `usage_data_<event_name>` must be enabled.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
- `event_name`: event name.
|
||||||
|
- `values`: values counted, one value or array of values.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
get ':id/registry/repositories' do
|
||||||
|
repositories = ContainerRepositoriesFinder.new(
|
||||||
|
user: current_user, subject: user_group
|
||||||
|
).execute
|
||||||
|
|
||||||
|
increment_unique_values('i_list_repositories', current_user.id)
|
||||||
|
|
||||||
|
present paginate(repositories), with: Entities::ContainerRegistry::Repository, tags: params[:tags], tags_count: params[:tags_count]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
1. Track event using base module `Gitlab::UsageDataCounters::HLLRedisCounter.track_event(entity_id, event_name)`.
|
1. Track event using base module `Gitlab::UsageDataCounters::HLLRedisCounter.track_event(entity_id, event_name)`.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ The DevOps Report gives you an overview of your entire instance's adoption of
|
||||||
[Concurrent DevOps](https://about.gitlab.com/topics/concurrent-devops/)
|
[Concurrent DevOps](https://about.gitlab.com/topics/concurrent-devops/)
|
||||||
from planning to monitoring.
|
from planning to monitoring.
|
||||||
|
|
||||||
This displays the usage of these GitLab features over
|
## DevOps Score
|
||||||
|
|
||||||
|
DevOps Score displays the usage of GitLab's major features on your instance over
|
||||||
the last 30 days, averaged over the number of active users in that time period. It also
|
the last 30 days, averaged over the number of active users in that time period. It also
|
||||||
provides a Lead score per feature, which is calculated based on GitLab's analysis
|
provides a Lead score per feature, which is calculated based on GitLab's analysis
|
||||||
of top-performing instances based on [usage ping data](../settings/usage_statistics.md#usage-ping-core-only) that GitLab has
|
of top-performing instances based on [usage ping data](../settings/usage_statistics.md#usage-ping-core-only) that GitLab has
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
|
|
@ -309,15 +309,29 @@ rating.
|
||||||
|
|
||||||
### Enabling Security Approvals within a project
|
### Enabling Security Approvals within a project
|
||||||
|
|
||||||
To enable Security Approvals, a [project approval rule](../project/merge_requests/merge_request_approvals.md#adding--editing-a-default-approval-rule)
|
To enable the `Vulnerability-Check` or `License-Check` Security Approvals, a [project approval rule](../project/merge_requests/merge_request_approvals.md#adding--editing-a-default-approval-rule)
|
||||||
must be created with the case-sensitive name `Vulnerability-Check`. This approval group must be set
|
must be created. A [security scanner job](#security-scanning-tools) must be enabled for
|
||||||
with the number of approvals required greater than zero. You must have Maintainer or Owner [permissions](../permissions.md#project-members-permissions) to manage approval rules.
|
`Vulnerability-Check`, and a [license scanning](../compliance/license_compliance/index.md#configuration)
|
||||||
|
job must be enabled for `License-Check`. When the proper jobs aren't configured, the following
|
||||||
|
appears:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
If at least one security scanner is enabled, you will be able to enable the `Vulnerability-Check` approval rule. If a license scanning job is enabled, you will be able to enable the `License-Check` rule.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
For this approval group, you must set the number of approvals required to greater than zero. You
|
||||||
|
must have Maintainer or Owner [permissions](../permissions.md#project-members-permissions)
|
||||||
|
to manage approval rules.
|
||||||
|
|
||||||
|
Follow these steps to enable `Vulnerability-Check`:
|
||||||
|
|
||||||
1. Navigate to your project's **Settings > General** and expand **Merge request approvals**.
|
1. Navigate to your project's **Settings > General** and expand **Merge request approvals**.
|
||||||
1. Click **Add approval rule**, or **Edit**.
|
1. Click **Enable**, or **Edit**.
|
||||||
- Add or change the **Rule name** to `Vulnerability-Check` (case sensitive).
|
1. Add or change the **Rule name** to `Vulnerability-Check` (case sensitive).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Once this group is added to your project, the approval rule is enabled for all merge requests.
|
Once this group is added to your project, the approval rule is enabled for all merge requests.
|
||||||
|
|
||||||
|
|
@ -334,32 +348,14 @@ An approval is optional when the security report:
|
||||||
- Contains no new vulnerabilities when compared to the target branch.
|
- Contains no new vulnerabilities when compared to the target branch.
|
||||||
- Contains only new vulnerabilities of `low` or `medium` severity.
|
- Contains only new vulnerabilities of `low` or `medium` severity.
|
||||||
|
|
||||||
## Enabling License Approvals within a project
|
### Enabling License Approvals within a project
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13067) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.3.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13067) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.3.
|
||||||
|
|
||||||
`License-Check` is an approval rule you can enable to allow an individual or group to approve a
|
`License-Check` is a [security approval rule](#enabling-security-approvals-within-a-project)
|
||||||
merge request that contains a `denied` license.
|
you can enable to allow an individual or group to approve a merge request that contains a `denied`
|
||||||
|
license. For instructions on enabling this rule, see
|
||||||
You can enable `License-Check` one of two ways:
|
[Enabling license approvals within a project](../compliance/license_compliance/index.md#enabling-license-approvals-within-a-project).
|
||||||
|
|
||||||
- Create a [project approval rule](../project/merge_requests/merge_request_approvals.md#multiple-approval-rules-premium)
|
|
||||||
with the case-sensitive name `License-Check`.
|
|
||||||
- Create an approval group in the [project policies section for License Compliance](../compliance/license_compliance/index.md#policies).
|
|
||||||
You must set this approval group's number of approvals required to greater than zero. Once you
|
|
||||||
enable this group in your project, the approval rule is enabled for all merge requests.
|
|
||||||
|
|
||||||
Any code changes cause the approvals required to reset.
|
|
||||||
|
|
||||||
An approval is required when a license report:
|
|
||||||
|
|
||||||
- Contains a dependency that includes a software license that is `denied`.
|
|
||||||
- Is not generated during pipeline execution.
|
|
||||||
|
|
||||||
An approval is optional when a license report:
|
|
||||||
|
|
||||||
- Contains no software license violations.
|
|
||||||
- Contains only new licenses that are `allowed` or unknown.
|
|
||||||
|
|
||||||
## Working in an offline environment
|
## Working in an offline environment
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
|
|
@ -724,17 +724,21 @@ Developers of the project can view the policies configured in a project.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Enabling License Approvals within a project
|
## Enabling License Approvals within a project
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13067) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.3.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13067) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.3.
|
||||||
|
|
||||||
`License-Check` is an approval rule you can enable to allow an approver, individual, or group to
|
`License-Check` is a [security approval](../../application_security/index.md#enabling-security-approvals-within-a-project) rule you can enable to allow an individual or group to approve a
|
||||||
approve a merge request that contains a `denied` license.
|
merge request that contains a `denied` license.
|
||||||
|
|
||||||
You can enable `License-Check` one of two ways:
|
You can enable `License-Check` one of two ways:
|
||||||
|
|
||||||
- Create a [project approval rule](../../project/merge_requests/merge_request_approvals.md#multiple-approval-rules-premium)
|
1. Navigate to your project's **Settings > General** and expand **Merge request approvals**.
|
||||||
with the case-sensitive name `License-Check`.
|
1. Click **Enable** or **Edit**.
|
||||||
|
1. Add or change the **Rule name** to `License-Check` (case sensitive).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
- Create an approval group in the [project policies section for License Compliance](#policies).
|
- Create an approval group in the [project policies section for License Compliance](#policies).
|
||||||
You must set this approval group's number of approvals required to greater than zero. Once you
|
You must set this approval group's number of approvals required to greater than zero. Once you
|
||||||
enable this group in your project, the approval rule is enabled for all merge requests.
|
enable this group in your project, the approval rule is enabled for all merge requests.
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,8 @@ module HamlLint
|
||||||
File.open(path_to_file).any? do |line|
|
File.open(path_to_file).any? do |line|
|
||||||
result = line.match(MARKDOWN_HEADER)
|
result = line.match(MARKDOWN_HEADER)
|
||||||
|
|
||||||
string_to_anchor(result[:header]) == anchor if result
|
# TODO:Do an exact match for anchors (Follow-up https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39850)
|
||||||
|
anchor.start_with?(string_to_anchor(result[:header])) if result
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -537,6 +537,20 @@ module API
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param event_name [String] the event name
|
||||||
|
# @param values [Array|String] the values counted
|
||||||
|
def increment_unique_values(event_name, values)
|
||||||
|
return unless values.present?
|
||||||
|
|
||||||
|
feature_name = "usage_data_#{event_name}"
|
||||||
|
return unless Feature.enabled?(feature_name)
|
||||||
|
return unless Gitlab::CurrentSettings.usage_ping_enabled?
|
||||||
|
|
||||||
|
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(values, event_name)
|
||||||
|
rescue => error
|
||||||
|
Gitlab::AppLogger.warn("Redis tracking event failed for event: #{event_name}, message: #{error.message}")
|
||||||
|
end
|
||||||
|
|
||||||
def with_api_params(&block)
|
def with_api_params(&block)
|
||||||
yield({ api: true, request: request })
|
yield({ api: true, request: request })
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ module API
|
||||||
|
|
||||||
entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic
|
entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic
|
||||||
users = users.preload(:identities, :u2f_registrations) if entity == Entities::UserWithAdmin
|
users = users.preload(:identities, :u2f_registrations) if entity == Entities::UserWithAdmin
|
||||||
|
users = users.preload(:identities, :webauthn_registrations) if entity == Entities::UserWithAdmin
|
||||||
users, options = with_custom_attributes(users, { with: entity, current_user: current_user })
|
users, options = with_custom_attributes(users, { with: entity, current_user: current_user })
|
||||||
|
|
||||||
users = users.preload(:user_detail)
|
users = users.preload(:user_detail)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ module Gitlab
|
||||||
private
|
private
|
||||||
|
|
||||||
def remove_orphan_references
|
def remove_orphan_references
|
||||||
invalid_references = project.lfs_objects_projects.where(lfs_object: orphan_objects) # rubocop:disable CodeReuse/ActiveRecord
|
invalid_references = project.lfs_objects_projects.lfs_object_in(orphan_objects)
|
||||||
|
|
||||||
if dry_run
|
if dry_run
|
||||||
log_info("Found invalid references: #{invalid_references.count}")
|
log_info("Found invalid references: #{invalid_references.count}")
|
||||||
|
|
@ -41,26 +41,22 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def lfs_oids_from_repository
|
def orphan_objects
|
||||||
project.repository.gitaly_blob_client.get_all_lfs_pointers.map(&:lfs_oid)
|
# Get these first so racing with a git push can't remove any LFS objects
|
||||||
end
|
oids = project.lfs_objects_oids
|
||||||
|
|
||||||
def orphan_oids
|
repos = [
|
||||||
lfs_oids_from_database - lfs_oids_from_repository
|
project.repository,
|
||||||
end
|
project.design_repository,
|
||||||
|
project.wiki.repository
|
||||||
|
].select(&:exists?)
|
||||||
|
|
||||||
def lfs_oids_from_database
|
repos.flat_map do |repo|
|
||||||
oids = []
|
oids -= repo.gitaly_blob_client.get_all_lfs_pointers.map(&:lfs_oid)
|
||||||
|
|
||||||
project.lfs_objects.each_batch do |relation|
|
|
||||||
oids += relation.pluck(:oid) # rubocop:disable CodeReuse/ActiveRecord
|
|
||||||
end
|
end
|
||||||
|
|
||||||
oids
|
# The remaining OIDs are not used by any repository, so are orphans
|
||||||
end
|
LfsObject.for_oids(oids)
|
||||||
|
|
||||||
def orphan_objects
|
|
||||||
LfsObject.where(oid: orphan_oids) # rubocop:disable CodeReuse/ActiveRecord
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def log_info(msg)
|
def log_info(msg)
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,11 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def web_url
|
def web_url
|
||||||
@submodule_links.first
|
@submodule_links&.web
|
||||||
end
|
end
|
||||||
|
|
||||||
def tree_url
|
def tree_url
|
||||||
@submodule_links.last
|
@submodule_links&.tree
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,18 @@ module Gitlab
|
||||||
class SubmoduleLinks
|
class SubmoduleLinks
|
||||||
include Gitlab::Utils::StrongMemoize
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
|
Urls = Struct.new(:web, :tree, :compare)
|
||||||
|
|
||||||
def initialize(repository)
|
def initialize(repository)
|
||||||
@repository = repository
|
@repository = repository
|
||||||
@cache_store = {}
|
@cache_store = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
def for(submodule, sha)
|
def for(submodule, sha, diff_file = nil)
|
||||||
submodule_url = submodule_url_for(sha, submodule.path)
|
submodule_url = submodule_url_for(sha, submodule.path)
|
||||||
SubmoduleHelper.submodule_links_for_url(submodule.id, submodule_url, repository)
|
old_submodule_id = old_submodule_id(submodule_url, diff_file)
|
||||||
|
urls = SubmoduleHelper.submodule_links_for_url(submodule.id, submodule_url, repository, old_submodule_id)
|
||||||
|
Urls.new(*urls) if urls.any?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
@ -29,5 +33,15 @@ module Gitlab
|
||||||
urls = submodule_urls_for(sha)
|
urls = submodule_urls_for(sha)
|
||||||
urls && urls[path]
|
urls && urls[path]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def old_submodule_id(submodule_url, diff_file)
|
||||||
|
return unless diff_file&.old_blob && diff_file&.old_content_sha
|
||||||
|
|
||||||
|
# if the submodule url has changed from old_sha to sha, a compare link does not make sense
|
||||||
|
#
|
||||||
|
old_submodule_url = submodule_url_for(diff_file.old_content_sha, diff_file.old_blob.path)
|
||||||
|
|
||||||
|
diff_file.old_blob.id if old_submodule_url == submodule_url
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue