Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-31 21:08:16 +00:00
parent e7fb614993
commit 32bbedbc21
158 changed files with 2467 additions and 1445 deletions

View File

@ -93,6 +93,10 @@ rules:
group: internal
alphabetize:
order: ignore
'no-restricted-syntax':
- error
- selector: ImportSpecifier[imported.name='GlSkeletonLoading']
message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.'
overrides:
- files:
- '{,ee/,jh/}spec/frontend*/**/*'
@ -107,6 +111,8 @@ overrides:
message: 'Using $nextTick from a component instance is discouraged. Import nextTick directly from the Vue package.'
- selector: Identifier[name='setImmediate']
message: 'Prefer explicit waitForPromises (or equivalent), or jest.runAllTimers (or equivalent) to vague setImmediate calls.'
- selector: ImportSpecifier[imported.name='GlSkeletonLoading']
message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.'
- files:
- 'config/**/*'
- 'scripts/**/*'

View File

@ -8,17 +8,17 @@
## Author's checklist
- [ ] Consider taking [the GitLab Technical Writing Fundamentals course](https://gitlab.edcast.com/pathways/ECL-02528ee2-c334-4e16-abf3-e9d8b8260de4).
- [ ] Optional. Consider taking [the GitLab Technical Writing Fundamentals course](https://gitlab.edcast.com/pathways/ECL-02528ee2-c334-4e16-abf3-e9d8b8260de4).
- [ ] Follow the:
- [Documentation process](https://docs.gitlab.com/ee/development/documentation/workflow.html).
- [Documentation guidelines](https://docs.gitlab.com/ee/development/documentation/).
- [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide/).
- [ ] Ensure that the [product tier badge](https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#product-tier-badges) is added to topic's `h1`.
- [ ] [Request a review](https://docs.gitlab.com/ee/development/code_review.html#dogfooding-the-reviewers-feature) based on:
- The documentation page's [metadata](https://docs.gitlab.com/ee/development/documentation/#metadata).
- The [associated Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments).
- [ ] If you're adding or changing the main heading of the page (H1), ensure that the [product tier badge](https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#product-tier-badges) is added.
- [ ] If you are a GitLab team member, [request a review](https://docs.gitlab.com/ee/development/code_review.html#dogfooding-the-attention-request-feature) based on:
- The documentation page's [metadata](https://docs.gitlab.com/ee/development/documentation/#metadata).
- The [associated Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments).
If you are only adding documentation, do not add any of the following labels:
If you are a GitLab team member and only adding documentation, do not add any of the following labels:
- `~"frontend"`
- `~"backend"`
@ -27,7 +27,7 @@ If you are only adding documentation, do not add any of the following labels:
These labels cause the MR to be added to code verification QA issues.
## Review checklist
## Reviewer's checklist
Documentation-related MRs should be reviewed by a Technical Writer for a non-blocking review, based on [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and the [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide/).
@ -35,13 +35,13 @@ Documentation-related MRs should be reviewed by a Technical Writer for a non-blo
- Technical writer review items:
- [ ] Ensure docs metadata is present and up-to-date.
- [ ] Ensure the appropriate [labels](https://about.gitlab.com/handbook/engineering/ux/technical-writing/workflow/#labels) are added to this MR.
- [ ] Ensure a release milestone is set.
- If relevant to this MR, ensure [content topic type](https://docs.gitlab.com/ee/development/documentation/structure.html) principles are in use, including:
- [ ] The headings should be something you'd do a Google search for. Instead of `Default behavior`, say something like `Default behavior when you close an issue`.
- [ ] The headings (other than the page title) should be active. Instead of `Configuring GDK`, say something like `Configure GDK`.
- [ ] Any task steps should be written as a numbered list.
- If the content still needs to be edited for topic types, you can create a follow-up issue with the ~"docs-technical-debt" label.
- [ ] Review by assigned maintainer, who can always request/require the reviews above. Maintainer's review can occur before or after a technical writer review.
- [ ] Ensure a release milestone is set.
/label ~documentation ~"type::maintenance"
/label ~documentation ~"type::maintenance" ~"docs::improvement"
/assign me

View File

@ -2,6 +2,31 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 14.9.2 (2022-03-31)
### Security (20 changes)
- [Quarantine UsageDataNonSqlMetrics failing test](gitlab-org/security/gitlab@123fc00ff9f407284ce05007ddc373e1bd0aeede) ([merge request](gitlab-org/security/gitlab!2364))
- [Disallow login if password matches a fixed list](gitlab-org/security/gitlab@1a128ae3fb17b3d83974bb08034e4ba7a7d54e3b) ([merge request](gitlab-org/security/gitlab!2357))
- [Update devise-two-factor to 4.0.2](gitlab-org/security/gitlab@17c70b13dcd437c05de63b3286245af8e6f42210) ([merge request](gitlab-org/security/gitlab!2349))
- [Limit the number of tags associated with a CI runner](gitlab-org/security/gitlab@ed5daced882a0206e050c4f676a888ac1c2417b1) ([merge request](gitlab-org/security/gitlab!2303))
- [GitLab Pages Security Updates for 14.9](gitlab-org/security/gitlab@79709cabf71a57a336f490636a7e32a208fe0229) ([merge request](gitlab-org/security/gitlab!2327))
- [Upgrade swagger-ui dependency](gitlab-org/security/gitlab@14280c1d844be3ffc2f30f5321a818a7b6c51770) ([merge request](gitlab-org/security/gitlab!2336))
- [Modify release link format check to avoid regex if string is too long](gitlab-org/security/gitlab@f516d883b46e1441410476dc140d69fde51cdf0f) ([merge request](gitlab-org/security/gitlab!2307))
- [Masks variables in error messages](gitlab-org/security/gitlab@9cf62118390c0cfba3d36a4231a30a7836f06e2f) ([merge request](gitlab-org/security/gitlab!2308))
- [Escape user provided string to prevent XSS](gitlab-org/security/gitlab@2da3502aef64ed1b01c13d82418950cf284098c6) ([merge request](gitlab-org/security/gitlab!2313))
- [Monkey patch of RDoc to prevent Ruby segfault](gitlab-org/security/gitlab@0ae4925089a1b5fd7c9abeeb0756b3a50e05799a) ([merge request](gitlab-org/security/gitlab!2321))
- [Project import maps members' created_by_id users based on source user ID](gitlab-org/security/gitlab@3826f2a7c652d3f74e45bfef8888601ca1c86ba1) ([merge request](gitlab-org/security/gitlab!2301))
- [Redact InvalidURIError error messages](gitlab-org/security/gitlab@59b60e9cf8f79d6f41000d34a4434c5a04988030) ([merge request](gitlab-org/security/gitlab!2295))
- [Fix access for approval rules API](gitlab-org/security/gitlab@7890215aa29624cd67c5bc8ac25175f2866479b7) ([merge request](gitlab-org/security/gitlab!2322))
- [Fix kroki exploit](gitlab-org/security/gitlab@b2a44b407ab85ca056a271ba4e708128ef08d25f) ([merge request](gitlab-org/security/gitlab!2306))
- [Fix blind SSRF when looking up SSH host keys for mirroring](gitlab-org/security/gitlab@5a9509b52584302c508bd6dff1454f80aae371ea) ([merge request](gitlab-org/security/gitlab!2309))
- [Escape original content in reference redactor](gitlab-org/security/gitlab@b33b170a2c2df8285999f3631e8a53d35e0eed22) ([merge request](gitlab-org/security/gitlab!2317))
- [Security fix for CI/CD analytics visibility](gitlab-org/security/gitlab@f3febd00b440475b2aca0b9bd6728fa5f8750288) ([merge request](gitlab-org/security/gitlab!2304))
- [Latest commit exposed through fork of a private project](gitlab-org/security/gitlab@3f20d4f294a12ceb33bec19d86790f582fb7fb48) ([merge request](gitlab-org/security/gitlab!2294))
- [Fix Asana integration restricted branch filter](gitlab-org/security/gitlab@08aa0f55b1b715f7311ee6502cd6f8a1b875f878) ([merge request](gitlab-org/security/gitlab!2300))
- [Revert "JH need more complex passwords"](gitlab-org/security/gitlab@e2fb87ec5d4e235d6b83454980cec9c049849a1c) ([merge request](gitlab-org/security/gitlab!2352))
## 14.9.1 (2022-03-23)
### Fixed (1 change)
@ -604,6 +629,32 @@ entry.
- [Clean up issue_boards_filtered_search feature flag](gitlab-org/gitlab@a97ed09ffb0d88007b21a314ab48b2e50d7c4bfa) ([merge request](gitlab-org/gitlab!80771))
- [Add table for storing issue tsvector](gitlab-org/gitlab@ceabf5a8ad0d67768b05a58a84b242495645a57c) ([merge request](gitlab-org/gitlab!71913))
## 14.8.5 (2022-03-31)
### Security (21 changes)
- [Update to commonmarker 0.23.4](gitlab-org/security/gitlab@51532ccc5f1b6b053d4ca6c54496607e62f8f25c) ([merge request](gitlab-org/security/gitlab!2282))
- [Revert merge request approval groups behavior](gitlab-org/security/gitlab@dd9724e429033974da6c3852dc6fd33f0f2b0a46) ([merge request](gitlab-org/security/gitlab!2334))
- [Disallow login if password matches a fixed list](gitlab-org/security/gitlab@6779d5f2948425a7ad7f19a6e10f82cc10b80989) ([merge request](gitlab-org/security/gitlab!2358))
- [Update devise-two-factor to 4.0.2](gitlab-org/security/gitlab@0329d2d82a9064c0bae36e7b993ee40df7c999bc) ([merge request](gitlab-org/security/gitlab!2350))
- [Limit the number of tags associated with a CI runner](gitlab-org/security/gitlab@8d5938c08fe66c22f1bc54ff76cc9daf2de86b1a) ([merge request](gitlab-org/security/gitlab!2302))
- [GitLab Pages Security Updates for 14.9](gitlab-org/security/gitlab@5a5a862c8a9e37ca2ea84133f92b216eaa7cd148) ([merge request](gitlab-org/security/gitlab!2328))
- [Upgrade swagger-ui dependency](gitlab-org/security/gitlab@afcb570867db61347bb6a4e243bb2557340191be) ([merge request](gitlab-org/security/gitlab!2337))
- [Modify release link format check to avoid regex if string is too long](gitlab-org/security/gitlab@a3ab0ff9c470c1c6e5b4fd055ddd02dffce32652) ([merge request](gitlab-org/security/gitlab!2243))
- [Masks variables in error messages](gitlab-org/security/gitlab@94236bbdb8eef6600562bdc4e242e07eaed8c50f) ([merge request](gitlab-org/security/gitlab!2291))
- [Escape user provided string to prevent XSS](gitlab-org/security/gitlab@03e695d4c34546582b503b3f7712246206b56b99) ([merge request](gitlab-org/security/gitlab!2314))
- [Monkey patch of RDoc to prevent Ruby segfault](gitlab-org/security/gitlab@14eec4487387bc0c999f1c48b046a3ed3848c5a1) ([merge request](gitlab-org/security/gitlab!2232))
- [Project import maps members' created_by_id users based on source user ID](gitlab-org/security/gitlab@7fd7ab3f57e8d8b4e0aed42aebe9a8b7436a6255) ([merge request](gitlab-org/security/gitlab!2238))
- [Redact InvalidURIError error messages](gitlab-org/security/gitlab@0592c182bfd60aee501c4c66f47a71c9469f2bcd) ([merge request](gitlab-org/security/gitlab!2296))
- [Fix access for approval rules API](gitlab-org/security/gitlab@987e06bacba224519adf94cda73b5a8b2e7b917a) ([merge request](gitlab-org/security/gitlab!2323))
- [Fix kroki exploit](gitlab-org/security/gitlab@bf056c683af25ec4b94c0efa7166eea399ed6502) ([merge request](gitlab-org/security/gitlab!2277))
- [Fix blind SSRF when looking up SSH host keys for mirroring](gitlab-org/security/gitlab@3c853a32a73aba15e309d05111b744455a360cca) ([merge request](gitlab-org/security/gitlab!2310))
- [Escape original content in reference redactor](gitlab-org/security/gitlab@00ee99bc3834d9d59572272064c9ad6abeae5975) ([merge request](gitlab-org/security/gitlab!2318))
- [Security fix for CI/CD analytics visibility](gitlab-org/security/gitlab@691d69be77ae3c8e0a2598b75ccf336b672fd540) ([merge request](gitlab-org/security/gitlab!2273))
- [Latest commit exposed through fork of a private project](gitlab-org/security/gitlab@6ca7a3b040edac06b23a697bfc2bf46f457d6b81) ([merge request](gitlab-org/security/gitlab!2271))
- [Fix Asana integration restricted branch filter](gitlab-org/security/gitlab@4c1db692b4e99fab6cdbb818cf02fb879f6d4886) ([merge request](gitlab-org/security/gitlab!2218))
- [Revert "JH need more complex passwords"](gitlab-org/security/gitlab@919aa2b28645d49fb71508362a0c61da39893c69) ([merge request](gitlab-org/security/gitlab!2353))
## 14.8.4 (2022-03-16)
### Added (1 change)
@ -1319,6 +1370,32 @@ entry.
- [Use `ssh_data` gem instead of `net-ssh` and `sshkey` where possible](gitlab-org/gitlab@59a0ee8605d509753c9aec719f8e0da77bcc679d) ([merge request](gitlab-org/gitlab!77424))
- [Remove feature flag already default enabled](gitlab-org/gitlab@9b7059a4bf9dc2ecdce1910a931cc6967d05b5ad) ([merge request](gitlab-org/gitlab!78238)) **GitLab Enterprise Edition**
## 14.7.7 (2022-03-31)
### Security (21 changes)
- [Update to commonmarker 0.23.4](gitlab-org/security/gitlab@eb4b231173c86901f93b5b7781716b1f7706dad1) ([merge request](gitlab-org/security/gitlab!2283))
- [Revert merge request approval groups behavior](gitlab-org/security/gitlab@08e3ecced649f6ad241db6de7050b1502f7bef21) ([merge request](gitlab-org/security/gitlab!2333))
- [Disallow login if password matches a fixed list](gitlab-org/security/gitlab@02a69ab32da1ac67d855de3ee388d0bd2bb6586e) ([merge request](gitlab-org/security/gitlab!2359))
- [Update devise-two-factor to 4.0.2](gitlab-org/security/gitlab@c9fde96c7780f5b883cd1ac63d7ac3d5f4d78dc6) ([merge request](gitlab-org/security/gitlab!2351))
- [Limit the number of tags associated with a CI runner](gitlab-org/security/gitlab@00124d5f8ba0d7437d1f6f19b029754bf481185b) ([merge request](gitlab-org/security/gitlab!2305))
- [GitLab Pages Security Updates for 14.9](gitlab-org/security/gitlab@d335917e233658fa9d4452053469c3582ef38368) ([merge request](gitlab-org/security/gitlab!2325))
- [Upgrade swagger-ui dependency](gitlab-org/security/gitlab@7a8ce32f70fd0338817705651ee0dbe0a277d5f1) ([merge request](gitlab-org/security/gitlab!2338))
- [Modify release link format check to avoid regex if string is too long](gitlab-org/security/gitlab@e18dc2be245bca7e192c8536d1ba7de2ad798c43) ([merge request](gitlab-org/security/gitlab!2244))
- [Masks variables in error messages](gitlab-org/security/gitlab@1706c5cf9b939a6ab0682db7b8945feb851a3f8b) ([merge request](gitlab-org/security/gitlab!2292))
- [Escape user provided string to prevent XSS](gitlab-org/security/gitlab@c57edf9ab52810d455e41d71bad4e4d12c098cad) ([merge request](gitlab-org/security/gitlab!2315))
- [Monkey patch of RDoc to prevent Ruby segfault](gitlab-org/security/gitlab@f9e5597d1864d03bf1f0103787becbc84886968d) ([merge request](gitlab-org/security/gitlab!2233))
- [Project import maps members' created_by_id users based on source user ID](gitlab-org/security/gitlab@3ea1e477e0596f15e040f42b59fa86953d057128) ([merge request](gitlab-org/security/gitlab!2239))
- [Redact InvalidURIError error messages](gitlab-org/security/gitlab@a42ede835e32f44b68c1affe78a7ee48332bb30a) ([merge request](gitlab-org/security/gitlab!2297))
- [Fix access for approval rules API](gitlab-org/security/gitlab@b8c3997763d1e041dc2b82e464a99a5b2f15a798) ([merge request](gitlab-org/security/gitlab!2324))
- [Fix kroki exploit](gitlab-org/security/gitlab@ad123e33510103af4fb00378ef1fc8dae4cacb21) ([merge request](gitlab-org/security/gitlab!2278))
- [Fix blind SSRF when looking up SSH host keys for mirroring](gitlab-org/security/gitlab@0209f44cb4876f0a9ef13d4c8875a95a0cda1e2f) ([merge request](gitlab-org/security/gitlab!2311))
- [Escape original content in reference redactor](gitlab-org/security/gitlab@f63861d8fe7b2b8d161162063e7995782cbfada8) ([merge request](gitlab-org/security/gitlab!2319))
- [Security fix for CI/CD analytics visibility](gitlab-org/security/gitlab@fea6a4ff80862f9dba493405d03d82cf129e8854) ([merge request](gitlab-org/security/gitlab!2274))
- [Latest commit exposed through fork of a private project](gitlab-org/security/gitlab@b573cea38cdce020e5f25fb9de60e0e506c87a9b) ([merge request](gitlab-org/security/gitlab!2272))
- [Fix Asana integration restricted branch filter](gitlab-org/security/gitlab@56e2d9ae3de4f587d2c8a5aa111c2922553d6b7b) ([merge request](gitlab-org/security/gitlab!2214))
- [Revert "JH need more complex passwords"](gitlab-org/security/gitlab@2419522b02700ce98e0c4d6e7bfd4d28b6464506) ([merge request](gitlab-org/security/gitlab!2354))
## 14.7.6 (2022-03-24)
### Added (1 change)

View File

@ -1 +1 @@
1.56.0
1.56.1

View File

@ -67,7 +67,7 @@ gem 'akismet', '~> 3.0'
gem 'invisible_captcha', '~> 1.1.0'
# Two-factor authentication
gem 'devise-two-factor', '~> 4.0.0'
gem 'devise-two-factor', '~> 4.0.2'
gem 'rqrcode-rails3', '~> 0.1.7'
gem 'attr_encrypted', '~> 3.1.0'
gem 'u2f', '~> 0.2.1'

View File

@ -270,11 +270,11 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise-two-factor (4.0.0)
activesupport (< 6.2)
devise-two-factor (4.0.2)
activesupport (< 7.1)
attr_encrypted (>= 1.3, < 4, != 2)
devise (~> 4.0)
railties (< 6.2)
railties (< 7.1)
rotp (~> 6.0)
diff-lcs (1.4.4)
diff_match_patch (0.1.0)
@ -1458,7 +1458,7 @@ DEPENDENCIES
derailed_benchmarks
device_detector
devise (~> 4.7.2)
devise-two-factor (~> 4.0.0)
devise-two-factor (~> 4.0.2)
diff_match_patch (~> 0.1.0)
diffy (~> 3.3)
discordrb-webhooks (~> 3.4)

View File

@ -0,0 +1,25 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
i18n: {
bodyText: __('Warning: Displaying this diagram might cause performance issues on this page.'),
buttonText: __('Display'),
},
components: {
GlAlert,
},
};
</script>
<template>
<gl-alert
:primary-button-text="$options.i18n.buttonText"
variant="warning"
@dismiss="$emit('closeAlert')"
@primaryAction="$emit('showImage')"
>
{{ $options.i18n.bodyText }}
</gl-alert>
</template>

View File

@ -1,3 +1,19 @@
// https://prosemirror.net/docs/ref/#model.ParseRule.priority
export const DEFAULT_PARSE_RULE_PRIORITY = 50;
export const HIGHER_PARSE_RULE_PRIORITY = 1 + DEFAULT_PARSE_RULE_PRIORITY;
export const unrestrictedPages = [
// Group wiki
'groups:wikis:show',
'groups:wikis:edit',
'groups:wikis:create',
// Project wiki
'projects:wikis:show',
'projects:wikis:edit',
'projects:wikis:create',
// Project files
'projects:show',
'projects:blob:show',
];

View File

@ -1,6 +1,7 @@
import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
import highlightCurrentUser from './highlight_current_user';
import { renderKroki } from './render_kroki';
import renderMath from './render_math';
import renderMermaid from './render_mermaid';
import renderSandboxedMermaid from './render_sandboxed_mermaid';
@ -12,6 +13,7 @@ import renderMetrics from './render_metrics';
//
$.fn.renderGFM = function renderGFM() {
syntaxHighlight(this.find('.js-syntax-highlight').get());
renderKroki(this.find('.js-render-kroki[hidden]').get());
renderMath(this.find('.js-render-math'));
if (gon.features?.sandboxedMermaid) {
renderSandboxedMermaid(this.find('.js-render-mermaid'));

View File

@ -0,0 +1,63 @@
import Vue from 'vue';
import DiagramPerformanceWarning from '../components/diagram_performance_warning.vue';
import { unrestrictedPages } from './constants';
/**
* Create alert element.
*
* @param {Element} krokiImage Kroki `img` element
* @return {Element} Alert element
*/
function createAlert(krokiImage) {
const app = new Vue({
el: document.createElement('div'),
name: 'DiagramPerformanceWarningRoot',
render(createElement) {
return createElement(DiagramPerformanceWarning, {
on: {
closeAlert() {
app.$destroy();
app.$el.remove();
},
showImage() {
krokiImage.removeAttribute('hidden');
app.$destroy();
app.$el.remove();
},
},
});
},
});
return app.$el;
}
/**
* Add warning alert to hidden Kroki images,
* or show Kroki image if on an unrestricted page.
*
* Kroki images are given a hidden attribute by the
* backend when the original markdown source is large.
*
* @param {Array<Element>} krokiImages Array of hidden Kroki `img` elements
*/
export function renderKroki(krokiImages) {
const pageName = document.querySelector('body').dataset.page;
const isUnrestrictedPage = unrestrictedPages.includes(pageName);
krokiImages.forEach((krokiImage) => {
if (isUnrestrictedPage) {
krokiImage.removeAttribute('hidden');
return;
}
const parent = krokiImage.parentElement;
// A single Kroki image is processed multiple times for some reason,
// so this condition ensures we only create one alert per Kroki image
if (!parent.hasAttribute('data-kroki-processed')) {
parent.setAttribute('data-kroki-processed', 'true');
parent.after(createAlert(krokiImage));
}
});
}

View File

@ -3,6 +3,7 @@ import { once, countBy } from 'lodash';
import createFlash from '~/flash';
import { darkModeEnabled } from '~/lib/utils/color_utils';
import { __, sprintf } from '~/locale';
import { unrestrictedPages } from './constants';
// Renders diagrams and flowcharts from text using Mermaid in any element with the
// `js-render-mermaid` class.
@ -30,24 +31,6 @@ let renderedMermaidBlocks = 0;
let mermaidModule = {};
// Whitelist pages where we won't impose any restrictions
// on mermaid rendering
const WHITELISTED_PAGES = [
// Group wiki
'groups:wikis:show',
'groups:wikis:edit',
'groups:wikis:create',
// Project wiki
'projects:wikis:show',
'projects:wikis:edit',
'projects:wikis:create',
// Project files
'projects:show',
'projects:blob:show',
];
export function initMermaid(mermaid) {
let theme = 'neutral';
@ -163,7 +146,7 @@ function renderMermaids($els) {
* up the entire thread and causing a DoS.
*/
if (
!WHITELISTED_PAGES.includes(pageName) &&
!unrestrictedPages.includes(pageName) &&
((source && source.length > MAX_CHAR_LIMIT) ||
renderedChars > MAX_CHAR_LIMIT ||
renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT ||

View File

@ -9,6 +9,7 @@ import {
} from '~/lib/utils/url_utility';
import { darkModeEnabled } from '~/lib/utils/color_utils';
import { setAttributes } from '~/lib/utils/dom_utils';
import { unrestrictedPages } from './constants';
// Renders diagrams and flowcharts from text using Mermaid in any element with the
// `js-render-mermaid` class.
@ -36,23 +37,6 @@ const BUFFER_IFRAME_HEIGHT = 10;
const elsProcessingMap = new WeakMap();
let renderedMermaidBlocks = 0;
// Pages without any restrictions on mermaid rendering
const PAGES_WITHOUT_RESTRICTIONS = [
// Group wiki
'groups:wikis:show',
'groups:wikis:edit',
'groups:wikis:create',
// Project wiki
'projects:wikis:show',
'projects:wikis:edit',
'projects:wikis:create',
// Project files
'projects:show',
'projects:blob:show',
];
function shouldLazyLoadMermaidBlock(source) {
/**
* If source contains `&`, which means that it might
@ -149,7 +133,7 @@ function renderMermaids($els) {
* up the entire thread and causing a DoS.
*/
if (
!PAGES_WITHOUT_RESTRICTIONS.includes(pageName) &&
!unrestrictedPages.includes(pageName) &&
((source && source.length > MAX_CHAR_LIMIT) ||
renderedChars > MAX_CHAR_LIMIT ||
renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT ||

View File

@ -1,6 +1,5 @@
import { SwaggerUIBundle } from 'swagger-ui-dist';
import createFlash from '~/flash';
import { removeParams, updateHistory } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
export default () => {
@ -8,14 +7,10 @@ export default () => {
Promise.all([import(/* webpackChunkName: 'openapi' */ 'swagger-ui-dist/swagger-ui.css')])
.then(() => {
// Temporary fix to prevent an XSS attack due to "useUnsafeMarkdown"
// Once we upgrade Swagger to "4.0.0", we can safely remove this as it will be deprecated
// Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/339696
updateHistory({ url: removeParams(['useUnsafeMarkdown']), replace: true });
SwaggerUIBundle({
url: el.dataset.endpoint,
dom_id: '#js-openapi-viewer',
useUnsafeMarkdown: false,
deepLinking: true,
});
})
.catch((error) => {

View File

@ -1,224 +0,0 @@
<script>
import { GlAlert, GlButton, GlDrawer, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { produce } from 'immer';
import { __, s__ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_GROUP } from '~/graphql_shared/constants';
import createContactMutation from './queries/create_contact.mutation.graphql';
import updateContactMutation from './queries/update_contact.mutation.graphql';
import getGroupContactsQuery from './queries/get_group_contacts.query.graphql';
export default {
components: {
GlAlert,
GlButton,
GlDrawer,
GlFormGroup,
GlFormInput,
},
inject: ['groupFullPath', 'groupId'],
props: {
drawerOpen: {
type: Boolean,
required: true,
},
contact: {
type: Object,
required: false,
default: () => {},
},
},
data() {
return {
firstName: '',
lastName: '',
phone: '',
email: '',
description: '',
submitting: false,
errorMessages: [],
};
},
computed: {
invalid() {
const { firstName, lastName, email } = this;
return firstName.trim() === '' || lastName.trim() === '' || email.trim() === '';
},
editMode() {
return Boolean(this.contact);
},
title() {
return this.editMode ? this.$options.i18n.editTitle : this.$options.i18n.newTitle;
},
buttonLabel() {
return this.editMode
? this.$options.i18n.editButtonLabel
: this.$options.i18n.createButtonLabel;
},
mutation() {
return this.editMode ? updateContactMutation : createContactMutation;
},
variables() {
const { contact, firstName, lastName, phone, email, description, editMode, groupId } = this;
const variables = {
input: {
firstName,
lastName,
phone,
email,
description,
},
};
if (editMode) {
variables.input.id = contact.id;
} else {
variables.input.groupId = convertToGraphQLId(TYPE_GROUP, groupId);
}
return variables;
},
},
mounted() {
if (this.editMode) {
const { contact } = this;
this.firstName = contact.firstName || '';
this.lastName = contact.lastName || '';
this.phone = contact.phone || '';
this.email = contact.email || '';
this.description = contact.description || '';
}
},
methods: {
save() {
const { mutation, variables, updateCache, close } = this;
this.submitting = true;
return this.$apollo
.mutate({
mutation,
variables,
update: updateCache,
})
.then(({ data }) => {
if (
data.customerRelationsContactCreate?.errors.length === 0 ||
data.customerRelationsContactUpdate?.errors.length === 0
) {
close(true);
}
this.submitting = false;
})
.catch(() => {
this.errorMessages = [this.$options.i18n.somethingWentWrong];
this.submitting = false;
});
},
close(success) {
this.$emit('close', success);
},
updateCache(store, { data }) {
const mutationData =
data.customerRelationsContactCreate || data.customerRelationsContactUpdate;
if (mutationData?.errors.length > 0) {
this.errorMessages = mutationData.errors;
return;
}
const queryArgs = {
query: getGroupContactsQuery,
variables: { groupFullPath: this.groupFullPath },
};
const sourceData = store.readQuery(queryArgs);
queryArgs.data = produce(sourceData, (draftState) => {
draftState.group.contacts.nodes = [
...sourceData.group.contacts.nodes.filter(({ id }) => id !== this.contact?.id),
mutationData.contact,
];
});
store.writeQuery(queryArgs);
},
getDrawerHeaderHeight() {
const wrapperEl = document.querySelector('.content-wrapper');
if (wrapperEl) {
return `${wrapperEl.offsetTop}px`;
}
return '';
},
},
i18n: {
createButtonLabel: s__('Crm|Create new contact'),
editButtonLabel: __('Save changes'),
cancel: __('Cancel'),
firstName: s__('Crm|First name'),
lastName: s__('Crm|Last name'),
email: s__('Crm|Email'),
phone: s__('Crm|Phone number (optional)'),
description: s__('Crm|Description (optional)'),
newTitle: s__('Crm|New contact'),
editTitle: s__('Crm|Edit contact'),
somethingWentWrong: __('Something went wrong. Please try again.'),
},
};
</script>
<template>
<gl-drawer
class="gl-drawer-responsive"
:open="drawerOpen"
:header-height="getDrawerHeaderHeight()"
@close="close(false)"
>
<template #title>
<h3>{{ title }}</h3>
</template>
<gl-alert v-if="errorMessages.length" variant="danger" @dismiss="errorMessages = []">
<ul class="gl-mb-0! gl-ml-5">
<li v-for="error in errorMessages" :key="error">
{{ error }}
</li>
</ul>
</gl-alert>
<form @submit.prevent="save">
<gl-form-group :label="$options.i18n.firstName" label-for="contact-first-name">
<gl-form-input id="contact-first-name" v-model="firstName" />
</gl-form-group>
<gl-form-group :label="$options.i18n.lastName" label-for="contact-last-name">
<gl-form-input id="contact-last-name" v-model="lastName" />
</gl-form-group>
<gl-form-group :label="$options.i18n.email" label-for="contact-email">
<gl-form-input id="contact-email" v-model="email" />
</gl-form-group>
<gl-form-group :label="$options.i18n.phone" label-for="contact-phone">
<gl-form-input id="contact-phone" v-model="phone" />
</gl-form-group>
<gl-form-group :label="$options.i18n.description" label-for="contact-description">
<gl-form-input id="contact-description" v-model="description" />
</gl-form-group>
<span class="gl-float-right">
<gl-button data-testid="cancel-button" @click="close(false)">
{{ $options.i18n.cancel }}
</gl-button>
<gl-button
variant="confirm"
:disabled="invalid"
:loading="submitting"
data-testid="save-contact-button"
type="submit"
>{{ buttonLabel }}</gl-button
>
</span>
</form>
</gl-drawer>
</template>

View File

@ -61,11 +61,6 @@ export default {
required: false,
default: null,
},
existingModel: {
type: Object,
required: false,
default: () => ({}),
},
additionalCreateParams: {
type: Object,
required: false,
@ -76,25 +71,42 @@ export default {
required: false,
default: () => MSG_SAVE_CHANGES,
},
existingId: {
type: String,
required: false,
default: null,
},
},
data() {
const initialModel = this.fields.reduce(
(map, field) =>
Object.assign(map, {
[field.name]: this.existingModel ? this.existingModel[field.name] : null,
}),
{},
);
return {
model: initialModel,
model: null,
submitting: false,
errorMessages: [],
records: [],
loading: true,
};
},
apollo: {
records: {
query() {
return this.getQuery.query;
},
variables() {
return this.getQuery.variables;
},
update(data) {
this.records = getPropValueByPath(data, this.getQueryNodePath).nodes || [];
this.setInitialModel();
this.loading = false;
},
error() {
this.errorMessages = [MSG_ERROR];
},
},
},
computed: {
isEditMode() {
return this.existingModel?.id;
return this.existingId;
},
isInvalid() {
const { fields, model } = this;
@ -115,13 +127,24 @@ export default {
);
if (isEditMode) {
return { input: { id: this.existingModel.id, ...variables } };
return { input: { id: this.existingId, ...variables } };
}
return { input: { ...additionalCreateParams, ...variables } };
},
},
methods: {
setInitialModel() {
const existingModel = this.records.find(({ id }) => id === this.existingId);
this.model = this.fields.reduce(
(map, field) =>
Object.assign(map, {
[field.name]: !this.isEditMode || !existingModel ? null : existingModel[field.name],
}),
{},
);
},
formatValue(model, field) {
if (!isEmpty(model[field.name]) && field.input?.type === 'number') {
return parseFloat(model[field.name]);
@ -173,7 +196,7 @@ export default {
const sourceData = store.readQuery(getQuery);
const newData = produce(sourceData, (draftState) => {
getPropValueByPath(draftState, getQueryNodePath).nodes.push(getFirstPropertyValue(result));
getPropValueByPath(draftState, getQueryNodePath).nodes.push(this.getPayload(result));
});
store.writeQuery({
@ -185,6 +208,14 @@ export default {
const optionalSuffix = field.required ? '' : ` ${MSG_OPTIONAL}`;
return field.label + optionalSuffix;
},
getPayload(data) {
if (!data) return null;
const keys = Object.keys(data);
if (keys[0] === '__typename') return data[keys[1]];
return data[keys[0]];
},
},
MSG_CANCEL,
INDEX_ROUTE_NAME,
@ -192,7 +223,7 @@ export default {
</script>
<template>
<mounting-portal mount-to="#js-crm-form-portal" append>
<mounting-portal v-if="!loading" mount-to="#js-crm-form-portal" append>
<gl-drawer class="gl-drawer-responsive gl-absolute" :open="drawerOpen" @close="close(false)">
<template #title>
<h3>{{ title }}</h3>

View File

@ -1,164 +0,0 @@
<script>
import { GlAlert, GlButton, GlDrawer, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { produce } from 'immer';
import { __, s__ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_GROUP } from '~/graphql_shared/constants';
import createOrganization from './queries/create_organization.mutation.graphql';
import getGroupOrganizationsQuery from './queries/get_group_organizations.query.graphql';
export default {
components: {
GlAlert,
GlButton,
GlDrawer,
GlFormGroup,
GlFormInput,
},
inject: ['groupFullPath', 'groupId'],
props: {
drawerOpen: {
type: Boolean,
required: true,
},
},
data() {
return {
name: '',
defaultRate: null,
description: '',
submitting: false,
errorMessages: [],
};
},
computed: {
invalid() {
return this.name.trim() === '';
},
},
methods: {
save() {
this.submitting = true;
return this.$apollo
.mutate({
mutation: createOrganization,
variables: {
input: {
groupId: convertToGraphQLId(TYPE_GROUP, this.groupId),
name: this.name,
defaultRate: this.defaultRate ? parseFloat(this.defaultRate) : null,
description: this.description,
},
},
update: this.updateCache,
})
.then(({ data }) => {
if (data.customerRelationsOrganizationCreate.errors.length === 0) this.close(true);
this.submitting = false;
})
.catch(() => {
this.errorMessages = [this.$options.i18n.somethingWentWrong];
this.submitting = false;
});
},
close(success) {
this.$emit('close', success);
},
updateCache(store, { data: { customerRelationsOrganizationCreate } }) {
if (customerRelationsOrganizationCreate.errors.length > 0) {
this.errorMessages = customerRelationsOrganizationCreate.errors;
return;
}
const variables = {
groupFullPath: this.groupFullPath,
};
const sourceData = store.readQuery({
query: getGroupOrganizationsQuery,
variables,
});
const data = produce(sourceData, (draftState) => {
draftState.group.organizations.nodes = [
...sourceData.group.organizations.nodes,
customerRelationsOrganizationCreate.organization,
];
});
store.writeQuery({
query: getGroupOrganizationsQuery,
variables,
data,
});
},
getDrawerHeaderHeight() {
const wrapperEl = document.querySelector('.content-wrapper');
if (wrapperEl) {
return `${wrapperEl.offsetTop}px`;
}
return '';
},
},
i18n: {
buttonLabel: s__('Crm|Create organization'),
cancel: __('Cancel'),
name: __('Name'),
defaultRate: s__('Crm|Default rate (optional)'),
description: __('Description (optional)'),
title: s__('Crm|New Organization'),
somethingWentWrong: __('Something went wrong. Please try again.'),
},
};
</script>
<template>
<gl-drawer
class="gl-drawer-responsive"
:open="drawerOpen"
:header-height="getDrawerHeaderHeight()"
@close="close(false)"
>
<template #title>
<h4>{{ $options.i18n.title }}</h4>
</template>
<gl-alert v-if="errorMessages.length" variant="danger" @dismiss="errorMessages = []">
<ul class="gl-mb-0! gl-ml-5">
<li v-for="error in errorMessages" :key="error">
{{ error }}
</li>
</ul>
</gl-alert>
<form @submit.prevent="save">
<gl-form-group :label="$options.i18n.name" label-for="organization-name">
<gl-form-input id="organization-name" v-model="name" />
</gl-form-group>
<gl-form-group :label="$options.i18n.defaultRate" label-for="organization-default-rate">
<gl-form-input
id="organization-default-rate"
v-model="defaultRate"
type="number"
step="0.01"
/>
</gl-form-group>
<gl-form-group :label="$options.i18n.description" label-for="organization-description">
<gl-form-input id="organization-description" v-model="description" />
</gl-form-group>
<span class="gl-float-right">
<gl-button data-testid="cancel-button" @click="close(false)">
{{ $options.i18n.cancel }}
</gl-button>
<gl-button
variant="confirm"
:disabled="invalid"
:loading="submitting"
data-testid="create-new-organization-button"
type="submit"
>{{ $options.i18n.buttonLabel }}</gl-button
>
</span>
</form>
</gl-drawer>
</template>

View File

@ -0,0 +1,78 @@
<script>
import { s__, __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_CRM_CONTACT, TYPE_GROUP } from '~/graphql_shared/constants';
import ContactForm from '../../components/form.vue';
import getGroupContactsQuery from './graphql/get_group_contacts.query.graphql';
import createContactMutation from './graphql/create_contact.mutation.graphql';
import updateContactMutation from './graphql/update_contact.mutation.graphql';
export default {
components: {
ContactForm,
},
inject: ['groupFullPath', 'groupId'],
props: {
isEditMode: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
contactGraphQLId() {
if (!this.isEditMode) return null;
return convertToGraphQLId(TYPE_CRM_CONTACT, this.$route.params.id);
},
groupGraphQLId() {
return convertToGraphQLId(TYPE_GROUP, this.groupId);
},
mutation() {
if (this.isEditMode) return updateContactMutation;
return createContactMutation;
},
getQuery() {
return {
query: getGroupContactsQuery,
variables: { groupFullPath: this.groupFullPath },
};
},
title() {
if (this.isEditMode) return s__('Crm|Edit contact');
return s__('Crm|New contact');
},
successMessage() {
if (this.isEditMode) return s__('Crm|Contact has been updated.');
return s__('Crm|Contact has been added.');
},
additionalCreateParams() {
return { groupId: this.groupGraphQLId };
},
},
fields: [
{ name: 'firstName', label: __('First name'), required: true },
{ name: 'lastName', label: __('Last name'), required: true },
{ name: 'email', label: __('Email'), required: true },
{ name: 'phone', label: __('Phone') },
{ name: 'description', label: __('Description') },
],
};
</script>
<template>
<contact-form
:drawer-open="true"
:get-query="getQuery"
get-query-node-path="group.contacts"
:mutation="mutation"
:additional-create-params="additionalCreateParams"
:existing-id="contactGraphQLId"
:fields="$options.fields"
:title="title"
:success-message="successMessage"
/>
</template>

View File

@ -2,11 +2,9 @@
import { GlAlert, GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import { s__, __ } from '~/locale';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPE_CRM_CONTACT } from '~/graphql_shared/constants';
import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from '../constants';
import getGroupContactsQuery from './queries/get_group_contacts.query.graphql';
import ContactForm from './contact_form.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { EDIT_ROUTE_NAME, NEW_ROUTE_NAME } from '../../constants';
import getGroupContactsQuery from './graphql/get_group_contacts.query.graphql';
export default {
components: {
@ -14,12 +12,11 @@ export default {
GlButton,
GlLoadingIcon,
GlTable,
ContactForm,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['groupFullPath', 'groupIssuesPath', 'canAdminCrmContact'],
inject: ['canAdminCrmContact', 'groupFullPath', 'groupIssuesPath'],
data() {
return {
contacts: [],
@ -48,50 +45,20 @@ export default {
isLoading() {
return this.$apollo.queries.contacts.loading;
},
showNewForm() {
return this.$route.name === NEW_ROUTE_NAME;
},
showEditForm() {
return !this.isLoading && this.$route.name === EDIT_ROUTE_NAME;
},
canAdmin() {
return parseBoolean(this.canAdminCrmContact);
},
editingContact() {
return this.contacts.find(
(contact) => contact.id === convertToGraphQLId(TYPE_CRM_CONTACT, this.$route.params.id),
);
},
},
methods: {
extractContacts(data) {
const contacts = data?.group?.contacts?.nodes || [];
return contacts.slice().sort((a, b) => a.firstName.localeCompare(b.firstName));
},
displayNewForm() {
if (this.showNewForm) return;
this.$router.push({ name: NEW_ROUTE_NAME });
},
hideNewForm(success) {
if (success) this.$toast.show(s__('Crm|Contact has been added'));
this.$router.replace({ name: INDEX_ROUTE_NAME });
},
hideEditForm(success) {
if (success) this.$toast.show(s__('Crm|Contact has been updated'));
this.editingContactId = 0;
this.$router.replace({ name: INDEX_ROUTE_NAME });
},
getIssuesPath(path, value) {
return `${path}?scope=all&state=opened&crm_contact_id=${value}`;
},
edit(value) {
if (this.showEditForm) return;
this.editingContactId = value;
this.$router.push({ name: EDIT_ROUTE_NAME, params: { id: value } });
getEditRoute(id) {
return { name: this.$options.EDIT_ROUTE_NAME, params: { id } };
},
},
fields: [
@ -119,10 +86,12 @@ export default {
emptyText: s__('Crm|No contacts found'),
issuesButtonLabel: __('View issues'),
editButtonLabel: __('Edit'),
title: s__('Crm|Customer Relations Contacts'),
title: s__('Crm|Customer relations contacts'),
newContact: s__('Crm|New contact'),
errorText: __('Something went wrong. Please try again.'),
},
EDIT_ROUTE_NAME,
NEW_ROUTE_NAME,
};
</script>
@ -137,24 +106,15 @@ export default {
<h2 class="gl-font-size-h2 gl-my-0">
{{ $options.i18n.title }}
</h2>
<div class="gl-display-none gl-md-display-flex gl-align-items-center gl-justify-content-end">
<gl-button
v-if="canAdmin"
variant="confirm"
data-testid="new-contact-button"
@click="displayNewForm"
>
{{ $options.i18n.newContact }}
</gl-button>
<div v-if="canAdmin">
<router-link :to="{ name: $options.NEW_ROUTE_NAME }">
<gl-button variant="confirm" data-testid="new-contact-button">
{{ $options.i18n.newContact }}
</gl-button>
</router-link>
</div>
</div>
<contact-form v-if="showNewForm" :drawer-open="showNewForm" @close="hideNewForm" />
<contact-form
v-if="showEditForm"
:contact="editingContact"
:drawer-open="showEditForm"
@close="hideEditForm"
/>
<router-view />
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
<gl-table
v-else
@ -164,23 +124,24 @@ export default {
:empty-text="$options.i18n.emptyText"
show-empty
>
<template #cell(id)="data">
<template #cell(id)="{ value: id }">
<gl-button
v-gl-tooltip.hover.bottom="$options.i18n.issuesButtonLabel"
class="gl-mr-3"
data-testid="issues-link"
icon="issues"
:aria-label="$options.i18n.issuesButtonLabel"
:href="getIssuesPath(groupIssuesPath, data.value)"
/>
<gl-button
v-if="canAdmin"
v-gl-tooltip.hover.bottom="$options.i18n.editButtonLabel"
data-testid="edit-contact-button"
icon="pencil"
:aria-label="$options.i18n.editButtonLabel"
@click="edit(data.value)"
:href="getIssuesPath(groupIssuesPath, id)"
/>
<router-link :to="getEditRoute(id)">
<gl-button
v-if="canAdmin"
v-gl-tooltip.hover.bottom="$options.i18n.editButtonLabel"
data-testid="edit-contact-button"
icon="pencil"
:aria-label="$options.i18n.editButtonLabel"
/>
</router-link>
</template>
</gl-table>
</div>

View File

@ -1,4 +1,5 @@
import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from './constants';
import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from '../constants';
import ContactFormWrapper from './components/contact_form_wrapper.vue';
export default [
{
@ -8,9 +9,12 @@ export default [
{
name: NEW_ROUTE_NAME,
path: '/new',
component: ContactFormWrapper,
},
{
name: EDIT_ROUTE_NAME,
path: '/:id/edit',
component: ContactFormWrapper,
props: { isEditMode: true },
},
];

View File

@ -0,0 +1,10 @@
#import "./crm_organization_fields.fragment.graphql"
mutation updateOrganization($input: CustomerRelationsOrganizationUpdateInput!) {
customerRelationsOrganizationUpdate(input: $input) {
organization {
...OrganizationFragment
}
errors
}
}

View File

@ -0,0 +1,80 @@
<script>
import { s__, __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_CRM_ORGANIZATION, TYPE_GROUP } from '~/graphql_shared/constants';
import OrganizationForm from '../../components/form.vue';
import getGroupOrganizationsQuery from './graphql/get_group_organizations.query.graphql';
import createOrganizationMutation from './graphql/create_organization.mutation.graphql';
import updateOrganizationMutation from './graphql/update_organization.mutation.graphql';
export default {
components: {
OrganizationForm,
},
inject: ['groupFullPath', 'groupId'],
props: {
isEditMode: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
organizationGraphQLId() {
if (!this.isEditMode) return null;
return convertToGraphQLId(TYPE_CRM_ORGANIZATION, this.$route.params.id);
},
groupGraphQLId() {
return convertToGraphQLId(TYPE_GROUP, this.groupId);
},
mutation() {
if (this.isEditMode) return updateOrganizationMutation;
return createOrganizationMutation;
},
getQuery() {
return {
query: getGroupOrganizationsQuery,
variables: { groupFullPath: this.groupFullPath },
};
},
title() {
if (this.isEditMode) return s__('Crm|Edit organization');
return s__('Crm|New organization');
},
successMessage() {
if (this.isEditMode) return s__('Crm|Organization has been updated.');
return s__('Crm|Organization has been added.');
},
additionalCreateParams() {
return { groupId: this.groupGraphQLId };
},
},
fields: [
{ name: 'name', label: __('Name'), required: true },
{
name: 'defaultRate',
label: s__('Crm|Default rate'),
input: { type: 'number', step: '0.01' },
},
{ name: 'description', label: __('Description') },
],
};
</script>
<template>
<organization-form
:drawer-open="true"
:get-query="getQuery"
get-query-node-path="group.organizations"
:mutation="mutation"
:additional-create-params="additionalCreateParams"
:existing-id="organizationGraphQLId"
:fields="$options.fields"
:title="title"
:success-message="successMessage"
/>
</template>

View File

@ -3,9 +3,8 @@ import { GlAlert, GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@
import { parseBoolean } from '~/lib/utils/common_utils';
import { s__, __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME } from '../constants';
import getGroupOrganizationsQuery from './queries/get_group_organizations.query.graphql';
import NewOrganizationForm from './new_organization_form.vue';
import { EDIT_ROUTE_NAME, NEW_ROUTE_NAME } from '../../constants';
import getGroupOrganizationsQuery from './graphql/get_group_organizations.query.graphql';
export default {
components: {
@ -13,7 +12,6 @@ export default {
GlButton,
GlLoadingIcon,
GlTable,
NewOrganizationForm,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -21,8 +19,8 @@ export default {
inject: ['canAdminCrmOrganization', 'groupFullPath', 'groupIssuesPath'],
data() {
return {
error: false,
organizations: [],
error: false,
};
},
apollo: {
@ -47,10 +45,7 @@ export default {
isLoading() {
return this.$apollo.queries.organizations.loading;
},
showNewForm() {
return this.$route.name === NEW_ROUTE_NAME;
},
canCreateNew() {
canAdmin() {
return parseBoolean(this.canAdminCrmOrganization);
},
},
@ -62,15 +57,8 @@ export default {
getIssuesPath(path, value) {
return `${path}?scope=all&state=opened&crm_organization_id=${value}`;
},
displayNewForm() {
if (this.showNewForm) return;
this.$router.push({ name: NEW_ROUTE_NAME });
},
hideNewForm(success) {
if (success) this.$toast.show(this.$options.i18n.organizationAdded);
this.$router.replace({ name: INDEX_ROUTE_NAME });
getEditRoute(id) {
return { name: this.$options.EDIT_ROUTE_NAME, params: { id } };
},
},
fields: [
@ -79,7 +67,7 @@ export default {
{ key: 'description', sortable: true },
{
key: 'id',
label: __('Issues'),
label: '',
formatter: (id) => {
return getIdFromGraphQLId(id);
},
@ -88,11 +76,13 @@ export default {
i18n: {
emptyText: s__('Crm|No organizations found'),
issuesButtonLabel: __('View issues'),
title: s__('Crm|Customer Relations Organizations'),
editButtonLabel: __('Edit'),
title: s__('Crm|Customer relations organizations'),
newOrganization: s__('Crm|New organization'),
errorText: __('Something went wrong. Please try again.'),
organizationAdded: s__('Crm|Organization has been added'),
},
EDIT_ROUTE_NAME,
NEW_ROUTE_NAME,
};
</script>
@ -108,15 +98,17 @@ export default {
{{ $options.i18n.title }}
</h2>
<div
v-if="canCreateNew"
v-if="canAdmin"
class="gl-display-none gl-md-display-flex gl-align-items-center gl-justify-content-end"
>
<gl-button variant="confirm" data-testid="new-organization-button" @click="displayNewForm">
{{ $options.i18n.newOrganization }}
</gl-button>
<router-link :to="{ name: $options.NEW_ROUTE_NAME }">
<gl-button variant="confirm" data-testid="new-organization-button">
{{ $options.i18n.newOrganization }}
</gl-button>
</router-link>
</div>
</div>
<new-organization-form v-if="showNewForm" :drawer-open="showNewForm" @close="hideNewForm" />
<router-view />
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
<gl-table
v-else
@ -126,14 +118,24 @@ export default {
:empty-text="$options.i18n.emptyText"
show-empty
>
<template #cell(id)="data">
<template #cell(id)="{ value: id }">
<gl-button
v-gl-tooltip.hover.bottom="$options.i18n.issuesButtonLabel"
class="gl-mr-3"
data-testid="issues-link"
icon="issues"
:aria-label="$options.i18n.issuesButtonLabel"
:href="getIssuesPath(groupIssuesPath, data.value)"
:href="getIssuesPath(groupIssuesPath, id)"
/>
<router-link :to="getEditRoute(id)">
<gl-button
v-if="canAdmin"
v-gl-tooltip.hover.bottom="$options.i18n.editButtonLabel"
data-testid="edit-organization-button"
icon="pencil"
:aria-label="$options.i18n.editButtonLabel"
/>
</router-link>
</template>
</gl-table>
</div>

View File

@ -0,0 +1,20 @@
import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from '../constants';
import OrganizationFormWrapper from './components/organization_form_wrapper.vue';
export default [
{
name: INDEX_ROUTE_NAME,
path: '/',
},
{
name: NEW_ROUTE_NAME,
path: '/new',
component: OrganizationFormWrapper,
},
{
name: EDIT_ROUTE_NAME,
path: '/:id/edit',
component: OrganizationFormWrapper,
props: { isEditMode: true },
},
];

View File

@ -3,6 +3,7 @@ export const MINIMUM_SEARCH_LENGTH = 3;
export const TYPE_BOARD = 'Board';
export const TYPE_CI_RUNNER = 'Ci::Runner';
export const TYPE_CRM_CONTACT = 'CustomerRelations::Contact';
export const TYPE_CRM_ORGANIZATION = 'CustomerRelations::Organization';
export const TYPE_DISCUSSION = 'Discussion';
export const TYPE_EPIC = 'Epic';
export const TYPE_EPIC_BOARD = 'Boards::EpicBoard';

View File

@ -9,7 +9,7 @@ const LINK_TAG_PATTERN = '[{text}](url)';
// a bullet point character (*+-) and an optional checkbox ([ ] [x])
// OR a number with a . after it and an optional checkbox ([ ] [x])
// followed by one or more whitespace characters
const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isUl>[*+-])|(?<isOl>\d+\.))( \[([x ])\])?\s)(?<content>.)?/;
const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isUl>[*+-])|(?<isOl>\d+\.))( \[([xX ])\])?\s)(?<content>.)?/;
function selectedText(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
@ -391,6 +391,8 @@ function handleContinueList(e, textArea) {
itemToInsert = `${indent}${leader}`;
}
itemToInsert = itemToInsert.replace(/\[x\]/i, '[ ]');
e.preventDefault();
updateText({

View File

@ -1,3 +1,3 @@
import initCrmContactsApp from '~/crm/contacts_bundle';
import initCrmContactsApp from '~/crm/contacts/bundle';
initCrmContactsApp();

View File

@ -1,3 +1,3 @@
import initCrmOrganizationsApp from '~/crm/organizations_bundle';
import initCrmOrganizationsApp from '~/crm/organizations/bundle';
initCrmOrganizationsApp();

View File

@ -1,5 +1,5 @@
<script>
import { GlSkeletonLoading } from '@gitlab/ui';
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { createAlert } from '~/flash';
import runnerJobsQuery from '../graphql/details/runner_jobs.query.graphql';
import { I18N_FETCH_ERROR, I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '../constants';

View File

@ -1,5 +1,5 @@
<script>
import { GlSkeletonLoading } from '@gitlab/ui';
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { sprintf, formatNumber } from '~/locale';
import { createAlert } from '~/flash';
import runnerProjectsQuery from '../graphql/details/runner_projects.query.graphql';

View File

@ -2,6 +2,7 @@
import { GlButton, GlCard, GlIcon, GlLink } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
import { REPORT_TYPE_SAST_IAC } from '~/vue_shared/security_reports/constants';
export default {
components: {
@ -61,6 +62,12 @@ export default {
const { name, description, configurationText } = this.feature.secondary ?? {};
return Boolean(name && description && configurationText);
},
// This condition is a temporary hack to not display any wrong information
// until this BE Bug is fixed: https://gitlab.com/gitlab-org/gitlab/-/issues/350307.
// More Information: https://gitlab.com/gitlab-org/gitlab/-/issues/350307#note_825447417
isNotSastIACTemporaryHack() {
return this.feature.type !== REPORT_TYPE_SAST_IAC;
},
},
methods: {
onError(message) {
@ -85,6 +92,7 @@ export default {
<h3 class="gl-font-lg gl-m-0 gl-mr-3">{{ feature.name }}</h3>
<div
v-if="isNotSastIACTemporaryHack"
:class="statusClasses"
data-testid="feature-status"
:data-qa-selector="`${feature.type}_status`"
@ -109,7 +117,7 @@ export default {
<gl-link :href="feature.helpPath">{{ $options.i18n.learnMore }}</gl-link>
</p>
<template v-if="available">
<template v-if="available && isNotSastIACTemporaryHack">
<gl-button
v-if="feature.configurationPath"
:href="feature.configurationPath"

View File

@ -1,5 +1,10 @@
<script>
import { GlAlert, GlKeysetPagination, GlSkeletonLoading, GlPagination } from '@gitlab/ui';
import {
GlAlert,
GlKeysetPagination,
GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlPagination,
} from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';

View File

@ -10,6 +10,10 @@ class Groups::Crm::OrganizationsController < Groups::ApplicationController
render action: "index"
end
def edit
render action: "index"
end
private
def authorize_read_crm_organization!

View File

@ -81,7 +81,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
def branch_to
@target_project = selected_target_project
if @target_project && params[:ref].present?
if @target_project && params[:ref].present? && Ability.allowed?(current_user, :create_merge_request_in, @target_project)
@ref = params[:ref]
@commit = @target_project.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref)
end

View File

@ -824,6 +824,8 @@ module Ci
variables.append(key: 'CI_OPEN_MERGE_REQUESTS', value: open_merge_requests_refs.join(','))
end
variables.append(key: 'CI_GITLAB_FIPS_MODE', value: 'true') if Gitlab::FIPS.enabled?
variables.append(key: 'CI_KUBERNETES_ACTIVE', value: 'true') if has_kubernetes_active?
variables.append(key: 'CI_DEPLOY_FREEZE', value: 'true') if freeze_period?

View File

@ -65,6 +65,8 @@ module Ci
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
MINUTES_COST_FACTOR_FIELDS = %i[public_projects_minutes_cost_factor private_projects_minutes_cost_factor].freeze
TAG_LIST_MAX_LENGTH = 50
has_many :builds
has_many :runner_projects, inverse_of: :runner, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :runner_projects, disable_joins: true
@ -520,6 +522,11 @@ module Ci
errors.add(:tags_list,
'can not be empty when runner is not allowed to pick untagged jobs')
end
if tag_list_changed? && tag_list.count > TAG_LIST_MAX_LENGTH
errors.add(:tags_list,
"Too many tags specified. Please limit the number of tags to #{TAG_LIST_MAX_LENGTH}")
end
end
def no_projects

View File

@ -59,12 +59,9 @@ module Integrations
def execute(data)
return unless supported_events.include?(data[:object_kind])
# check the branch restriction is poplulated and branch is not included
branch = Gitlab::Git.ref_name(data[:ref])
branch_restriction = restrict_to_branch.to_s
if branch_restriction.present? && branch_restriction.index(branch).nil?
return
end
return unless branch_allowed?(branch)
user = data[:user_name]
project_name = project.full_name
@ -103,5 +100,13 @@ module Integrations
end
end
end
private
def branch_allowed?(branch_name)
return true if restrict_to_branch.blank?
restrict_to_branch.to_s.gsub(/\s+/, '').split(',').include?(branch_name)
end
end
end

View File

@ -9,10 +9,20 @@ module Releases
# See https://gitlab.com/gitlab-org/gitlab/-/issues/218753
# Regex modified to prevent catastrophic backtracking
FILEPATH_REGEX = %r{\A\/[^\/](?!.*\/\/.*)[\-\.\w\/]+[\da-zA-Z]+\z}.freeze
FILEPATH_MAX_LENGTH = 128
validates :url, presence: true, addressable_url: { schemes: %w(http https ftp) }, uniqueness: { scope: :release }
validates :name, presence: true, uniqueness: { scope: :release }
validates :filepath, uniqueness: { scope: :release }, format: { with: FILEPATH_REGEX }, allow_blank: true, length: { maximum: 128 }
validates :filepath, uniqueness: { scope: :release }, allow_blank: true
validate :filepath_format_valid?
# we use a custom validator here to prevent running the regex if the string is too long
# see https://gitlab.com/gitlab-org/gitlab/-/issues/273771
def filepath_format_valid?
return if filepath.nil? # valid use case
return errors.add(:filepath, "is too long (maximum is #{FILEPATH_MAX_LENGTH} characters)") if filepath.length > FILEPATH_MAX_LENGTH
return errors.add(:filepath, 'is in an invalid format') unless FILEPATH_REGEX.match? filepath
end
scope :sorted, -> { order(created_at: :desc) }

View File

@ -46,11 +46,11 @@ class SshHostKey
.select(&:valid?)
end
attr_reader :project, :url, :compare_host_keys
attr_reader :project, :url, :ip, :compare_host_keys
def initialize(project:, url:, compare_host_keys: nil)
@project = project
@url = normalize_url(url)
@url, @ip = normalize_url(url)
@compare_host_keys = compare_host_keys
end
@ -90,9 +90,11 @@ class SshHostKey
end
def calculate_reactive_cache
input = [ip, url.hostname].compact.join(' ')
known_hosts, errors, status =
Open3.popen3({}, *%W[ssh-keyscan -T 5 -p #{url.port} -f-]) do |stdin, stdout, stderr, wait_thr|
stdin.puts(url.host)
stdin.puts(input)
stdin.close
[
@ -127,11 +129,31 @@ class SshHostKey
end
def normalize_url(url)
full_url = ::Addressable::URI.parse(url)
raise ArgumentError, "Invalid URL" unless full_url&.scheme == 'ssh'
url, real_hostname = Gitlab::UrlBlocker.validate!(
url,
schemes: %w[ssh],
allow_localhost: allow_local_requests?,
allow_local_network: allow_local_requests?,
dns_rebind_protection: Gitlab::CurrentSettings.dns_rebinding_protection_enabled?
)
Addressable::URI.parse("ssh://#{full_url.host}:#{full_url.inferred_port}")
rescue Addressable::URI::InvalidURIError
# When DNS rebinding protection is required, the hostname is replaced by the
# resolved IP. However, `url` is used in `id`, so we can't change it. Track
# the resolved IP separately instead.
if real_hostname
ip = url.hostname
url.hostname = real_hostname
end
# Ensure ssh://foo and ssh://foo:22 share the same cache
url.port = url.inferred_port
[url, ip]
rescue Gitlab::UrlBlocker::BlockedUrlError
raise ArgumentError, "Invalid URL"
end
def allow_local_requests?
Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
end
end

View File

@ -879,6 +879,23 @@ class User < ApplicationRecord
reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
end
# See https://gitlab.com/gitlab-org/security/gitlab/-/issues/638
DISALLOWED_PASSWORDS = %w[123qweQWE!@#000000000].freeze
# Overwrites valid_password? from Devise::Models::DatabaseAuthenticatable
# In constant-time, check both that the password isn't on a denylist AND
# that the password is the user's password
def valid_password?(password)
password_allowed = true
DISALLOWED_PASSWORDS.each do |disallowed_password|
password_allowed = false if Devise.secure_compare(password, disallowed_password)
end
original_result = super
password_allowed && original_result
end
def remember_me!
super if ::Gitlab::Database.read_write?
end

View File

@ -240,7 +240,6 @@ class ProjectPolicy < BasePolicy
rule { can?(:guest_access) }.policy do
enable :read_project
enable :create_merge_request_in
enable :read_issue_board
enable :read_issue_board_list
enable :read_wiki
@ -497,7 +496,7 @@ class ProjectPolicy < BasePolicy
prevent(*create_read_update_admin_destroy(:issue_board_list))
end
rule { merge_requests_disabled | repository_disabled }.policy do
rule { merge_requests_disabled | repository_disabled | ~can?(:download_code) }.policy do
prevent :create_merge_request_in
prevent :create_merge_request_from
prevent(*create_read_update_admin_destroy(:merge_request))
@ -600,13 +599,14 @@ class ProjectPolicy < BasePolicy
enable :read_cycle_analytics
enable :read_pages_content
enable :read_analytics
enable :read_ci_cd_analytics
enable :read_insights
# NOTE: may be overridden by IssuePolicy
enable :read_issue
end
rule { can?(:public_access) & public_builds }.enable :read_ci_cd_analytics
rule { public_builds }.policy do
enable :read_build
end
@ -664,6 +664,10 @@ class ProjectPolicy < BasePolicy
enable :read_security_configuration
end
rule { can?(:guest_access) & can?(:read_commit_status) }.policy do
enable :create_merge_request_in
end
# Design abilities could also be prevented in the issue policy.
rule { design_management_disabled }.policy do
prevent :read_design

View File

@ -77,7 +77,10 @@ module QuickActions
# want to also handle bare usernames. The ReferenceExtractor also has
# different behaviour, and will return all group members for groups named
# using a user-style reference, which is not in scope here.
#
# nb: underscores may be passed in escaped to protect them from markdown rendering
args = params.split(/\s|,/).select(&:present?).uniq - ['and']
args.map! { _1.gsub(/\\_/, '_') }
usernames = (args - ['me']).map { _1.delete_prefix('@') }
found = User.by_username(usernames).to_a.select { can?(:read_user, _1) }
found_names = found.map(&:username).to_set

View File

@ -1,4 +1,8 @@
- breadcrumb_title _('Customer Relations Contacts')
- page_title _('Customer Relations Contacts')
- breadcrumb_title _('Customer relations contacts')
- page_title _('Customer relations contacts')
- @content_wrapper_class = "gl-relative"
= content_for :after_content do
#js-crm-form-portal
#js-crm-contacts-app{ data: { group_full_path: @group.full_path, group_issues_path: issues_group_path(@group), group_id: @group.id, can_admin_crm_contact: can?(current_user, :admin_crm_contact, @group).to_s, base_path: group_crm_contacts_path(@group) } }

View File

@ -1,4 +1,8 @@
- breadcrumb_title _('Customer Relations Organizations')
- page_title _('Customer Relations Organizations')
- breadcrumb_title _('Customer relations organizations')
- page_title _('Customer relations organizations')
- @content_wrapper_class = "gl-relative"
= content_for :after_content do
#js-crm-form-portal
#js-crm-organizations-app{ data: { base_path: group_crm_organizations_path(@group), can_admin_crm_organization: can?(current_user, :admin_crm_organization, @group).to_s, group_full_path: @group.full_path, group_id: @group.id, group_issues_path: issues_group_path(@group) } }

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
# Monkey patch of RDoc to prevent Ruby segfault due to
# stack buffer overflow Ruby bug -
# https://bugs.ruby-lang.org/issues/16376
#
# Safe to remove once GitLab upgrades to Ruby 3.0
# or once the fix is backported to 2.7.x and
# GitLab upgrades.
# https://gitlab.com/gitlab-org/gitlab/-/issues/351179
class RDoc::Markup::ToHtml
def parseable?(_)
false
end
end
class RDoc::Markup::Verbatim
def ruby?
false
end
end

View File

@ -135,7 +135,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
namespace :crm do
resources :contacts, only: [:index, :new, :edit]
resources :organizations, only: [:index, :new]
resources :organizations, only: [:index, :new, :edit]
end
end

View File

@ -11,7 +11,7 @@ module Db
name: FFaker::Name.name,
email: FFaker::Internet.email,
confirmed_at: DateTime.now,
password: Gitlab::Password.test_default
password: '12345678'
)
::AbuseReport.create(reporter: ::User.take, user: reported_user, message: 'User sends spam')

View File

@ -58,6 +58,7 @@ There are also a number of [variables you can use to configure runner behavior](
| `CI_ENVIRONMENT_URL` | 9.3 | all | The URL of the environment for this job. Available if [`environment:url`](../yaml/index.md#environmenturl) is set. |
| `CI_ENVIRONMENT_ACTION` | 13.11 | all | The action annotation specified for this job's environment. Available if [`environment:action`](../yaml/index.md#environmentaction) is set. Can be `start`, `prepare`, or `stop`. |
| `CI_ENVIRONMENT_TIER` | 14.0 | all | The [deployment tier of the environment](../environments/index.md#deployment-tier-of-environments) for this job. |
| `CI_GITLAB_FIPS_MODE` | 14.10 | all | The configuration setting for whether FIPS mode is enabled in the GitLab instance. |
| `CI_HAS_OPEN_REQUIREMENTS` | 13.1 | all | Only available if the pipeline's project has an open [requirement](../../user/project/requirements/index.md). `true` when available. |
| `CI_JOB_ID` | 9.0 | all | The internal ID of the job, unique across all jobs in the GitLab instance. |
| `CI_JOB_IMAGE` | 12.9 | 12.9 | The name of the Docker image running the job. |

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -49,7 +49,7 @@ To view a group's contacts:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Customer relations > Contacts**.
![Contacts list](crm_contacts_v14_6.png)
![Contacts list](crm_contacts_v14_10.png)
### Create a contact
@ -86,7 +86,7 @@ To view a group's organizations:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Customer relations > Organizations**.
![Organizations list](crm_organizations_v14_6.png)
![Organizations list](crm_organizations_v14_10.png)
### Create an organization

View File

@ -6,8 +6,10 @@ require "asciidoctor/extensions/asciidoctor_kroki/extension"
module Banzai
module Filter
# HTML that replaces all diagrams supported by Kroki with the corresponding img tags.
#
# If the source content is large then the hidden attribute is added to the img tag.
class KrokiFilter < HTML::Pipeline::Filter
MAX_CHARACTER_LIMIT = 2000
def call
return doc unless settings.kroki_enabled
@ -21,10 +23,16 @@ module Banzai
diagram_format = "svg"
doc.xpath(xpath).each do |node|
diagram_type = node.parent['lang']
img_tag = Nokogiri::HTML::DocumentFragment.parse(%(<img src="#{create_image_src(diagram_type, diagram_format, node.content)}"/>))
diagram_src = node.content
image_src = create_image_src(diagram_type, diagram_format, diagram_src)
img_tag = Nokogiri::HTML::DocumentFragment.parse(%(<img src="#{image_src}" />))
img_tag = img_tag.children.first
unless img_tag.nil?
lazy_load = diagram_src.length > MAX_CHARACTER_LIMIT
img_tag.set_attribute('hidden', '') if lazy_load
img_tag.set_attribute('class', 'js-render-kroki')
img_tag.set_attribute('data-diagram', node.parent['lang'])
img_tag.set_attribute('data-diagram-src', "data:text/plain;base64,#{Base64.strict_encode64(node.content)}")

View File

@ -56,7 +56,7 @@ module Banzai
retry
end
sourcepos_attr = sourcepos ? "data-sourcepos=\"#{sourcepos}\"" : ''
sourcepos_attr = sourcepos ? "data-sourcepos=\"#{escape_once(sourcepos)}\"" : ''
highlighted = %(<div class="gl-relative markdown-code-block js-markdown-code"><pre #{sourcepos_attr} class="#{css_classes}"
lang="#{language}"

View File

@ -12,10 +12,10 @@ module Banzai
def self.filters
@filters ||= FilterArray[
Filter::PlantumlFilter,
Filter::KrokiFilter,
# Must always be before the SanitizationFilter to prevent XSS attacks
Filter::SpacedLinkFilter,
Filter::SanitizationFilter,
Filter::KrokiFilter,
Filter::AssetProxyFilter,
Filter::SyntaxHighlightFilter,
Filter::MathFilter,

View File

@ -65,16 +65,15 @@ module Banzai
#
def redacted_node_content(node)
original_content = node.attr('data-original')
link_reference = node.attr('data-link-reference')
original_content = CGI.escape_html(original_content) if original_content
# Build the raw <a> tag just with a link as href and content if
# it's originally a link pattern. We shouldn't return a plain text href.
original_link =
if link_reference == 'true'
if node.attr('data-link-reference') == 'true'
href = node.attr('href')
content = original_content
%(<a href="#{href}">#{content}</a>)
%(<a href="#{href}">#{original_content}</a>)
end
# The reference should be replaced by the original link's content,

View File

@ -239,8 +239,8 @@ module Gitlab
name: name.strip.presence || valid_username,
username: valid_username,
email: email,
password: Gitlab::Password.test_default(21),
password_confirmation: Gitlab::Password.test_default(21),
password: auth_hash.password,
password_confirmation: auth_hash.password,
password_automatically_set: true
}
end

View File

@ -70,6 +70,16 @@ module Gitlab
}
end
def mask_variables_from(location)
variables.reduce(location.dup) do |loc, variable|
if variable[:masked]
Gitlab::Ci::MaskSecret.mask!(loc, variable[:value])
else
loc
end
end
end
protected
attr_writer :expandset, :execution_deadline, :logger

View File

@ -37,7 +37,7 @@ module Gitlab
def validate_content!
return unless ensure_preconditions_satisfied!
errors.push("File `#{location}` is empty!") unless content.present?
errors.push("File `#{masked_location}` is empty!") unless content.present?
end
def ensure_preconditions_satisfied!

View File

@ -79,21 +79,21 @@ module Gitlab
def validate_location!
if invalid_location_type?
errors.push("Included file `#{location}` needs to be a string")
errors.push("Included file `#{masked_location}` needs to be a string")
elsif invalid_extension?
errors.push("Included file `#{location}` does not have YAML extension!")
errors.push("Included file `#{masked_location}` does not have YAML extension!")
end
end
def validate_content!
if content.blank?
errors.push("Included file `#{location}` is empty or does not exist!")
errors.push("Included file `#{masked_location}` is empty or does not exist!")
end
end
def validate_hash!
if to_hash.blank?
errors.push("Included file `#{location}` does not have valid YAML syntax!")
errors.push("Included file `#{masked_location}` does not have valid YAML syntax!")
end
end
@ -104,6 +104,12 @@ module Gitlab
def expand_context_attrs
{}
end
def masked_location
strong_memoize(:masked_location) do
context.mask_variables_from(location)
end
end
end
end
end

View File

@ -23,11 +23,11 @@ module Gitlab
def validate_content!
if context.project&.repository.nil?
errors.push("Local file `#{location}` does not have project!")
errors.push("Local file `#{masked_location}` does not have project!")
elsif content.nil?
errors.push("Local file `#{location}` does not exist!")
errors.push("Local file `#{masked_location}` does not exist!")
elsif content.blank?
errors.push("Local file `#{location}` is empty!")
errors.push("Local file `#{masked_location}` is empty!")
end
end

View File

@ -35,9 +35,9 @@ module Gitlab
elsif sha.nil?
errors.push("Project `#{project_name}` reference `#{ref_name}` does not exist!")
elsif content.nil?
errors.push("Project `#{project_name}` file `#{location}` does not exist!")
errors.push("Project `#{project_name}` file `#{masked_location}` does not exist!")
elsif content.blank?
errors.push("Project `#{project_name}` file `#{location}` is empty!")
errors.push("Project `#{project_name}` file `#{masked_location}` is empty!")
end
end

View File

@ -24,7 +24,7 @@ module Gitlab
super
unless ::Gitlab::UrlSanitizer.valid?(location)
errors.push("Remote file `#{location}` does not have a valid address!")
errors.push("Remote file `#{masked_location}` does not have a valid address!")
end
end
@ -32,17 +32,17 @@ module Gitlab
begin
response = Gitlab::HTTP.get(location)
rescue SocketError
errors.push("Remote file `#{location}` could not be fetched because of a socket error!")
errors.push("Remote file `#{masked_location}` could not be fetched because of a socket error!")
rescue Timeout::Error
errors.push("Remote file `#{location}` could not be fetched because of a timeout error!")
errors.push("Remote file `#{masked_location}` could not be fetched because of a timeout error!")
rescue Gitlab::HTTP::Error
errors.push("Remote file `#{location}` could not be fetched because of HTTP error!")
errors.push("Remote file `#{masked_location}` could not be fetched because of HTTP error!")
rescue Gitlab::HTTP::BlockedUrlError => e
errors.push("Remote file could not be fetched because #{e}!")
end
if response&.code.to_i >= 400
errors.push("Remote file `#{location}` could not be fetched because of HTTP code `#{response.code}` error!")
errors.push("Remote file `#{masked_location}` could not be fetched because of HTTP code `#{response.code}` error!")
end
response.body if errors.none?

View File

@ -26,7 +26,7 @@ module Gitlab
super
unless template_name_valid?
errors.push("Template file `#{location}` is not a valid location!")
errors.push("Template file `#{masked_location}` is not a valid location!")
end
end

View File

@ -143,7 +143,7 @@ module Gitlab
file_class.new(location, context)
end.select(&:matching?)
raise AmbigiousSpecificationError, "Include `#{location.to_json}` needs to match exactly one accessor!" unless matching.one?
raise AmbigiousSpecificationError, "Include `#{masked_location(location.to_json)}` needs to match exactly one accessor!" unless matching.one?
matching.first
end
@ -182,6 +182,10 @@ module Gitlab
def expand(data)
ExpandVariables.expand(data, -> { context.variables_hash })
end
def masked_location(location)
context.mask_variables_from(location)
end
end
end
end

View File

@ -4,10 +4,9 @@ module Gitlab
module Database
module BackgroundMigration
class BatchedMigrationWrapper
extend Gitlab::Utils::StrongMemoize
def initialize(connection: ApplicationRecord.connection)
def initialize(connection: ApplicationRecord.connection, metrics: PrometheusMetrics.new)
@connection = connection
@metrics = metrics
end
# Wraps the execution of a batched_background_migration.
@ -28,12 +27,12 @@ module Gitlab
raise
ensure
track_prometheus_metrics(batch_tracking_record)
metrics.track(batch_tracking_record)
end
private
attr_reader :connection
attr_reader :connection, :metrics
def start_tracking_execution(tracking_record)
tracking_record.run!
@ -63,80 +62,6 @@ module Gitlab
job_class.new
end
end
def track_prometheus_metrics(tracking_record)
migration = tracking_record.batched_migration
base_labels = migration.prometheus_labels
metric_for(:gauge_batch_size).set(base_labels, tracking_record.batch_size)
metric_for(:gauge_sub_batch_size).set(base_labels, tracking_record.sub_batch_size)
metric_for(:gauge_interval).set(base_labels, tracking_record.batched_migration.interval)
metric_for(:gauge_job_duration).set(base_labels, (tracking_record.finished_at - tracking_record.started_at).to_i)
metric_for(:counter_updated_tuples).increment(base_labels, tracking_record.batch_size)
metric_for(:gauge_migrated_tuples).set(base_labels, tracking_record.batched_migration.migrated_tuple_count)
metric_for(:gauge_total_tuple_count).set(base_labels, tracking_record.batched_migration.total_tuple_count)
metric_for(:gauge_last_update_time).set(base_labels, Time.current.to_i)
if metrics = tracking_record.metrics
metrics['timings']&.each do |key, timings|
summary = metric_for(:histogram_timings)
labels = base_labels.merge(operation: key)
timings.each do |timing|
summary.observe(labels, timing)
end
end
end
end
def metric_for(name)
self.class.metrics[name]
end
def self.metrics
strong_memoize(:metrics) do
{
gauge_batch_size: Gitlab::Metrics.gauge(
:batched_migration_job_batch_size,
'Batch size for a batched migration job'
),
gauge_sub_batch_size: Gitlab::Metrics.gauge(
:batched_migration_job_sub_batch_size,
'Sub-batch size for a batched migration job'
),
gauge_interval: Gitlab::Metrics.gauge(
:batched_migration_job_interval_seconds,
'Interval for a batched migration job'
),
gauge_job_duration: Gitlab::Metrics.gauge(
:batched_migration_job_duration_seconds,
'Duration for a batched migration job'
),
counter_updated_tuples: Gitlab::Metrics.counter(
:batched_migration_job_updated_tuples_total,
'Number of tuples updated by batched migration job'
),
gauge_migrated_tuples: Gitlab::Metrics.gauge(
:batched_migration_migrated_tuples_total,
'Total number of tuples migrated by a batched migration'
),
histogram_timings: Gitlab::Metrics.histogram(
:batched_migration_job_query_duration_seconds,
'Query timings for a batched migration job',
{},
[0.1, 0.25, 0.5, 1, 5].freeze
),
gauge_total_tuple_count: Gitlab::Metrics.gauge(
:batched_migration_total_tuple_count,
'Total tuple count the migration needs to touch'
),
gauge_last_update_time: Gitlab::Metrics.gauge(
:batched_migration_last_update_time_seconds,
'Unix epoch time in seconds'
)
}
end
end
end
end
end

View File

@ -0,0 +1,93 @@
# frozen_string_literal: true
module Gitlab
module Database
module BackgroundMigration
class PrometheusMetrics
extend Gitlab::Utils::StrongMemoize
QUERY_TIMING_BUCKETS = [0.1, 0.25, 0.5, 1, 5].freeze
def track(job_record)
migration_record = job_record.batched_migration
base_labels = migration_record.prometheus_labels
metric_for(:gauge_batch_size).set(base_labels, job_record.batch_size)
metric_for(:gauge_sub_batch_size).set(base_labels, job_record.sub_batch_size)
metric_for(:gauge_interval).set(base_labels, job_record.batched_migration.interval)
metric_for(:gauge_job_duration).set(base_labels, (job_record.finished_at - job_record.started_at).to_i)
metric_for(:counter_updated_tuples).increment(base_labels, job_record.batch_size)
metric_for(:gauge_migrated_tuples).set(base_labels, migration_record.migrated_tuple_count)
metric_for(:gauge_total_tuple_count).set(base_labels, migration_record.total_tuple_count)
metric_for(:gauge_last_update_time).set(base_labels, Time.current.to_i)
track_timing_metrics(base_labels, job_record.metrics)
end
def self.metrics
strong_memoize(:metrics) do
{
gauge_batch_size: Gitlab::Metrics.gauge(
:batched_migration_job_batch_size,
'Batch size for a batched migration job'
),
gauge_sub_batch_size: Gitlab::Metrics.gauge(
:batched_migration_job_sub_batch_size,
'Sub-batch size for a batched migration job'
),
gauge_interval: Gitlab::Metrics.gauge(
:batched_migration_job_interval_seconds,
'Interval for a batched migration job'
),
gauge_job_duration: Gitlab::Metrics.gauge(
:batched_migration_job_duration_seconds,
'Duration for a batched migration job'
),
counter_updated_tuples: Gitlab::Metrics.counter(
:batched_migration_job_updated_tuples_total,
'Number of tuples updated by batched migration job'
),
gauge_migrated_tuples: Gitlab::Metrics.gauge(
:batched_migration_migrated_tuples_total,
'Total number of tuples migrated by a batched migration'
),
histogram_timings: Gitlab::Metrics.histogram(
:batched_migration_job_query_duration_seconds,
'Query timings for a batched migration job',
{},
QUERY_TIMING_BUCKETS
),
gauge_total_tuple_count: Gitlab::Metrics.gauge(
:batched_migration_total_tuple_count,
'Total tuple count the migration needs to touch'
),
gauge_last_update_time: Gitlab::Metrics.gauge(
:batched_migration_last_update_time_seconds,
'Unix epoch time in seconds'
)
}
end
end
private
def track_timing_metrics(base_labels, metrics)
return unless metrics && metrics['timings']
metrics['timings'].each do |key, timings|
summary = metric_for(:histogram_timings)
labels = base_labels.merge(operation: key)
timings.each do |timing|
summary.observe(labels, timing)
end
end
end
def metric_for(name)
self.class.metrics[name]
end
end
end
end
end

View File

@ -19,7 +19,8 @@ module Gitlab
PROCESSORS = [
::Gitlab::ErrorTracking::Processor::SidekiqProcessor,
::Gitlab::ErrorTracking::Processor::GrpcErrorProcessor,
::Gitlab::ErrorTracking::Processor::ContextPayloadProcessor
::Gitlab::ErrorTracking::Processor::ContextPayloadProcessor,
::Gitlab::ErrorTracking::Processor::SanitizeErrorMessageProcessor
].freeze
class << self

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
module Gitlab
module ErrorTracking
module Processor
module Concerns
module ProcessesExceptions
private
def extract_exceptions_from(event)
exceptions = if event.is_a?(Raven::Event)
event.instance_variable_get(:@interfaces)[:exception]&.values
else
event&.exception&.instance_variable_get(:@values)
end
Array.wrap(exceptions)
end
def set_exception_message(exception, message)
if exception.respond_to?(:value=)
exception.value = message
else
exception.instance_variable_set(:@value, message)
end
end
def valid_exception?(exception)
case exception
when Raven::SingleExceptionInterface, Sentry::SingleExceptionInterface
exception&.value.present?
else
false
end
end
end
end
end
end
end

View File

@ -4,6 +4,8 @@ module Gitlab
module ErrorTracking
module Processor
module GrpcErrorProcessor
extend Gitlab::ErrorTracking::Processor::Concerns::ProcessesExceptions
DEBUG_ERROR_STRING_REGEX = RE2('(.*) debug_error_string:(.*)')
class << self
@ -19,9 +21,6 @@ module Gitlab
def process_first_exception_value(event)
# Better in new version, will be event.exception.values
exceptions = extract_exceptions_from(event)
return unless exceptions.is_a?(Array)
exception = exceptions.first
return unless valid_exception?(exception)
@ -39,11 +38,7 @@ module Gitlab
exceptions.each do |exception|
next unless valid_exception?(exception)
if exception.respond_to?(:value=)
exception.value = message
else
exception.instance_variable_set(:@value, message)
end
set_exception_message(exception, message)
end
end
@ -59,16 +54,6 @@ module Gitlab
fingerprint[1] = message if message
end
private
def extract_exceptions_from(event)
if event.is_a?(Raven::Event)
event.instance_variable_get(:@interfaces)[:exception]&.values
else
event.exception&.instance_variable_get(:@values)
end
end
def custom_grpc_fingerprint?(fingerprint)
fingerprint.is_a?(Array) && fingerprint.length == 2 && fingerprint[0].start_with?('GRPC::')
end
@ -82,15 +67,6 @@ module Gitlab
[match[1], match[2]]
end
def valid_exception?(exception)
case exception
when Raven::SingleExceptionInterface, Sentry::SingleExceptionInterface
exception&.value
else
false
end
end
end
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Gitlab
module ErrorTracking
module Processor
module SanitizeErrorMessageProcessor
extend Gitlab::ErrorTracking::Processor::Concerns::ProcessesExceptions
class << self
def call(event)
exceptions = extract_exceptions_from(event)
exceptions.each do |exception|
next unless valid_exception?(exception)
message = Gitlab::Sanitizers::ExceptionMessage.clean(exception.type, exception.value)
set_exception_message(exception, message)
end
event
end
end
end
end
end
end

View File

@ -10,7 +10,7 @@ module Gitlab
# Use periods to flatten the fields.
payload.merge!(
'exception.class' => exception.class.name,
'exception.message' => exception.message
'exception.message' => sanitize_message(exception)
)
if exception.backtrace
@ -38,6 +38,10 @@ module Gitlab
rescue PgQuery::ParseError
sql
end
def sanitize_message(exception)
Gitlab::Sanitizers::ExceptionMessage.clean(exception.class.name, exception.message)
end
end
end
end

View File

@ -19,9 +19,8 @@ module Gitlab
@exported_members.inject(missing_keys_tracking_hash) do |hash, member|
if member['user']
old_user_id = member['user']['id']
old_user_email = member.dig('user', 'public_email') || member.dig('user', 'email')
existing_user = User.find_by(find_user_query(old_user_email)) if old_user_email
hash[old_user_id] = existing_user.id if existing_user && add_team_member(member, existing_user)
existing_user_id = existing_users_email_map[get_email(member)]
hash[old_user_id] = existing_user_id if existing_user_id && add_team_member(member, existing_user_id)
else
add_team_member(member)
end
@ -72,11 +71,45 @@ module Gitlab
member&.user == @user && member.access_level >= highest_access_level
end
def add_team_member(member, existing_user = nil)
return true if existing_user && @importable.members.exists?(user_id: existing_user.id)
# Returns {email => user_id} hash where user_id is an ID at current instance
def existing_users_email_map
@existing_users_email_map ||= begin
emails = @exported_members.map { |member| get_email(member) }
User.by_user_email(emails).pluck(:email, :id).to_h
end
end
# Returns {user_id => email} hash where user_id is an ID at source "old" instance
def exported_members_email_map
@exported_members_email_map ||= begin
result = {}
@exported_members.each do |member|
email = get_email(member)
next unless email
result[member.dig('user', 'id')] = email
end
result
end
end
def get_email(member_data)
return unless member_data['user']
member_data.dig('user', 'public_email') || member_data.dig('user', 'email')
end
def add_team_member(member, existing_user_id = nil)
return true if existing_user_id && @importable.members.exists?(user_id: existing_user_id)
member['user'] = existing_user
member_hash = member_hash(member)
if existing_user_id
member_hash.delete('user')
member_hash['user_id'] = existing_user_id
end
member = relation_class.create(member_hash)
@ -92,11 +125,19 @@ module Gitlab
end
def member_hash(member)
parsed_hash(member).merge(
result = parsed_hash(member).merge(
'source_id' => @importable.id,
'importing' => true,
'access_level' => [member['access_level'], highest_access_level].min
).except('user_id')
if result['created_by_id']
created_by_email = exported_members_email_map[result['created_by_id']]
result['created_by_id'] = existing_users_email_map[created_by_email]
end
result
end
def parsed_hash(member)
@ -104,14 +145,6 @@ module Gitlab
relation_class: relation_class)
end
def find_user_query(email)
user_arel[:email].eq(email)
end
def user_arel
@user_arel ||= User.arel_table
end
def relation_class
case @importable
when ::Project
@ -143,7 +176,7 @@ module Gitlab
def base_log_params(member_hash)
{
user_id: member_hash['user']&.id,
user_id: member_hash['user_id'],
access_level: member_hash['access_level'],
importable_type: @importable.class.to_s,
importable_id: @importable.id,

View File

@ -1,14 +0,0 @@
# frozen_string_literal: true
# This module is used to return fake strong password for tests
module Gitlab
module Password
DEFAULT_LENGTH = 12
TEST_DEFAULT = "123qweQWE!@#" + "0" * (User.password_length.max - DEFAULT_LENGTH)
def self.test_default(length = 12)
password_length = [[User.password_length.min, length].max, User.password_length.max].min
TEST_DEFAULT[...password_length]
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Gitlab
module Sanitizers
module ExceptionMessage
FILTERED_STRING = '[FILTERED]'
EXCEPTION_NAMES = %w(URI::InvalidURIError Addressable::URI::InvalidURIError).freeze
MESSAGE_REGEX = %r{(\A[^:]+:\s).*\Z}.freeze
class << self
def clean(exception_name, message)
return message unless exception_name.in?(EXCEPTION_NAMES)
message.sub(MESSAGE_REGEX, '\1' + FILTERED_STRING)
end
end
end
end
end

View File

@ -125,7 +125,7 @@ class GroupSeeder
name: FFaker::Name.name,
email: FFaker::Internet.email,
confirmed_at: DateTime.now,
password: Gitlab::Password.test_default
password: Devise.friendly_token
)
end

View File

@ -10829,43 +10829,25 @@ msgstr ""
msgid "Critical vulnerabilities present"
msgstr ""
msgid "Crm|Contact has been added"
msgid "Crm|Contact has been added."
msgstr ""
msgid "Crm|Contact has been updated"
msgid "Crm|Contact has been updated."
msgstr ""
msgid "Crm|Create new contact"
msgid "Crm|Customer relations contacts"
msgstr ""
msgid "Crm|Create organization"
msgid "Crm|Customer relations organizations"
msgstr ""
msgid "Crm|Customer Relations Contacts"
msgstr ""
msgid "Crm|Customer Relations Organizations"
msgstr ""
msgid "Crm|Default rate (optional)"
msgstr ""
msgid "Crm|Description (optional)"
msgid "Crm|Default rate"
msgstr ""
msgid "Crm|Edit contact"
msgstr ""
msgid "Crm|Email"
msgstr ""
msgid "Crm|First name"
msgstr ""
msgid "Crm|Last name"
msgstr ""
msgid "Crm|New Organization"
msgid "Crm|Edit organization"
msgstr ""
msgid "Crm|New contact"
@ -10880,10 +10862,10 @@ msgstr ""
msgid "Crm|No organizations found"
msgstr ""
msgid "Crm|Organization has been added"
msgid "Crm|Organization has been added."
msgstr ""
msgid "Crm|Phone number (optional)"
msgid "Crm|Organization has been updated."
msgstr ""
msgid "Cron Timezone"
@ -10994,18 +10976,18 @@ msgstr ""
msgid "Custom range (UTC)"
msgstr ""
msgid "Customer Relations Contacts"
msgstr ""
msgid "Customer Relations Organizations"
msgstr ""
msgid "Customer experience improvement and third-party offers"
msgstr ""
msgid "Customer relations"
msgstr ""
msgid "Customer relations contacts"
msgstr ""
msgid "Customer relations organizations"
msgstr ""
msgid "Customize CI/CD settings, including Auto DevOps, shared runners, and job artifacts."
msgstr ""
@ -13223,6 +13205,9 @@ msgstr ""
msgid "Dismissed on pipeline %{pipelineLink} at %{projectLink}"
msgstr ""
msgid "Display"
msgstr ""
msgid "Display alerts from all configured monitoring tools."
msgstr ""
@ -27269,6 +27254,9 @@ msgstr ""
msgid "Phabricator Tasks"
msgstr ""
msgid "Phone"
msgstr ""
msgid "Pick a name"
msgstr ""

View File

@ -170,7 +170,7 @@
"sortablejs": "^1.10.2",
"string-hash": "1.1.3",
"style-loader": "^2.0.0",
"swagger-ui-dist": "^3.52.3",
"swagger-ui-dist": "4.8.0",
"three": "^0.84.0",
"three-orbit-controls": "^82.1.0",
"three-stl-loader": "^1.0.4",

View File

@ -612,8 +612,8 @@ RSpec.describe Admin::UsersController do
end
context 'when the new password does not match the password confirmation' do
let(:password) { Gitlab::Password.test_default }
let(:password_confirmation) { "not" + Gitlab::Password.test_default }
let(:password) { 'some_password' }
let(:password_confirmation) { 'not_same_as_password' }
it 'shows the edit page again' do
update_password(user, password, password_confirmation)

View File

@ -186,6 +186,7 @@ RSpec.describe Projects::MergeRequests::CreationsController do
it 'fetches the commit if a user has access' do
expect(Ability).to receive(:allowed?).with(user, :read_project, project) { true }
expect(Ability).to receive(:allowed?).with(user, :create_merge_request_in, project) { true }.at_least(:once)
get :branch_to,
params: {
@ -199,8 +200,25 @@ RSpec.describe Projects::MergeRequests::CreationsController do
expect(response).to have_gitlab_http_status(:ok)
end
it 'does not load the commit when the user cannot create_merge_request_in' do
expect(Ability).to receive(:allowed?).with(user, :read_project, project) { true }
expect(Ability).to receive(:allowed?).with(user, :create_merge_request_in, project) { false }.at_least(:once)
get :branch_to,
params: {
namespace_id: fork_project.namespace,
project_id: fork_project,
target_project_id: project.id,
ref: 'master'
}
expect(assigns(:commit)).to be_nil
expect(response).to have_gitlab_http_status(:ok)
end
it 'does not load the commit when the user cannot read the project' do
expect(Ability).to receive(:allowed?).with(user, :read_project, project) { false }
expect(Ability).to receive(:allowed?).with(user, :create_merge_request_in, project) { true }.at_least(:once)
get :branch_to,
params: {

View File

@ -177,6 +177,7 @@ RSpec.describe Projects::MirrorsController do
INVALID
git@example.com:foo/bar.git
ssh://git@example.com:foo/bar.git
ssh://127.0.0.1/foo/bar.git
].each do |url|
it "returns an error with a 400 response for URL #{url.inspect}" do
do_get(project, url)

View File

@ -521,7 +521,7 @@ RSpec.describe RegistrationsController do
end
it 'succeeds if password is confirmed' do
post :destroy, params: { password: Gitlab::Password.test_default }
post :destroy, params: { password: '12345678' }
expect_success
end
@ -562,7 +562,7 @@ RSpec.describe RegistrationsController do
end
it 'fails' do
delete :destroy, params: { password: Gitlab::Password.test_default }
delete :destroy, params: { password: '12345678' }
expect_failure(s_('Profiles|You must transfer ownership or delete groups you are an owner of before you can delete your account'))
end

View File

@ -5,7 +5,7 @@ FactoryBot.define do
email { generate(:email) }
name { generate(:name) }
username { generate(:username) }
password { Gitlab::Password.test_default }
password { "12345678" }
role { 'software_developer' }
confirmed_at { Time.now }
confirmation_token { nil }
@ -27,6 +27,10 @@ FactoryBot.define do
after(:build) { |user, _| user.block! }
end
trait :disallowed_password do
password { User::DISALLOWED_PASSWORDS.first }
end
trait :blocked_pending_approval do
after(:build) { |user, _| user.block_pending_approval! }
end

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'kroki rendering', :js do
let_it_be(:project) { create(:project, :public) }
before do
stub_application_setting(kroki_enabled: true, kroki_url: 'http://localhost:8000')
end
it 'shows kroki image' do
plain_text = 'This text length is ignored. ' * 300
description = <<~KROKI
#{plain_text}
```plantuml
A -> A: T
```
KROKI
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
within('.description') do
expect(page).to have_css('img')
expect(page).not_to have_text 'Warning: Displaying this diagram might cause performance issues on this page.'
end
end
it 'hides kroki image and shows warning alert when kroki source size is large' do
plantuml_text = 'A -> A: T ' * 300
description = <<~KROKI
```plantuml
#{plantuml_text}
```
KROKI
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
within('.description') do
expect(page).not_to have_css('img')
expect(page).to have_text 'Warning: Displaying this diagram might cause performance issues on this page.'
click_button 'Display'
expect(page).to have_css('img')
expect(page).not_to have_text 'Warning: Displaying this diagram might cause performance issues on this page.'
end
end
end

View File

@ -44,8 +44,8 @@ RSpec.describe 'Password reset' do
visit(edit_user_password_path(reset_password_token: token))
fill_in 'New password', with: "new" + Gitlab::Password.test_default
fill_in 'Confirm new password', with: "new" + Gitlab::Password.test_default
fill_in 'New password', with: 'hello1234'
fill_in 'Confirm new password', with: 'hello1234'
click_button 'Change your password'

View File

@ -29,7 +29,7 @@ RSpec.describe 'Profile account page', :js do
it 'deletes user', :js, :sidekiq_might_not_need_inline do
click_button 'Delete account'
fill_in 'password', with: Gitlab::Password.test_default
fill_in 'password', with: '12345678'
page.within '.modal' do
click_button 'Delete account'

View File

@ -39,7 +39,7 @@ RSpec.describe 'Profile > Password' do
describe 'User puts the same passwords in the field and in the confirmation' do
it 'shows a success message' do
fill_passwords(Gitlab::Password.test_default, Gitlab::Password.test_default)
fill_passwords('mypassword', 'mypassword')
page.within('[data-testid="alert-info"]') do
expect(page).to have_content('Password was successfully updated. Please sign in again.')
@ -79,7 +79,7 @@ RSpec.describe 'Profile > Password' do
end
context 'Change password' do
let(:new_password) { "new" + Gitlab::Password.test_default }
let(:new_password) { '22233344' }
before do
sign_in(user)
@ -170,8 +170,8 @@ RSpec.describe 'Profile > Password' do
expect(page).to have_current_path new_profile_password_path, ignore_query: true
fill_in :user_password, with: user.password
fill_in :user_new_password, with: Gitlab::Password.test_default
fill_in :user_password_confirmation, with: Gitlab::Password.test_default
fill_in :user_new_password, with: '12345678'
fill_in :user_password_confirmation, with: '12345678'
click_button 'Set new password'
expect(page).to have_current_path new_user_session_path, ignore_query: true

View File

@ -948,6 +948,53 @@ RSpec.describe 'File blob', :js do
end
end
end
context 'openapi.yml' do
before do
file_name = 'openapi.yml'
create_file(file_name, '
swagger: \'2.0\'
info:
title: Classic API Resource Documentation
description: |
<div class="foo-bar" style="background-color: red;" data-foo-bar="baz">
<h1>Swagger API documentation</h1>
</div>
version: production
basePath: /JSSResource/
produces:
- application/xml
- application/json
consumes:
- application/xml
- application/json
security:
- basicAuth: []
paths:
/accounts:
get:
responses:
\'200\':
description: No response was specified
tags:
- accounts
operationId: findAccounts
summary: Finds all accounts
')
visit_blob(file_name, useUnsafeMarkdown: '1')
click_button('Display rendered file')
wait_for_requests
end
it 'removes `style`, `class`, and `data-*`` attributes from HTML' do
expect(page).to have_css('h1', text: 'Swagger API documentation')
expect(page).not_to have_css('.foo-bar')
expect(page).not_to have_css('[style="background-color: red;"]')
expect(page).not_to have_css('[data-foo-bar="baz"]')
end
end
end
context 'realtime pipelines' do

View File

@ -1050,6 +1050,53 @@ RSpec.describe 'File blob', :js do
end
end
end
context 'openapi.yml' do
before do
file_name = 'openapi.yml'
create_file(file_name, '
swagger: \'2.0\'
info:
title: Classic API Resource Documentation
description: |
<div class="foo-bar" style="background-color: red;" data-foo-bar="baz">
<h1>Swagger API documentation</h1>
</div>
version: production
basePath: /JSSResource/
produces:
- application/xml
- application/json
consumes:
- application/xml
- application/json
security:
- basicAuth: []
paths:
/accounts:
get:
responses:
\'200\':
description: No response was specified
tags:
- accounts
operationId: findAccounts
summary: Finds all accounts
')
visit_blob(file_name, useUnsafeMarkdown: '1')
click_button('Display rendered file')
wait_for_requests
end
it 'removes `style`, `class`, and `data-*`` attributes from HTML' do
expect(page).to have_css('h1', text: 'Swagger API documentation')
expect(page).not_to have_css('.foo-bar')
expect(page).not_to have_css('[style="background-color: red;"]')
expect(page).not_to have_css('[data-foo-bar="baz"]')
end
end
end
context 'realtime pipelines' do

View File

@ -9,7 +9,7 @@ RSpec.describe 'Session TTLs', :clean_gitlab_redis_shared_state do
visit new_user_session_path
# The session key only gets created after a post
fill_in 'user_login', with: 'non-existant@gitlab.org'
fill_in 'user_password', with: Gitlab::Password.test_default
fill_in 'user_password', with: '12345678'
click_button 'Sign in'
expect(page).to have_content('Invalid login or password')

View File

@ -49,15 +49,15 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
expect(page).to have_current_path edit_user_password_path, ignore_query: true
expect(page).to have_content('Please create a password for your new account.')
fill_in 'user_password', with: Gitlab::Password.test_default
fill_in 'user_password_confirmation', with: Gitlab::Password.test_default
fill_in 'user_password', with: 'password'
fill_in 'user_password_confirmation', with: 'password'
click_button 'Change your password'
expect(page).to have_current_path new_user_session_path, ignore_query: true
expect(page).to have_content(I18n.t('devise.passwords.updated_not_active'))
fill_in 'user_login', with: user.username
fill_in 'user_password', with: Gitlab::Password.test_default
fill_in 'user_password', with: 'password'
click_button 'Sign in'
expect_single_session_with_authenticated_ttl
@ -150,6 +150,27 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
end
end
describe 'with a disallowed password' do
let(:user) { create(:user, :disallowed_password) }
before do
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
.and increment(:user_password_invalid_counter)
end
it 'disallows login' do
gitlab_sign_in(user, password: user.password)
expect(page).to have_content('Invalid login or password.')
end
it 'does not update Devise trackable attributes' do
expect { gitlab_sign_in(user, password: user.password) }
.not_to change { User.ghost.reload.sign_in_count }
end
end
describe 'with the ghost user' do
it 'disallows login' do
expect(authentication_metrics)
@ -210,7 +231,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
end
it 'does not allow sign-in if the user password is updated before entering a one-time code' do
user.update!(password: "new" + Gitlab::Password.test_default)
user.update!(password: 'new_password')
enter_code(user.current_otp)
@ -447,7 +468,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
fill_in 'user_password', with: Gitlab::Password.test_default
fill_in 'user_password', with: '12345678'
click_button 'Sign in'
expect(page).to have_current_path(new_profile_password_path, ignore_query: true)
@ -456,7 +477,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
end
context 'with invalid username and password' do
let(:user) { create(:user, password: "not" + Gitlab::Password.test_default) }
let(:user) { create(:user, password: 'not-the-default') }
it 'blocks invalid login' do
expect(authentication_metrics)
@ -767,7 +788,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
fill_in 'user_password', with: Gitlab::Password.test_default
fill_in 'user_password', with: '12345678'
click_button 'Sign in'
@ -788,7 +809,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
fill_in 'user_password', with: Gitlab::Password.test_default
fill_in 'user_password', with: '12345678'
click_button 'Sign in'
@ -810,7 +831,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
fill_in 'user_password', with: Gitlab::Password.test_default
fill_in 'user_password', with: '12345678'
click_button 'Sign in'
@ -845,7 +866,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
fill_in 'user_password', with: Gitlab::Password.test_default
fill_in 'user_password', with: '12345678'
click_button 'Sign in'
fill_in 'user_otp_attempt', with: user.reload.current_otp
@ -871,7 +892,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
fill_in 'user_password', with: Gitlab::Password.test_default
fill_in 'user_password', with: '12345678'
click_button 'Sign in'
expect_to_be_on_terms_page
@ -879,7 +900,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
expect(page).to have_current_path(new_profile_password_path, ignore_query: true)
fill_in 'user_password', with: Gitlab::Password.test_default
fill_in 'user_password', with: '12345678'
fill_in 'user_new_password', with: 'new password'
fill_in 'user_password_confirmation', with: 'new password'
click_button 'Set new password'

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