Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-07 12:08:27 +00:00
parent 444f662b8d
commit a865379008
160 changed files with 3558 additions and 733 deletions

View File

@ -45,6 +45,7 @@ docs lint:
image: "registry.gitlab.com/gitlab-org/gitlab-docs/lint:vale-2.3.3-markdownlint-0.23.2"
stage: test
needs: []
allow_failure: true
script:
- scripts/lint-doc.sh
# Prepare docs for build

View File

@ -63,6 +63,8 @@ linters:
- "app/views/admin/users/new.html.haml"
- "app/views/admin/users/projects.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/new.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/_hook.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"
- "ee/app/views/admin/_namespace_plan_info.html.haml"
- "ee/app/views/admin/application_settings/_templates.html.haml"

View File

@ -512,3 +512,5 @@ gem 'json_schemer', '~> 0.2.12'
gem 'oj', '~> 3.10.6'
gem 'multi_json', '~> 1.14.1'
gem 'yajl-ruby', '~> 1.4.1', require: 'yajl'
gem 'webauthn', '~> 2.3'

View File

@ -73,6 +73,7 @@ GEM
public_suffix (>= 2.0.2, < 5.0)
aes_key_wrap (1.0.1)
akismet (3.0.0)
android_key_attestation (0.3.0)
apollo_upload_server (2.0.2)
graphql (>= 1.8)
rails (>= 4.2)
@ -93,6 +94,7 @@ GEM
encryptor (~> 3.0.0)
attr_required (1.0.1)
awesome_print (1.8.0)
awrence (1.1.1)
aws-eventstream (1.1.0)
aws-partitions (1.345.0)
aws-sdk-cloudformation (1.41.0)
@ -167,6 +169,7 @@ GEM
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
cbor (0.5.9.6)
character_set (1.4.0)
charlock_holmes (0.7.6)
childprocess (3.0.0)
@ -189,6 +192,9 @@ GEM
contracts (0.11.0)
cork (0.3.0)
colored2 (~> 3.1)
cose (1.0.0)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 0.4.0)
countries (3.0.0)
i18n_data (~> 0.8.0)
sixarm_ruby_unaccent (~> 1.1)
@ -802,6 +808,8 @@ GEM
validate_email
validate_url
webfinger (>= 1.0.1)
openssl (2.2.0)
openssl-signature_algorithm (0.4.0)
opentracing (0.5.0)
optimist (3.0.1)
org-ruby (0.9.12)
@ -1026,6 +1034,8 @@ GEM
rubyzip (2.0.0)
rugged (0.28.4.1)
safe_yaml (1.0.4)
safety_net_attestation (0.4.0)
jwt (~> 2.0)
sanitize (5.2.1)
crass (~> 1.0.2)
nokogiri (>= 1.8.0)
@ -1050,6 +1060,7 @@ GEM
scss_lint (0.56.0)
rake (>= 0.9, < 13)
sass (~> 3.5.3)
securecompare (1.0.0)
seed-fu (2.3.7)
activerecord (>= 3.1)
activesupport (>= 3.1)
@ -1135,6 +1146,9 @@ GEM
parslet (~> 1.8.0)
toml-rb (1.0.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)
htmlentities (~> 4.3.1)
nokogiri (>= 1.7.0, <= 2.0)
@ -1186,6 +1200,16 @@ GEM
vmstat (2.3.0)
warden (1.2.8)
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)
activesupport
httpclient (>= 2.4)
@ -1472,6 +1496,7 @@ DEPENDENCIES
validates_hostname (~> 1.0.10)
version_sorter (~> 2.2.4)
vmstat (~> 2.3.0)
webauthn (~> 2.3)
webmock (~> 3.5.1)
webpack-rails (~> 0.9.10)
wikicloth (= 0.8.1)

View File

@ -1,14 +1,23 @@
import $ from 'jquery';
import initU2F from './u2f';
import initWebauthn from './webauthn';
import U2FRegister from './u2f/register';
import WebAuthnRegister from './webauthn/register';
export const mount2faAuthentication = () => {
// Soon this will conditionally mount a webauthn app (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26692)
initU2F();
if (gon.webauthn) {
initWebauthn();
} else {
initU2F();
}
};
export const mount2faRegistration = () => {
// Soon this will conditionally mount a webauthn app (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26692)
const u2fRegister = new U2FRegister($('#js-register-u2f'), gon.u2f);
u2fRegister.start();
if (gon.webauthn) {
const webauthnRegister = new WebAuthnRegister($('#js-register-token-2fa'), gon.webauthn);
webauthnRegister.start();
} else {
const u2fRegister = new U2FRegister($('#js-register-token-2fa'), gon.u2f);
u2fRegister.start();
}
};

View File

@ -40,7 +40,6 @@ export default class U2FAuthenticate {
this.signRequests = u2fParams.sign_requests.map(request => omit(request, 'challenge'));
this.templates = {
setup: '#js-authenticate-token-2fa-setup',
inProgress: '#js-authenticate-token-2fa-in-progress',
error: '#js-authenticate-token-2fa-error',
authenticated: '#js-authenticate-token-2fa-authenticated',
@ -86,7 +85,7 @@ export default class U2FAuthenticate {
renderError(error) {
this.renderTemplate('error', {
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);
}

View File

@ -1,5 +1,6 @@
import $ from 'jquery';
import { template as lodashTemplate } from 'lodash';
import { __ } from '~/locale';
import importU2FLibrary from './util';
import U2FError from './error';
@ -24,11 +25,10 @@ export default class U2FRegister {
this.signRequests = u2fParams.sign_requests;
this.templates = {
notSupported: '#js-register-u2f-not-supported',
setup: '#js-register-u2f-setup',
inProgress: '#js-register-u2f-in-progress',
error: '#js-register-u2f-error',
registered: '#js-register-u2f-registered',
message: '#js-register-2fa-message',
setup: '#js-register-token-2fa-setup',
error: '#js-register-token-2fa-error',
registered: '#js-register-token-2fa-registered',
};
}
@ -65,18 +65,22 @@ export default class U2FRegister {
renderSetup() {
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() {
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();
}
renderError(error) {
this.renderTemplate('error', {
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);
}
@ -89,6 +93,10 @@ export default class U2FRegister {
}
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).",
),
});
}
}

View File

@ -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');
}
}

View File

@ -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.');
}
}

View File

@ -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,
});
}
}

View File

@ -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();
};

View File

@ -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 });
}
}

View File

@ -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',
]),
};
}

View File

@ -2,7 +2,14 @@
/* eslint-disable vue/no-v-html */
import { escape } from 'lodash';
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 FileIcon from '~/vue_shared/components/file_icon.vue';
import { truncateSha } from '~/lib/utils/text_utility';
@ -21,9 +28,11 @@ export default {
GlIcon,
FileIcon,
DiffStats,
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
SafeHtml: GlSafeHtmlDirective,
},
props: {
discussionPath: {
@ -77,6 +86,21 @@ export default {
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() {
if (this.diffFile.submodule) {
return `${this.diffFile.file_path} @ ${truncateSha(this.diffFile.blob.id)}`;
@ -311,5 +335,18 @@ export default {
</a>
</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>
</template>

View File

@ -26,7 +26,7 @@ export default {
<div
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"
>
<span class="gl-text-body">{{ dependency.version_pattern }}</span>

View File

@ -1,5 +1,5 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as Flash } from '~/flash';
@ -12,6 +12,9 @@ export default {
components: {
DeprecatedModal,
},
directives: {
SafeHtml,
},
props: {
issueCount: {
type: Number,
@ -125,7 +128,7 @@ Once deleted, it cannot be undone or recovered.`),
@submit="onSubmit"
>
<template #body="props">
<p v-html="props.text"></p>
<p v-safe-html="props.text"></p>
</template>
</deprecated-modal>
</template>

View File

@ -14,10 +14,20 @@ import createTestReportsStore from './stores/test_reports';
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 => {
if (!document.querySelector(SELECTORS.PIPELINE_GRAPH)) {
return;
}
// eslint-disable-next-line no-new
new Vue({
el: '#js-pipeline-graph-vue',
el: SELECTORS.PIPELINE_GRAPH,
components: {
pipelineGraph,
},
@ -47,9 +57,12 @@ const createPipelinesDetailApp = mediator => {
};
const createPipelineHeaderApp = mediator => {
if (!document.querySelector(SELECTORS.PIPELINE_HEADER)) {
return;
}
// eslint-disable-next-line no-new
new Vue({
el: '#js-pipeline-header-vue',
el: SELECTORS.PIPELINE_HEADER,
components: {
pipelineHeader,
},
@ -93,9 +106,8 @@ const createPipelineHeaderApp = mediator => {
};
const createTestDetails = () => {
const el = document.querySelector('#js-pipeline-tests-detail');
const el = document.querySelector(SELECTORS.PIPELINE_TESTS);
const { summaryEndpoint, suiteEndpoint } = el?.dataset || {};
const testReportsStore = createTestReportsStore({
summaryEndpoint,
suiteEndpoint,
@ -115,7 +127,7 @@ const createTestDetails = () => {
};
export default () => {
const { dataset } = document.querySelector('.js-pipeline-details-vue');
const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
mediator.fetchPipeline();

View File

@ -17,7 +17,7 @@ export default function deviseState() {
return stateKey.pipelineFailed;
} else if (this.workInProgress) {
return stateKey.workInProgress;
} else if (this.hasMergeableDiscussionsState) {
} else if (this.hasMergeableDiscussionsState && !this.autoMergeEnabled) {
return stateKey.unresolvedDiscussions;
} else if (this.isPipelineBlocked) {
return stateKey.pipelineBlocked;

View File

@ -193,6 +193,10 @@ table {
}
}
.detected {
width: 9%;
}
.status {
width: 8%;
}
@ -202,7 +206,7 @@ table {
}
.identifier {
width: 12%;
width: 16%;
}
.scanner {

View File

@ -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) {
.todos-filters {
.filter-categories {
@ -206,6 +194,10 @@
.dropdown-menu-toggle {
width: 100%;
}
.dropdown-menu-toggle-sort {
width: auto;
}
}
}

View File

@ -257,7 +257,8 @@
}
}
table.u2f-registrations {
table.u2f-registrations,
.webauthn-registrations {
th:not(:last-child),
td:not(:last-child) {
border-right: solid 1px transparent;

View File

@ -112,27 +112,10 @@
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 {
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 {
@media (min-width: $breakpoint-sm) {
@ -152,15 +135,11 @@
}
}
.gl-align-items-stretch {
align-items: stretch;
}
.gl-min-h-6 {
min-height: $gl-spacing-scale-6;
}
.gl-justify-content-md-end {
.gl-md-justify-content-end {
@media (min-width: $breakpoint-md) {
width: auto !important;
}

View File

@ -11,7 +11,13 @@ module Authenticates2FAForAdminMode
return handle_locked_user(user) unless user.can?(:log_in)
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'
end
@ -24,7 +30,11 @@ module Authenticates2FAForAdminMode
if user_params[:otp_attempt].present? && session[:otp_user_id]
admin_mode_authenticate_with_two_factor_via_otp(user)
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])
admin_mode_prompt_for_two_factor(user)
else
@ -52,18 +62,17 @@ module Authenticates2FAForAdminMode
def admin_mode_authenticate_with_two_factor_via_u2f(user)
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge])
# 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
admin_handle_two_factor_success
else
user.increment_failed_attempts!
Gitlab::AppLogger.info("Failed Admin Mode Login: user=#{user.username} ip=#{request.remote_ip} method=U2F")
flash.now[:alert] = _('Authentication via U2F device failed.')
admin_handle_two_factor_failure(user, 'U2F')
end
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
@ -81,4 +90,21 @@ module Authenticates2FAForAdminMode
flash.now[:alert] = _('Invalid login or password')
render :new
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

View File

@ -23,8 +23,14 @@ module AuthenticatesWithTwoFactor
session[:otp_user_id] = user.id
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'
end
@ -46,7 +52,11 @@ module AuthenticatesWithTwoFactor
if user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user)
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])
prompt_for_two_factor(user)
end
@ -89,16 +99,17 @@ module AuthenticatesWithTwoFactor
# Authenticate using the response from a U2F (universal 2nd factor) device
def authenticate_with_two_factor_via_u2f(user)
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge])
# 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)
handle_two_factor_success(user)
else
user.increment_failed_attempts!
Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=U2F")
flash.now[:alert] = _('Authentication via U2F device failed.')
prompt_for_two_factor(user)
handle_two_factor_failure(user, 'U2F')
end
end
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
@ -116,8 +127,39 @@ module AuthenticatesWithTwoFactor
sign_requests: sign_requests })
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
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)
clear_two_factor_attempt!

View File

@ -2,6 +2,9 @@
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_two_factor_requirement
before_action do
push_frontend_feature_flag(:webauthn)
end
def show
unless current_user.two_factor_enabled?
@ -33,7 +36,12 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
@qr_code = build_qr_code
@account_string = account_string
setup_u2f_registration
if Feature.enabled?(:webauthn)
setup_webauthn_registration
else
setup_u2f_registration
end
end
def create
@ -48,7 +56,13 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
else
@error = _('Invalid pin code')
@qr_code = build_qr_code
setup_u2f_registration
if Feature.enabled?(:webauthn)
setup_webauthn_registration
else
setup_u2f_registration
end
render 'show'
end
end
@ -56,7 +70,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
# A U2F (universal 2nd factor) device's information is stored after successful
# registration, which is then used while 2FA authentication is taking place.
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?
session.delete(:challenges)
@ -68,6 +82,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
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
Users::UpdateService.new(current_user, user: current_user).execute! do |user|
@codes = user.generate_otp_backup_codes!
@ -112,11 +141,11 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
# Actual communication is performed using a Javascript API
def setup_u2f_registration
@u2f_registration ||= U2fRegistration.new
@u2f_registrations = current_user.u2f_registrations
@registrations = u2f_registrations
u2f = U2F::U2F.new(u2f_app_id)
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)
gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
@ -124,8 +153,53 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
sign_requests: sign_requests })
end
def u2f_registration_params
params.require(:u2f_registration).permit(:device_response, :name)
def device_registration_params
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
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
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

View File

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

View File

@ -6,6 +6,9 @@ class ProfilesController < Profiles::ApplicationController
before_action :user
before_action :authorize_change_username!, only: :update_username
skip_before_action :require_email, only: [:show, :update]
before_action do
push_frontend_feature_flag(:webauthn)
end
def show
end

View File

@ -43,6 +43,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:discussion_locked,
label_ids: [],
assignee_ids: [],
reviewer_ids: [],
update_task: [:index, :checked, :line_number, :line_source]
]
end

View File

@ -42,7 +42,7 @@ class ProjectsController < Projects::ApplicationController
before_action only: [:edit] do
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
layout :determine_layout

View File

@ -29,6 +29,9 @@ class SessionsController < Devise::SessionsController
before_action :save_failed_login, if: :action_new_and_failed_login?
before_action :load_recaptcha
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 :verify_known_sign_in, only: [:create]
@ -293,7 +296,9 @@ class SessionsController < Devise::SessionsController
def authentication_method
if user_params[:otp_attempt]
"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"
else
"standard"

View File

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

View File

@ -105,6 +105,9 @@ module Types
Types::AlertManagement::AlertType,
null: true,
description: 'Alert associated to this issue'
field :severity, Types::IssuableSeverityEnum, null: true,
description: 'Severity level of the incident'
end
end

View File

@ -100,20 +100,43 @@ module DiffHelper
end
def submodule_link(blob, ref, repository = @repository)
project_url, tree_url = submodule_links(blob, ref, repository)
commit_id = if tree_url.nil?
Commit.truncate_sha(blob.id)
else
link_to Commit.truncate_sha(blob.id), tree_url
end
urls = submodule_links(blob, ref, repository)
folder_name = truncate(blob.name, length: 40)
folder_name = link_to(folder_name, urls.web) if urls&.web
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')
].join(' ').html_safe
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)
project_raw_url(@project, tree_join(diff_file.content_sha, diff_file.file_path), only_path: only_path)
end

View File

@ -6,12 +6,12 @@ module SubmoduleHelper
VALID_SUBMODULE_PROTOCOLS = %w[http https git ssh].freeze
# links to files listing for submodule if submodule is a project on this server
def submodule_links(submodule_item, ref = nil, repository = @repository)
repository.submodule_links.for(submodule_item, ref)
def submodule_links(submodule_item, ref = nil, repository = @repository, diff_file = nil)
repository.submodule_links.for(submodule_item, ref, diff_file)
end
def submodule_links_for_url(submodule_item_id, url, repository)
return [nil, nil] unless url
def submodule_links_for_url(submodule_item_id, url, repository, old_submodule_item_id = nil)
return [nil, nil, nil] unless url
if url == '.' || url == './'
url = File.join(Gitlab.config.gitlab.url, repository.project.full_path)
@ -34,21 +34,24 @@ module SubmoduleHelper
project.sub!(/\.git\z/, '')
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)
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)
gist_github_com_tree_links(namespace, project, submodule_item_id)
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)
gitlab_com_tree_links(namespace, project, submodule_item_id)
gitlab_com_tree_links(namespace, project, submodule_item_id, old_submodule_item_id)
else
[sanitize_submodule_url(url), nil]
[sanitize_submodule_url(url), nil, nil]
end
else
[sanitize_submodule_url(url), nil]
[sanitize_submodule_url(url), nil, nil]
end
end
@ -79,22 +82,30 @@ module SubmoduleHelper
url.start_with?('../', './')
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, [base, '/-/tree/', commit].join('')]
[
base,
[base, '/-/tree/', commit].join(''),
([base, '/-/compare/', old_commit, '...', commit].join('') if old_commit)
]
end
def gist_github_com_tree_links(namespace, project, commit)
base = ['https://gist.github.com/', namespace, '/', project].join('')
[base, [base, commit].join('/')]
[base, [base, commit].join('/'), nil]
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, [base, '/tree/', commit].join('')]
[
base,
[base, '/tree/', commit].join(''),
([base, '/compare/', old_commit, '...', commit].join('') if old_commit)
]
end
def relative_self_links(relative_path, commit, project)
def relative_self_links(relative_path, commit, old_commit, project)
relative_path = relative_path.rstrip
absolute_project_path = "/" + project.full_path
@ -107,7 +118,7 @@ module SubmoduleHelper
target_namespace_path = File.dirname(submodule_project_path)
if target_namespace_path == '/' || target_namespace_path.start_with?(absolute_project_path)
return [nil, nil]
return [nil, nil, nil]
end
target_namespace_path.sub!(%r{^/}, '')
@ -116,10 +127,11 @@ module SubmoduleHelper
begin
[
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
[nil, nil]
[nil, nil, nil]
end
end

View File

@ -108,6 +108,14 @@ module Ci
Ci::BuildTraceChunkFlushWorker.perform_async(id)
end
def persisted?
!redis?
end
def live?
redis?
end
private
def get_data
@ -170,14 +178,6 @@ module Ci
save! if changed?
end
def persisted?
!redis?
end
def live?
redis?
end
def full?
size == CHUNK_SIZE
end

View File

@ -262,7 +262,7 @@ module Ci
scope :internal, -> { where(source: internal_sources) }
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_sha, -> (sha) { where(sha: sha) }
scope :for_source_sha, -> (source_sha) { where(source_sha: source_sha) }
@ -1033,7 +1033,11 @@ module Ci
end
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
def source_ref_path

View File

@ -36,6 +36,23 @@ module Enums
}
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
# `Ci::Pipeline`.
def self.config_sources
@ -50,24 +67,6 @@ module Enums
parameter_source: 7
}
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

View File

@ -177,6 +177,10 @@ module Issuable
assignees.count > 1
end
def allows_reviewers?
false
end
def supports_time_tracking?
is_a?(TimeTrackable) && !incident?
end
@ -185,6 +189,12 @@ module Issuable
is_a?(Issue) && super
end
def severity
return IssuableSeverity::DEFAULT unless incident?
issuable_severity&.severity || IssuableSeverity::DEFAULT
end
private
def description_max_length_for_new_records_is_valid

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class IssuableSeverity < ApplicationRecord
DEFAULT = 'unknown'
belongs_to :issue
validates :issue, presence: true, uniqueness: true

View File

@ -60,6 +60,7 @@ class Issue < ApplicationRecord
end
end
has_one :issuable_severity
has_one :sentry_issue
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

View File

@ -19,6 +19,7 @@ class LfsObjectsProject < ApplicationRecord
}
scope :project_id_in, ->(ids) { where(project_id: ids) }
scope :lfs_object_in, -> (lfs_objects) { where(lfs_object: lfs_objects) }
private

View File

@ -12,6 +12,7 @@ class MembersPreloader
ActiveRecord::Associations::Preloader.new.preload(members, :source)
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), :webauthn_registrations)
end
end

View File

@ -955,8 +955,9 @@ class MergeRequest < ApplicationRecord
self.class.wip_title(self.title)
end
def mergeable?(skip_ci_check: false)
return false unless mergeable_state?(skip_ci_check: skip_ci_check)
def mergeable?(skip_ci_check: false, skip_discussions_check: false)
return false unless mergeable_state?(skip_ci_check: skip_ci_check,
skip_discussions_check: skip_discussions_check)
check_mergeability
@ -1658,6 +1659,10 @@ class MergeRequest < ApplicationRecord
end
end
def allows_reviewers?
Feature.enabled?(:merge_request_reviewers, project)
end
private
def with_rebase_lock

View File

@ -280,10 +280,9 @@ class Project < ApplicationRecord
# The relation :all_pipelines is intended to be used when we want to get the
# whole list of pipelines associated to the 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
# those pipeline which are directly related to CI. There are
# other pipelines, like webide ones, that we won't retrieve
# if we use this relation.
# The relation :ci_pipelines includes all those that directly contribute to the
# latest status of a ref. This does not include dangling pipelines such as those
# from webide, child pipelines, etc.
has_many :ci_pipelines,
-> { ci_sources },
class_name: 'Ci::Pipeline',
@ -2709,9 +2708,11 @@ class Project < ApplicationRecord
end
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

View File

@ -113,6 +113,7 @@ class User < ApplicationRecord
has_many :personal_access_tokens, dependent: :destroy # 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 :webauthn_registrations
has_many :chat_names, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :user_synced_attributes_metadata, autosave: true
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 :job_title, :job_title=, 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_detail, update_only: true
@ -434,14 +436,21 @@ class User < ApplicationRecord
FROM u2f_registrations AS u2f
WHERE u2f.user_id = users.id
) OR users.otp_required_for_login = ?
OR
EXISTS (
SELECT *
FROM webauthn_registrations AS webauthn
WHERE webauthn.user_id = users.id
)
SQL
where(with_u2f_registrations, true)
end
def self.without_two_factor
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)
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id
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
#
@ -754,11 +763,12 @@ class User < ApplicationRecord
otp_backup_codes: nil
)
self.u2f_registrations.destroy_all # rubocop: disable Cop/DestroyAll
self.webauthn_registrations.destroy_all # rubocop: disable Cop/DestroyAll
end
end
def two_factor_enabled?
two_factor_otp_enabled? || two_factor_u2f_enabled?
two_factor_otp_enabled? || two_factor_webauthn_u2f_enabled?
end
def two_factor_otp_enabled?
@ -773,6 +783,16 @@ class User < ApplicationRecord
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
if namespace&.any_project_has_container_registry_tags?
errors.add(:username, _('cannot be changed if a personal project has container registry tags.'))

View File

@ -12,11 +12,23 @@ class DiffFileBaseEntity < Grape::Entity
expose :submodule?, as: :submodule
expose :submodule_link do |diff_file, options|
memoized_submodule_links(diff_file, options).first
memoized_submodule_links(diff_file, options)&.web
end
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
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)
strong_memoize(:submodule_links) do
if diff_file.submodule?
options[:submodule_links].for(diff_file.blob, diff_file.content_sha)
else
[]
end
next unless diff_file.submodule?
options[:submodule_links].for(diff_file.blob, diff_file.content_sha, diff_file)
end
end

View File

@ -9,6 +9,7 @@ class MergeRequestBasicEntity < Grape::Entity
expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity
expose :assignees, using: API::Entities::UserBasic
expose :reviewers, if: -> (m) { m.allows_reviewers? }, using: API::Entities::UserBasic
expose :task_status, :task_status_short
expose :lock_version, :lock_version
end

View File

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

View File

@ -4,4 +4,8 @@ class MergeRequestSidebarExtrasEntity < IssuableSidebarExtrasEntity
expose :assignees do |merge_request|
MergeRequestAssigneeEntity.represent(merge_request.assignees, merge_request: merge_request)
end
expose :reviewers, if: -> (m) { m.allows_reviewers? } do |merge_request|
MergeRequestReviewerEntity.represent(merge_request.reviewers, merge_request: merge_request)
end
end

View File

@ -46,7 +46,7 @@ class IssuableBaseService < BaseService
params[:assignee_ids] = params[:assignee_ids].first(1)
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]
params[:assignee_ids] = []
@ -57,15 +57,15 @@ class IssuableBaseService < BaseService
end
end
def assignee_can_read?(issuable, assignee_id)
new_assignee = User.find_by_id(assignee_id)
def user_can_read?(issuable, user_id)
user = User.find_by_id(user_id)
return false unless new_assignee
return false unless user
ability_name = :"read_#{issuable.to_ability_name}"
resource = issuable.persisted? ? issuable : project
can?(new_assignee, ability_name, resource)
can?(user, ability_name, resource)
end
def filter_milestone

View File

@ -97,6 +97,28 @@ module MergeRequests
unless merge_request.can_allow_collaboration?(current_user)
params.delete(:allow_collaboration)
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
def merge_request_metrics_service(merge_request)

View File

@ -10,13 +10,14 @@ module MergeRequests
class MergeService < MergeRequests::MergeBaseService
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)
FfMergeService.new(project, current_user, params).execute(merge_request)
return
end
@merge_request = merge_request
@options = options
validate!
@ -55,7 +56,7 @@ module MergeRequests
error =
if @merge_request.should_be_rebased?
'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'
elsif !@merge_request.squash && project.squash_always?
'This project requires squashing commits when merge requests are accepted.'

View File

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

View File

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

View File

@ -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
= 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.')

View File

@ -11,5 +11,5 @@
.login-body
- if current_user.two_factor_otp_enabled?
= render 'admin/sessions/two_factor_otp'
- if current_user.two_factor_u2f_enabled?
= render 'u2f/authenticate', render_remember_me: false, target_path: admin_session_path
- if current_user.two_factor_webauthn_u2f_enabled?
= render 'authentication/authenticate', render_remember_me: false, target_path: admin_session_path

View File

@ -6,7 +6,7 @@
%script#js-authenticate-token-2fa-error{ type: "text/template" }
%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?")
%script#js-authenticate-token-2fa-authenticated{ type: "text/template" }

View File

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

View File

@ -3,7 +3,7 @@
.login-box
.login-body
- 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
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
%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.
.prepend-top-20
= f.submit "Verify code", class: "btn btn-success", data: { qa_selector: 'verify_code_button' }
- if @user.two_factor_u2f_enabled?
= render "u2f/authenticate", params: params, resource: resource, resource_name: resource_name, render_remember_me: true, target_path: new_user_session_path
- if @user.two_factor_webauthn_u2f_enabled?
= render "authentication/authenticate", params: params, resource: resource, resource_name: resource_name, render_remember_me: true, target_path: new_user_session_path

View File

@ -1,6 +1,7 @@
- page_title _('Two-Factor Authentication'), _('Account')
- add_to_breadcrumbs(_('Two-Factor Authentication'), profile_account_path)
- @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 }
.row.gl-mt-3
@ -18,7 +19,7 @@
%div
= link_to _('Disable two-factor authentication'), profile_two_factor_auth_path,
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'
= form_tag codes_profile_two_factor_auth_path, {style: 'display: inline-block', method: :post} do |f|
= submit_tag _('Regenerate recovery codes'), class: 'btn'
@ -58,22 +59,35 @@
.row.gl-mt-3
.col-lg-4
%h4.gl-mt-0
= _('Register Universal Two-Factor (U2F) Device')
- if webauthn_enabled
= _('Register WebAuthn Device')
- else
= _('Register Universal Two-Factor (U2F) Device')
%p
= _('Use a hardware device to add the second factor of authentication.')
%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
- if @u2f_registration.errors.present?
= form_errors(@u2f_registration)
= render "u2f/register"
- registration = webauthn_enabled ? @webauthn_registration : @u2f_registration
- if registration.errors.present?
= 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
%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.table.table-bordered.u2f-registrations
%colgroup
@ -86,12 +100,15 @@
%th= s_('2FADevice|Registered On')
%th
%tbody
- @u2f_registrations.each do |registration|
- @registrations.each do |registration|
%tr
%td= registration.name.presence || html_escape_once(_("&lt;no name set&gt;")).html_safe
%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= registration[:name].presence || html_escape_once(_("&lt;no name set&gt;")).html_safe
%td= registration[:created_at].to_date.to_s(:medium)
%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
.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.")

View File

@ -9,6 +9,10 @@
.file-header-content
= 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?
- blob = diff_file.blob
.file-actions.d-none.d-sm-block

View File

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

View File

@ -897,7 +897,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
:idempotent: true
:tags: []
- :name: pipeline_background:ci_daily_build_group_report_results
:feature_category: :continuous_integration

View File

@ -1,10 +1,12 @@
# frozen_string_literal: true
module Ci
class BuildTraceChunkFlushWorker # rubocop:disable Scalability/IdempotentWorker
class BuildTraceChunkFlushWorker
include ApplicationWorker
include PipelineBackgroundQueue
idempotent!
# rubocop: disable CodeReuse/ActiveRecord
def perform(chunk_id)
::Ci::BuildTraceChunk.find_by(id: chunk_id).try do |chunk|

View File

@ -0,0 +1,5 @@
---
title: Optimise index on audit events for CSV export
merge_request: 41266
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Prevent MRs to be dropped from Merge Trains for open discussions
merge_request: 39957
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: WebAuthn support (behind feature flag)
merge_request: 26692
author: Jan Beckmann
type: added

View File

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

View File

@ -0,0 +1,5 @@
---
title: Exposes Incident's severity via GraphQL
merge_request: 40945
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Correctly preserve LFS objects in design or wiki repositories
merge_request: 41352
author:
type: fixed

View File

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

View File

@ -0,0 +1,5 @@
---
title: Replace v-html to v-safe-html directive
merge_request: 41305
author: Kazuya Kojima
type: other

View File

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

View File

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

View File

@ -63,9 +63,11 @@ resource :profile, only: [:show, :update] do
post :create_u2f
post :codes
patch :skip
post :create_webauthn
end
end
resources :u2f_registrations, only: [:destroy]
resources :webauthn_registrations, only: [:destroy]
end
end

View File

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

View File

@ -0,0 +1 @@
5c065dc7905fd1292e270d2248810d71fa71d6b6996e9d60c463a7eb36042881

View File

@ -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 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_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 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_user_id_and_name ON public.award_emoji USING btree (user_id, name);

View File

@ -15,7 +15,9 @@ See [Geo current limitations](../replication/index.md#current-limitations) for m
CAUTION: **Warning:**
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

View File

@ -588,8 +588,9 @@ database encryption. Proceed with caution.
1. On the **GitLab server**, make the following changes to `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_pages['enable'] = false
pages_external_url "http://<pages_server_URL>"
gitlab_pages['enable'] = false
gitlab_rails['pages_enabled']=false
gitlab_rails['pages_path'] = "/mnt/pages"
```

View File

@ -11,8 +11,7 @@ If you use a cloud-managed service, or provide your own PostgreSQL instance:
1. Set up PostgreSQL according to the
[database requirements document](../../install/requirements.md#database).
1. Set up a `gitlab` username with a password of your choice. The `gitlab` user
needs privileges to create the `gitlabhq_production` database.
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).
1. If you are using a cloud-managed service, you may need to grant additional
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.

View File

@ -5679,6 +5679,11 @@ type EpicIssue implements Noteable {
"""
relativePosition: Int
"""
Severity level of the incident
"""
severity: IssuableSeverity
"""
State of the issue
"""
@ -6927,6 +6932,11 @@ type Group {
"""
severity: [VulnerabilitySeverity!]
"""
List vulnerabilities by sort order
"""
sort: VulnerabilitySort = severity_desc
"""
Filter vulnerabilities by state
"""
@ -7263,6 +7273,36 @@ type InstanceSecurityDashboard {
): 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
"""
@ -7509,6 +7549,11 @@ type Issue implements Noteable {
"""
relativePosition: Int
"""
Severity level of the incident
"""
severity: IssuableSeverity
"""
State of the issue
"""
@ -12689,6 +12734,11 @@ type Project {
"""
severity: [VulnerabilitySeverity!]
"""
List vulnerabilities by sort order
"""
sort: VulnerabilitySort = severity_desc
"""
Filter vulnerabilities by state
"""
@ -13451,6 +13501,11 @@ type Query {
"""
severity: [VulnerabilitySeverity!]
"""
List vulnerabilities by sort order
"""
sort: VulnerabilitySort = severity_desc
"""
Filter vulnerabilities by state
"""
@ -18389,6 +18444,21 @@ enum VulnerabilitySeverity {
UNKNOWN
}
"""
Vulnerability sort values
"""
enum VulnerabilitySort {
"""
Severity in ascending order
"""
severity_asc
"""
Severity in descending order
"""
severity_desc
}
"""
The state of the vulnerability.
"""

View File

@ -15844,6 +15844,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "severity",
"description": "Severity level of the incident",
"args": [
],
"type": {
"kind": "ENUM",
"name": "IssuableSeverity",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "state",
"description": "State of the issue",
@ -19047,6 +19061,16 @@
},
"defaultValue": null
},
{
"name": "sort",
"description": "List vulnerabilities by sort order",
"type": {
"kind": "ENUM",
"name": "VulnerabilitySort",
"ofType": null
},
"defaultValue": "severity_desc"
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
@ -20075,6 +20099,47 @@
"enumValues": 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",
"name": "IssuableState",
@ -20727,6 +20792,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "severity",
"description": "Severity level of the incident",
"args": [
],
"type": {
"kind": "ENUM",
"name": "IssuableSeverity",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "state",
"description": "State of the issue",
@ -37132,6 +37211,16 @@
},
"defaultValue": null
},
{
"name": "sort",
"description": "List vulnerabilities by sort order",
"type": {
"kind": "ENUM",
"name": "VulnerabilitySort",
"ofType": null
},
"defaultValue": "severity_desc"
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
@ -39461,6 +39550,16 @@
},
"defaultValue": null
},
{
"name": "sort",
"description": "List vulnerabilities by sort order",
"type": {
"kind": "ENUM",
"name": "VulnerabilitySort",
"ofType": null
},
"defaultValue": "severity_desc"
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
@ -54034,6 +54133,29 @@
],
"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",
"name": "VulnerabilityState",

View File

@ -950,6 +950,7 @@ Relationship between an epic and an issue
| `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
| `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) |
| `severity` | IssuableSeverity | Severity level of the incident |
| `state` | IssueState! | State of the issue |
| `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 |
@ -1123,6 +1124,7 @@ Represents a Group Membership
| `milestone` | Milestone | Milestone of the issue |
| `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) |
| `severity` | IssuableSeverity | Severity level of the incident |
| `state` | IssueState! | State of the issue |
| `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 |

View File

@ -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:
```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:
```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.
## Single 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
## Single project issue
Get a single project issue.

View File

@ -288,6 +288,29 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
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)`.
Arguments:

View File

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

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

View File

@ -309,15 +309,29 @@ rating.
### 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)
must be created with the case-sensitive name `Vulnerability-Check`. This approval group must be set
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.
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. A [security scanner job](#security-scanning-tools) must be enabled for
`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:
![Unconfigured Approval Rules](img/unconfigured_security_approval_rules_and_jobs_v13_4.png)
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.
![Unconfigured Approval Rules with valid pipeline jobs](img/unconfigured_security_approval_rules_and_enabled_jobs_v13_4.png)
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. Click **Add approval rule**, or **Edit**.
- Add or change the **Rule name** to `Vulnerability-Check` (case sensitive).
1. Click **Enable**, or **Edit**.
1. Add or change the **Rule name** to `Vulnerability-Check` (case sensitive).
![Vulnerability Check Approver Rule](img/vulnerability-check_v13_0.png)
![Vulnerability Check Approver Rule](img/vulnerability-check_v13_4.png)
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 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.
`License-Check` is an approval rule you can enable to allow an individual or group to approve a
merge request that contains a `denied` license.
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)
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.
`License-Check` is a [security approval rule](#enabling-security-approvals-within-a-project)
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
[Enabling license approvals within a project](../compliance/license_compliance/index.md#enabling-license-approvals-within-a-project).
## Working in an offline environment

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@ -724,17 +724,21 @@ Developers of the project can view the policies configured in a project.
![View Policies](img/policies_v13_0.png)
### 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.
`License-Check` is an approval rule you can enable to allow an approver, individual, or group to
approve a merge request that contains a `denied` license.
`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
merge request that contains a `denied` license.
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)
with the case-sensitive name `License-Check`.
1. Navigate to your project's **Settings > General** and expand **Merge request approvals**.
1. Click **Enable** or **Edit**.
1. Add or change the **Rule name** to `License-Check` (case sensitive).
![License Check Approver Rule](img/license-check_v13_4.png)
- 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
enable this group in your project, the approval rule is enabled for all merge requests.

View File

@ -92,7 +92,8 @@ module HamlLint
File.open(path_to_file).any? do |line|
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

View File

@ -537,6 +537,20 @@ module API
)
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)
yield({ api: true, request: request })
end

View File

@ -116,6 +116,7 @@ module API
entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic
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 = users.preload(:user_detail)

View File

@ -25,7 +25,7 @@ module Gitlab
private
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
log_info("Found invalid references: #{invalid_references.count}")
@ -41,26 +41,22 @@ module Gitlab
end
end
def lfs_oids_from_repository
project.repository.gitaly_blob_client.get_all_lfs_pointers.map(&:lfs_oid)
end
def orphan_objects
# Get these first so racing with a git push can't remove any LFS objects
oids = project.lfs_objects_oids
def orphan_oids
lfs_oids_from_database - lfs_oids_from_repository
end
repos = [
project.repository,
project.design_repository,
project.wiki.repository
].select(&:exists?)
def lfs_oids_from_database
oids = []
project.lfs_objects.each_batch do |relation|
oids += relation.pluck(:oid) # rubocop:disable CodeReuse/ActiveRecord
repos.flat_map do |repo|
oids -= repo.gitaly_blob_client.get_all_lfs_pointers.map(&:lfs_oid)
end
oids
end
def orphan_objects
LfsObject.where(oid: orphan_oids) # rubocop:disable CodeReuse/ActiveRecord
# The remaining OIDs are not used by any repository, so are orphans
LfsObject.for_oids(oids)
end
def log_info(msg)

View File

@ -24,11 +24,11 @@ module Gitlab
end
def web_url
@submodule_links.first
@submodule_links&.web
end
def tree_url
@submodule_links.last
@submodule_links&.tree
end
end
end

View File

@ -4,14 +4,18 @@ module Gitlab
class SubmoduleLinks
include Gitlab::Utils::StrongMemoize
Urls = Struct.new(:web, :tree, :compare)
def initialize(repository)
@repository = repository
@cache_store = {}
end
def for(submodule, sha)
def for(submodule, sha, diff_file = nil)
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
private
@ -29,5 +33,15 @@ module Gitlab
urls = submodule_urls_for(sha)
urls && urls[path]
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

Some files were not shown because too many files have changed in this diff Show More