diff --git a/app/assets/javascripts/clusters_list/constants.js b/app/assets/javascripts/clusters_list/constants.js index 5cf6fd050a1..4d019708198 100644 --- a/app/assets/javascripts/clusters_list/constants.js +++ b/app/assets/javascripts/clusters_list/constants.js @@ -75,14 +75,14 @@ export const I18N_AGENT_TABLE = { neverConnectedText: s__('ClusterAgents|Never'), versionMismatchTitle: s__('ClusterAgents|Agent version mismatch'), versionMismatchText: s__( - "ClusterAgents|The Agent version do not match each other across your cluster's pods. This can happen when a new Agent version was just deployed and Kubernetes is shutting down the old pods.", + "ClusterAgents|The agent version do not match each other across your cluster's pods. This can happen when a new agent version was just deployed and Kubernetes is shutting down the old pods.", ), versionOutdatedTitle: s__('ClusterAgents|Agent version update required'), versionOutdatedText: s__( - 'ClusterAgents|Your Agent version is out of sync with your GitLab version (v%{version}), which might cause compatibility problems. Update the Agent installed on your cluster to the most recent version.', + 'ClusterAgents|Your agent version is out of sync with your GitLab version (v%{version}), which might cause compatibility problems. Update the agent installed on your cluster to the most recent version.', ), versionMismatchOutdatedTitle: s__('ClusterAgents|Agent version mismatch and update'), - viewDocsText: s__('ClusterAgents|How to update the Agent?'), + viewDocsText: s__('ClusterAgents|How to update an agent?'), }; export const I18N_AGENT_MODAL = { @@ -91,7 +91,7 @@ export const I18N_AGENT_MODAL = { close: __('Close'), cancel: __('Cancel'), - modalTitle: s__('ClusterAgents|Connect a cluster through the Agent'), + modalTitle: s__('ClusterAgents|Connect a cluster through an agent'), selectAgentTitle: s__('ClusterAgents|Select an agent to register with GitLab'), selectAgentBody: s__( 'ClusterAgents|Register an agent to generate a token that will be used to install the agent on your cluster in the next step.', @@ -125,7 +125,7 @@ export const I18N_AGENT_MODAL = { unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'), }, empty_state: { - modalTitle: s__('ClusterAgents|Connect your cluster through the Agent'), + modalTitle: s__('ClusterAgents|Connect your cluster through an agent'), modalBody: s__( "ClusterAgents|To install a new agent, first add the agent's configuration file to this repository. %{linkStart}Learn more about installing GitLab Agent.%{linkEnd}", ), @@ -197,8 +197,8 @@ export const I18N_CLUSTERS_EMPTY_STATE = { export const AGENT_CARD_INFO = { tabName: 'agent', - title: sprintf(s__('ClusterAgents|%{number} of %{total} Agents')), - emptyTitle: s__('ClusterAgents|No Agents'), + title: sprintf(s__('ClusterAgents|%{number} of %{total} agents')), + emptyTitle: s__('ClusterAgents|No agents'), tooltip: { label: s__('ClusterAgents|Recommended'), title: s__('ClusterAgents|GitLab Agent'), @@ -209,7 +209,7 @@ export const AGENT_CARD_INFO = { ), link: helpPagePath('user/clusters/agent/index'), }, - actionText: s__('ClusterAgents|Install new Agent'), + actionText: s__('ClusterAgents|Install a new agent'), footerText: sprintf(s__('ClusterAgents|View all %{number} agents')), installAgentDisabledHint: s__( 'ClusterAgents|Requires a Maintainer or greater role to install new agents', @@ -253,7 +253,7 @@ export const CLUSTERS_TABS = [ export const CLUSTERS_ACTIONS = { actionsButton: s__('ClusterAgents|Actions'), createNewCluster: s__('ClusterAgents|Create a new cluster'), - connectWithAgent: s__('ClusterAgents|Connect with Agent'), + connectWithAgent: s__('ClusterAgents|Connect with an agent'), connectExistingCluster: s__('ClusterAgents|Connect with a certificate'), agent: s__('ClusterAgents|Agent'), certificate: s__('ClusterAgents|Certificate'), diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js index 1fc05c434ca..e0995a5974c 100644 --- a/app/assets/javascripts/content_editor/services/content_editor.js +++ b/app/assets/javascripts/content_editor/services/content_editor.js @@ -1,9 +1,12 @@ +import { TextSelection } from 'prosemirror-state'; import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants'; + /* eslint-disable no-underscore-dangle */ export class ContentEditor { - constructor({ tiptapEditor, serializer, eventHub }) { + constructor({ tiptapEditor, serializer, deserializer, eventHub }) { this._tiptapEditor = tiptapEditor; this._serializer = serializer; + this._deserializer = deserializer; this._eventHub = eventHub; } @@ -31,15 +34,22 @@ export class ContentEditor { } async setSerializedContent(serializedContent) { - const { _tiptapEditor: editor, _serializer: serializer, _eventHub: eventHub } = this; + const { _tiptapEditor: editor, _deserializer: deserializer, _eventHub: eventHub } = this; + const { doc, tr } = editor.state; + const selection = TextSelection.create(doc, 0, doc.content.size); try { eventHub.$emit(LOADING_CONTENT_EVENT); - const document = await serializer.deserialize({ + const newDoc = await deserializer.deserialize({ schema: editor.schema, content: serializedContent, }); - editor.commands.setContent(document); + if (newDoc) { + tr.setSelection(selection) + .replaceSelectionWith(newDoc, false) + .setMeta('preventUpdate', true); + editor.view.dispatch(tr); + } eventHub.$emit(LOADING_SUCCESS_EVENT); } catch (e) { eventHub.$emit(LOADING_ERROR_EVENT, e); diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js index e26fc5ca8c9..354244b85ff 100644 --- a/app/assets/javascripts/content_editor/services/create_content_editor.js +++ b/app/assets/javascripts/content_editor/services/create_content_editor.js @@ -55,6 +55,7 @@ import Video from '../extensions/video'; import WordBreak from '../extensions/word_break'; import { ContentEditor } from './content_editor'; import createMarkdownSerializer from './markdown_serializer'; +import createMarkdownDeserializer from './markdown_deserializer'; import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts'; const createTiptapEditor = ({ extensions = [], ...options } = {}) => @@ -138,7 +139,8 @@ export const createContentEditor = ({ const allExtensions = [...builtInContentEditorExtensions, ...extensions]; const trackedExtensions = allExtensions.map(trackInputRulesAndShortcuts); const tiptapEditor = createTiptapEditor({ extensions: trackedExtensions, ...tiptapOptions }); - const serializer = createMarkdownSerializer({ render: renderMarkdown, serializerConfig }); + const serializer = createMarkdownSerializer({ serializerConfig }); + const deserializer = createMarkdownDeserializer({ render: renderMarkdown }); - return new ContentEditor({ tiptapEditor, serializer, eventHub }); + return new ContentEditor({ tiptapEditor, serializer, eventHub, deserializer }); }; diff --git a/app/assets/javascripts/content_editor/services/markdown_deserializer.js b/app/assets/javascripts/content_editor/services/markdown_deserializer.js new file mode 100644 index 00000000000..ccffcd4cee8 --- /dev/null +++ b/app/assets/javascripts/content_editor/services/markdown_deserializer.js @@ -0,0 +1,27 @@ +import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model'; + +export default ({ render }) => { + /** + * Converts a Markdown string into a ProseMirror JSONDocument based + * on a ProseMirror schema. + * @param {ProseMirror.Schema} params.schema A ProseMirror schema that defines + * the types of content supported in the document + * @param {String} params.content An arbitrary markdown string + * @returns A ProseMirror JSONDocument + */ + return { + deserialize: async ({ schema, content }) => { + const html = await render(content); + + if (!html) return null; + + const parser = new DOMParser(); + const { body } = parser.parseFromString(html, 'text/html'); + + // append original source as a comment that nodes can access + body.append(document.createComment(content)); + + return ProseMirrorDOMParser.fromSchema(schema).parse(body); + }, + }; +}; diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js index 925b411e51c..eaaf69c3068 100644 --- a/app/assets/javascripts/content_editor/services/markdown_serializer.js +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -1,4 +1,3 @@ -import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model'; import { MarkdownSerializer as ProseMirrorMarkdownSerializer, defaultMarkdownSerializer, @@ -237,31 +236,7 @@ const defaultSerializerConfig = { * that parses the Markdown and converts it into HTML. * @returns a markdown serializer */ -export default ({ render = () => null, serializerConfig = {} } = {}) => ({ - /** - * Converts a Markdown string into a ProseMirror JSONDocument based - * on a ProseMirror schema. - * @param {ProseMirror.Schema} params.schema A ProseMirror schema that defines - * the types of content supported in the document - * @param {String} params.content An arbitrary markdown string - * @returns A ProseMirror JSONDocument - */ - deserialize: async ({ schema, content }) => { - const html = await render(content); - - if (!html) return null; - - const parser = new DOMParser(); - const { body } = parser.parseFromString(html, 'text/html'); - - // append original source as a comment that nodes can access - body.append(document.createComment(content)); - - const state = ProseMirrorDOMParser.fromSchema(schema).parse(body); - - return state.toJSON(); - }, - +export default ({ serializerConfig = {} } = {}) => ({ /** * Converts a ProseMirror JSONDocument based * on a ProseMirror schema into Markdown diff --git a/config/feature_flags/development/fips_mode.yml b/config/feature_flags/development/fips_mode.yml new file mode 100644 index 00000000000..cade948b886 --- /dev/null +++ b/config/feature_flags/development/fips_mode.yml @@ -0,0 +1,8 @@ +--- +name: fips_mode +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81418/diffs?view=inline +rollout_issue_url: +milestone: '14.9' +type: development +group: group::source code +default_enabled: false diff --git a/config/initializers_before_autoloader/000_inflections.rb b/config/initializers_before_autoloader/000_inflections.rb index 876ae5da230..64686bdd962 100644 --- a/config/initializers_before_autoloader/000_inflections.rb +++ b/config/initializers_before_autoloader/000_inflections.rb @@ -40,4 +40,5 @@ ActiveSupport::Inflector.inflections do |inflect| inflect.acronym 'JH' inflect.acronym 'CSP' inflect.acronym 'VSCode' + inflect.acronym 'FIPS' end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 69f375bcb75..9ee7202235f 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1858,6 +1858,7 @@ Input type: `DastSiteProfileCreateInput` | `fullPath` | [`ID!`](#id) | Project the site profile belongs to. | | `profileName` | [`String!`](#string) | Name of the site profile. | | `requestHeaders` | [`String`](#string) | Comma-separated list of request header names and values to be added to every request made by DAST. | +| `scanMethod` | [`DastScanMethodType`](#dastscanmethodtype) | Scan method by the scanner. Is not saved or updated if `dast_api_scanner` feature flag is disabled. | | `targetType` | [`DastTargetTypeEnum`](#dasttargettypeenum) | Type of target to be scanned. | | `targetUrl` | [`String`](#string) | URL of the target to be scanned. | @@ -1903,6 +1904,7 @@ Input type: `DastSiteProfileUpdateInput` | `id` | [`DastSiteProfileID!`](#dastsiteprofileid) | ID of the site profile to be updated. | | `profileName` | [`String!`](#string) | Name of the site profile. | | `requestHeaders` | [`String`](#string) | Comma-separated list of request header names and values to be added to every request made by DAST. | +| `scanMethod` | [`DastScanMethodType`](#dastscanmethodtype) | Scan method by the scanner. Is not saved or updated if `dast_api_scanner` feature flag is disabled. | | `targetType` | [`DastTargetTypeEnum`](#dasttargettypeenum) | Type of target to be scanned. | | `targetUrl` | [`String`](#string) | URL of the target to be scanned. | @@ -9861,6 +9863,7 @@ Represents a DAST Site Profile. | `profileName` | [`String`](#string) | Name of the site profile. | | `referencedInSecurityPolicies` | [`[String!]`](#string) | List of security policy names that are referencing given project. | | `requestHeaders` | [`String`](#string) | Comma-separated list of request header names and values to be added to every request made by DAST. | +| `scanMethod` | [`DastScanMethodType`](#dastscanmethodtype) | Scan method used by the scanner. Always returns `null` if `dast_api_scanner` feature flag is disabled. | | `targetType` | [`DastTargetTypeEnum`](#dasttargettypeenum) | Type of target to be scanned. | | `targetUrl` | [`String`](#string) | URL of the target to be scanned. | | `userPermissions` | [`DastSiteProfilePermissions!`](#dastsiteprofilepermissions) | Permissions for the current user on the resource. | @@ -17241,6 +17244,17 @@ Unit for the duration of Dast Profile Cadence. | `WEEK` | DAST Profile Cadence duration in weeks. | | `YEAR` | DAST Profile Cadence duration in years. | +### `DastScanMethodType` + +Scan method to be used by the scanner. + +| Value | Description | +| ----- | ----------- | +| `HAR` | HAR scan method. | +| `OPENAPI` | OpenAPI scan method. | +| `POSTMAN_COLLECTION` | Postman scan method. | +| `WEBSITE` | Website scan method. | + ### `DastScanTypeEnum` | Value | Description | diff --git a/lib/gitlab/fips.rb b/lib/gitlab/fips.rb new file mode 100644 index 00000000000..3b3eac6e5e2 --- /dev/null +++ b/lib/gitlab/fips.rb @@ -0,0 +1,19 @@ +# rubocop: disable Naming/FileName +# frozen_string_literal: true + +module Gitlab + class FIPS + # A simple utility class for FIPS-related helpers + + class << self + # Returns whether we should be running in FIPS mode or not + # + # @return [Boolean] + def enabled? + Feature.enabled?(:fips_mode, default_enabled: :yaml) + end + end + end +end + +# rubocop: enable Naming/FileName diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2537e9e573d..689cc7e367e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7618,7 +7618,7 @@ msgstr "" msgid "ClusterAgents|%{name} successfully deleted" msgstr "" -msgid "ClusterAgents|%{number} of %{total} Agents" +msgid "ClusterAgents|%{number} of %{total} agents" msgstr "" msgid "ClusterAgents|%{number} of %{total} clusters connected through cluster certificates" @@ -7693,22 +7693,22 @@ msgstr "" msgid "ClusterAgents|Configuration" msgstr "" -msgid "ClusterAgents|Connect a cluster through the Agent" +msgid "ClusterAgents|Connect a cluster through an agent" msgstr "" msgid "ClusterAgents|Connect existing cluster" msgstr "" -msgid "ClusterAgents|Connect with Agent" +msgid "ClusterAgents|Connect with a certificate" msgstr "" -msgid "ClusterAgents|Connect with a certificate" +msgid "ClusterAgents|Connect with an agent" msgstr "" msgid "ClusterAgents|Connect with the GitLab Agent" msgstr "" -msgid "ClusterAgents|Connect your cluster through the Agent" +msgid "ClusterAgents|Connect your cluster through an agent" msgstr "" msgid "ClusterAgents|Connected" @@ -7768,10 +7768,10 @@ msgstr "" msgid "ClusterAgents|How to register an agent?" msgstr "" -msgid "ClusterAgents|How to update the Agent?" +msgid "ClusterAgents|How to update an agent?" msgstr "" -msgid "ClusterAgents|Install new Agent" +msgid "ClusterAgents|Install a new agent" msgstr "" msgid "ClusterAgents|Last connected %{timeAgo}." @@ -7798,7 +7798,7 @@ msgstr "" msgid "ClusterAgents|Never connected" msgstr "" -msgid "ClusterAgents|No Agents" +msgid "ClusterAgents|No agents" msgstr "" msgid "ClusterAgents|No clusters connected through cluster certificates" @@ -7852,15 +7852,15 @@ msgstr "" msgid "ClusterAgents|Tell us what you think" msgstr "" -msgid "ClusterAgents|The Agent version do not match each other across your cluster's pods. This can happen when a new Agent version was just deployed and Kubernetes is shutting down the old pods." -msgstr "" - msgid "ClusterAgents|The GitLab Agent provides an increased level of security when connecting Kubernetes clusters to GitLab. %{linkStart}Learn more about the GitLab Agent.%{linkEnd}" msgstr "" msgid "ClusterAgents|The agent has not been connected in a long time. There might be a connectivity issue. Last contact was %{timeAgo}." msgstr "" +msgid "ClusterAgents|The agent version do not match each other across your cluster's pods. This can happen when a new agent version was just deployed and Kubernetes is shutting down the old pods." +msgstr "" + msgid "ClusterAgents|The recommended installation method includes the token. If you want to follow the advanced installation method provided in the docs, make sure you save the token value before you close this window." msgstr "" @@ -7908,7 +7908,7 @@ msgstr "" msgid "ClusterAgents|You will need to create a token to connect to your agent" msgstr "" -msgid "ClusterAgents|Your Agent version is out of sync with your GitLab version (v%{version}), which might cause compatibility problems. Update the Agent installed on your cluster to the most recent version." +msgid "ClusterAgents|Your agent version is out of sync with your GitLab version (v%{version}), which might cause compatibility problems. Update the agent installed on your cluster to the most recent version." msgstr "" msgid "ClusterAgents|Your instance doesn't have the %{linkStart}GitLab Agent Server (KAS)%{linkEnd} set up. Ask a GitLab Administrator to install it." diff --git a/spec/features/projects/cluster_agents_spec.rb b/spec/features/projects/cluster_agents_spec.rb index d2b07bbc1de..e9162359940 100644 --- a/spec/features/projects/cluster_agents_spec.rb +++ b/spec/features/projects/cluster_agents_spec.rb @@ -27,7 +27,7 @@ RSpec.describe 'ClusterAgents', :js do end it 'displays empty state', :aggregate_failures do - expect(page).to have_content('Install new Agent') + expect(page).to have_content('Install a new agent') expect(page).to have_selector('.empty-state') end end diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js index ac4f71a80cb..324ed17cc66 100644 --- a/spec/frontend/content_editor/services/content_editor_spec.js +++ b/spec/frontend/content_editor/services/content_editor_spec.js @@ -10,6 +10,7 @@ import { createTestEditor } from '../test_utils'; describe('content_editor/services/content_editor', () => { let contentEditor; let serializer; + let deserializer; let eventHub; beforeEach(() => { @@ -17,8 +18,9 @@ describe('content_editor/services/content_editor', () => { jest.spyOn(tiptapEditor, 'destroy'); serializer = { deserialize: jest.fn() }; + deserializer = { deserialize: jest.fn() }; eventHub = eventHubFactory(); - contentEditor = new ContentEditor({ tiptapEditor, serializer, eventHub }); + contentEditor = new ContentEditor({ tiptapEditor, serializer, deserializer, eventHub }); }); describe('.dispose', () => { @@ -33,7 +35,7 @@ describe('content_editor/services/content_editor', () => { describe('when setSerializedContent succeeds', () => { beforeEach(() => { - serializer.deserialize.mockResolvedValueOnce(''); + deserializer.deserialize.mockResolvedValueOnce(''); }); it('emits loadingContent and loadingSuccess event in the eventHub', () => { @@ -54,7 +56,7 @@ describe('content_editor/services/content_editor', () => { const error = 'error'; beforeEach(() => { - serializer.deserialize.mockRejectedValueOnce(error); + deserializer.deserialize.mockRejectedValueOnce(error); }); it('emits loadingError event', async () => { diff --git a/spec/frontend/content_editor/services/markdown_deserializer_spec.js b/spec/frontend/content_editor/services/markdown_deserializer_spec.js new file mode 100644 index 00000000000..dcd5b2eea25 --- /dev/null +++ b/spec/frontend/content_editor/services/markdown_deserializer_spec.js @@ -0,0 +1,51 @@ +import createMarkdownDeserializer from '~/content_editor/services/markdown_deserializer'; +import Bold from '~/content_editor/extensions/bold'; +import { createTestEditor, createDocBuilder } from '../test_utils'; + +describe('content_editor/services/markdown_deserializer', () => { + let renderMarkdown; + let doc; + let p; + let bold; + let tiptapEditor; + + beforeEach(() => { + tiptapEditor = createTestEditor({ + extensions: [Bold], + }); + + ({ + builders: { doc, p, bold }, + } = createDocBuilder({ + tiptapEditor, + names: { + bold: { markType: Bold.name }, + }, + })); + renderMarkdown = jest.fn(); + }); + + it('transforms HTML returned by render function to a ProseMirror document', async () => { + const deserializer = createMarkdownDeserializer({ render: renderMarkdown }); + const expectedDoc = doc(p(bold('Bold text'))); + + renderMarkdown.mockResolvedValueOnce('

Bold text

'); + + const result = await deserializer.deserialize({ + content: 'content', + schema: tiptapEditor.schema, + }); + + expect(result.toJSON()).toEqual(expectedDoc.toJSON()); + }); + + describe('when the render function returns an empty value', () => { + it('also returns null', async () => { + const deserializer = createMarkdownDeserializer({ render: renderMarkdown }); + + renderMarkdown.mockResolvedValueOnce(null); + + expect(await deserializer.deserialize({ content: 'content' })).toBe(null); + }); + }); +}); diff --git a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js index 6f908f468f6..21b3d211821 100644 --- a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js +++ b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js @@ -2,7 +2,7 @@ import { Extension } from '@tiptap/core'; import BulletList from '~/content_editor/extensions/bullet_list'; import ListItem from '~/content_editor/extensions/list_item'; import Paragraph from '~/content_editor/extensions/paragraph'; -import markdownSerializer from '~/content_editor/services/markdown_serializer'; +import markdownDeserializer from '~/content_editor/services/markdown_deserializer'; import { getMarkdownSource } from '~/content_editor/services/markdown_sourcemap'; import { createTestEditor, createDocBuilder } from '../test_utils'; @@ -53,9 +53,8 @@ const { describe('content_editor/services/markdown_sourcemap', () => { it('gets markdown source for a rendered HTML element', async () => { - const deserialized = await markdownSerializer({ + const deserialized = await markdownDeserializer({ render: () => BULLET_LIST_HTML, - serializerConfig: {}, }).deserialize({ schema: tiptapEditor.schema, content: BULLET_LIST_MARKDOWN, @@ -76,6 +75,6 @@ describe('content_editor/services/markdown_sourcemap', () => { ), ); - expect(deserialized).toEqual(expected.toJSON()); + expect(deserialized.toJSON()).toEqual(expected.toJSON()); }); }); diff --git a/spec/lib/gitlab/fips_spec.rb b/spec/lib/gitlab/fips_spec.rb new file mode 100644 index 00000000000..2ede2e3adf3 --- /dev/null +++ b/spec/lib/gitlab/fips_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Gitlab::FIPS do + describe ".enabled?" do + subject { described_class.enabled? } + + context "feature flag is enabled" do + it { is_expected.to be_truthy } + end + + context "feature flag is disabled" do + before do + stub_feature_flags(fips_mode: false) + end + + it { is_expected.to be_falsey } + end + end +end