Commit 48c5b6e5 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents af2798f7 dbe2280c
......@@ -37,7 +37,6 @@ export default {
openedAgoJira: __('opened %{timeAgoString} by %{user} in Jira'),
openedAgoServiceDesk: __('opened %{timeAgoString} by %{email} via %{user}'),
},
inject: ['scopedLabelsAvailable'],
components: {
IssueAssignees,
GlLink,
......@@ -51,6 +50,7 @@ export default {
GlTooltip,
SafeHtml,
},
inject: ['scopedLabelsAvailable'],
props: {
issuable: {
type: Object,
......
......@@ -2,7 +2,6 @@
import ReplyPlaceholder from './discussion_reply_placeholder.vue';
import ResolveDiscussionButton from './discussion_resolve_button.vue';
import ResolveWithIssueButton from './discussion_resolve_with_issue_button.vue';
import JumpToNextDiscussionButton from './discussion_jump_to_next_button.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
......@@ -11,7 +10,6 @@ export default {
ReplyPlaceholder,
ResolveDiscussionButton,
ResolveWithIssueButton,
JumpToNextDiscussionButton,
},
mixins: [glFeatureFlagsMixin()],
props: {
......@@ -38,9 +36,6 @@ export default {
},
},
computed: {
hideJumpToNextUnresolvedInThreads() {
return this.glFeatures.hideJumpToNextUnresolvedInThreads;
},
resolvableNotes() {
return this.discussion.notes.filter((x) => x.resolvable);
},
......@@ -74,15 +69,5 @@ export default {
:url="resolveWithIssuePath"
/>
</div>
<div
v-if="
!hideJumpToNextUnresolvedInThreads &&
discussion.resolvable &&
shouldShowJumpToNextDiscussion
"
class="btn-group discussion-actions ml-sm-2"
>
<jump-to-next-discussion-button :from-discussion-id="discussion.id" />
</div>
</div>
</template>
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import discussionNavigation from '../mixins/discussion_navigation';
export default {
name: 'JumpToNextDiscussionButton',
components: {
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [discussionNavigation],
props: {
fromDiscussionId: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="btn-group" role="group">
<button
ref="button"
v-gl-tooltip
class="btn btn-default discussion-next-btn"
:title="s__('MergeRequests|Jump to next unresolved thread')"
data-track-event="click_button"
data-track-label="mr_next_unresolved_thread"
data-track-property="click_next_unresolved_thread"
@click="jumpToNextRelativeDiscussion(fromDiscussionId)"
>
<gl-icon name="comment-next" />
</button>
</div>
</template>
import Labels from 'ee_else_ce/labels';
document.addEventListener('DOMContentLoaded', () => new Labels());
// eslint-disable-next-line no-new
new Labels();
......@@ -33,7 +33,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:file_identifier_hash)
push_frontend_feature_flag(:batch_suggestions, @project, default_enabled: true)
push_frontend_feature_flag(:approvals_commented_by, @project, default_enabled: true)
push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, default_enabled: true)
push_frontend_feature_flag(:merge_request_widget_graphql, @project)
push_frontend_feature_flag(:unified_diff_components, @project, default_enabled: true)
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
......
......@@ -44,6 +44,7 @@
%small.badge.badge-pill= limited_counter_with_delimiter(User.without_projects)
.nav-controls
= render_if_exists 'admin/users/admin_email_users'
= render_if_exists 'admin/users/admin_export_user_permissions'
= link_to s_('AdminUsers|New user'), new_admin_user_path, class: 'btn gl-button btn-success btn-search float-right'
.filtered-search-block.row-content-block.border-top-0
......
---
title: Allow group owners and auditors to login to SSO-enforced groups without SSO
merge_request: 50199
author:
type: changed
---
name: hide_jump_to_next_unresolved_in_threads
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37873
rollout_issue_url:
milestone: '13.3'
type: development
group: group::code review
default_enabled: true
......@@ -73,7 +73,7 @@ to your needs:
![Use a `.gitlab-ci.yml` template](img/add_file_template_11_10.png)
While building your `.gitlab-ci.yml`, you can use the [CI/CD configuration visualization](yaml/visualization.md) to facilitate your writing experience.
While building your `.gitlab-ci.yml`, you can use the [CI/CD configuration visualization](pipeline_editor/index.md#visualize-ci-configuration) to facilitate your writing experience.
For a broader overview, see the [CI/CD getting started](quick_start/README.md) guide.
......
---
stage: Verify
group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference
---
# Pipeline Editor **(CORE)**
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4540) in GitLab 13.8.
> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-pipeline-editor). **(CORE ONLY)**
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
The pipeline editor is the primary place to edit the GitLab CI/CD configuration in
your `.gitlab-ci.yml` file. To access it, go to **CI/CD > Editor**.
From the pipeline editor page you can:
- [Validate](#validate-ci-configuration) your configuration syntax while editing the file.
- Do a deeper [lint](#lint-ci-configuration) of your configuration, that verifies it with any configuration
added with the [`include`](../yaml/README.md#include) keyword.
- See a [visualization](#visualize-ci-configuration) of the current configuration.
- [Commit](#commit-changes-to-ci-configuration) the changes to a specific branch.
NOTE:
You must have already [created a CI/CD configuration file](../quick_start/README.md#create-a-gitlab-ciyml-file)
to use the editor.
## Validate CI configuration
As you edit your pipeline configuration, it is continually validated against the GitLab CI/CD
pipeline schema. It checks the syntax of your CI YAML configuration, and also runs
some basic logical validations.
The result of this validation is shown at the top of the editor page. If your configuration
is invalid, a tip is shown to help you fix the problem:
![Errors in a CI configuration validation](img/pipeline_editor_validate_v13_8.png)
## Lint CI configuration
To test the validity of your GitLab CI/CD configuration before committing the changes,
you can use the CI lint tool. To access it, go to **CI/CD > Editor** and select the **Lint** tab.
This tool checks for syntax and logical errors but goes into more detail than the
automatic [validation](#validate-ci-configuration) in the editor.
The results are updated in real-time. Any changes you make to the configuration are
reflected in the CI lint. It displays the same results as the existing [CI Lint tool](../lint.md).
![Linting errors in a CI configuration](img/pipeline_editor_lint_v13_8.png)
## Visualize CI configuration
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241722) in GitLab 13.5.
> - [Moved to **CI/CD > Editor**](https://gitlab.com/gitlab-org/gitlab/-/issues/263141) in GitLab 13.7.
> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-cicd-configuration-visualization). **(CORE ONLY)**
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
To see a visualization of your `gitlab-ci.yml` configuration, navigate to **CI/CD > Editor**
and select the `visualization` tab. The visualization shows all stages and jobs.
[`needs`](../yaml/README.md#needs) relationships are displayed as lines connecting jobs together, showing the hierarchy of execution:
![CI configuration Visualization](img/ci_config_visualization_v13_7.png)
Hovering on a job highlights its `needs` relationships:
![CI configuration visualization on hover](img/ci_config_visualization_hover_v13_7.png)
If the configuration does not have any `needs` relationships, then no lines are drawn because
each job depends only on the previous stage being completed successfully.
### Enable or disable CI/CD configuration visualization **(CORE ONLY)**
CI/CD configuration visualization is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:ci_config_visualization_tab)
```
To disable it:
```ruby
Feature.disable(:ci_config_visualization_tab)
```
## Commit changes to CI configuration
The commit form appears at the bottom of each tab in the editor so you can commit
your changes at any time.
When you are satisfied with your changes, add a descriptive commit message and enter
a branch. The branch field defaults to your project's default branch.
If you enter a new branch name, the **Start a new merge request with these changes**
checkbox appears. Select it to start a new merge request after you commit the changes.
![The commit form with a new branch](img/pipeline_editor_commit_v13_8.png)
## Enable or disable pipeline editor **(CORE ONLY)**
The pipeline editor is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:ci_pipeline_editor_page)
```
To disable it:
```ruby
Feature.disable(:ci_pipeline_editor_page)
```
......@@ -129,7 +129,7 @@ The pipeline starts when the commit is committed.
- To validate your `.gitlab-ci.yml` file, use the
[CI Lint tool](../lint.md), which is available in every project.
- You can also use [CI/CD configuration visualization](../yaml/visualization.md) to
- You can also use [CI/CD configuration visualization](../pipeline_editor/index.md#visualize-ci-configuration) to
view a graphical representation of your `.gitlab-ci.yml` file.
- For the complete `.gitlab-ci.yml` syntax, see
[the `.gitlab-ci.yml` reference topic](../yaml/README.md).
......
......@@ -3058,8 +3058,6 @@ larger than the [maximum artifact size](../../user/gitlab_com/index.md#gitlab-ci
Job artifacts are only collected for successful jobs by default, and
artifacts are restored after [caches](#cache).
[Not all executors can use caches](https://docs.gitlab.com/runner/executors/#compatibility-chart).
[Read more about artifacts](../pipelines/job_artifacts.md).
#### `artifacts:paths`
......
......@@ -27,7 +27,7 @@ The scripts are grouped into **jobs**, and jobs run as part of a larger
**pipeline**. You can group multiple independent jobs into **stages** that run in a defined order.
You should organize your jobs in a sequence that suits your application and is in accordance with
the tests you wish to perform. To [visualize](visualization.md) the process, imagine
the tests you wish to perform. To [visualize](../pipeline_editor/index.md#visualize-ci-configuration) the process, imagine
the scripts you add to jobs are the same as CLI commands you run on your computer.
When you add a `.gitlab-ci.yml` file to your
......
---
stage: Verify
group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
redirect_to: '../pipeline_editor/index.md#visualize-ci-configuration'
---
# Visualize your CI/CD configuration
This document was moved to [another location](../pipeline_editor/index.md#visualize-ci-configuration).
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241722) in GitLab 13.5.
> - [Moved to **CI/CD > Editor**](https://gitlab.com/gitlab-org/gitlab/-/issues/263141) in GitLab 13.7.
> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-cicd-configuration-visualization). **(CORE ONLY)**
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
To see a visualization of your `gitlab-ci.yml` configuration, navigate to **CI/CD > Editor**
and select the `Visualization` tab. The visualization shows all stages and jobs.
[`needs`](README.md#needs) relationships are displayed as lines connecting jobs together, showing the hierarchy of execution:
![CI Config Visualization](img/ci_config_visualization_v13_7.png)
Hovering on a job highlights its `needs` relationships:
![CI Config Visualization on hover](img/ci_config_visualization_hover_v13_7.png)
If the configuration does not have any `needs` relationships, then no lines are drawn because
each job depends only on the previous stage being completed successfully.
You can only preview one `gitlab-ci.yml` file at a time. Configuration imported with
[`includes`](README.md#include) is ignored and not included in the visualization.
## Enable or disable CI/CD configuration visualization **(CORE ONLY)**
CI/CD configuration visualization is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:ci_config_visualization_tab)
```
To disable it:
```ruby
Feature.disable(:ci_config_visualization_tab)
```
<!-- This redirect file can be deleted after 2021-04-13. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
......@@ -90,27 +90,6 @@ When a link of a commit reference is found in a thread inside a merge
request, it will be automatically converted to a link in the context of the
current merge request.
### Jumping between unresolved threads (deprecated)
> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/199718) in GitLab 13.3.
> - This button's removal is behind a feature flag enabled by default.
> - For GitLab self-managed instances, GitLab administrators with access to the
[GitLab Rails console](../../administration/feature_flags.md) can opt to disable it by running
`Feature.disable(:hide_jump_to_next_unresolved_in_threads)` (for the instance) or
`Feature.disable(:hide_jump_to_next_unresolved_in_threads, Project.find(<project id>))`
(per project.) **(CORE ONLY)**
When a merge request has a large number of comments it can be difficult to track
what remains unresolved. You can jump between unresolved threads with the
Jump button next to the Reply field on a thread.
You can also use keyboard shortcuts to navigate among threads:
- Use <kbd>n</kbd> to jump to the next unresolved thread.
- Use <kbd>p</kbd> to jump to the previous unresolved thread.
!["8/9 threads resolved"](img/threads_resolved.png)
### Marking a comment or thread as resolved
You can mark a thread as resolved by clicking the **Resolve thread**
......
......@@ -81,6 +81,7 @@ Please note that the certificate [fingerprint algorithm](#additional-providers-a
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5291) in GitLab 11.8.
- [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/9255) in GitLab 11.11 with ongoing enforcement in the GitLab UI.
- [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/292811) in GitLab 13.8, with an updated timeout experience.
- [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/211962) in GitLab 13.8 with allowing group owners to not go through SSO.
With this option enabled, users must go through your group's GitLab single sign-on URL. They may also be added via SCIM, if configured. Users can't be added manually, and may only access project/group resources via the UI by signing in through the SSO URL.
......
......@@ -2,10 +2,11 @@
import { GlDropdown, GlDropdownItem, GlTab, GlTabs } from '@gitlab/ui';
import { camelCase, kebabCase } from 'lodash';
import * as Sentry from '~/sentry/wrapper';
import { s__ } from '~/locale';
import { __, s__ } from '~/locale';
import { getLocationHash } from '~/lib/utils/url_utility';
import * as cacheUtils from '../graphql/cache_utils';
import { getProfileSettings } from '../settings/profiles';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
......@@ -14,6 +15,7 @@ export default {
GlTab,
GlTabs,
},
mixins: [glFeatureFlagsMixin()],
props: {
createNewProfilePaths: {
type: Object,
......@@ -37,6 +39,7 @@ export default {
return getProfileSettings({
createNewProfilePaths,
isDastSavedScansEnabled: this.glFeatures.dastSavedScans,
});
},
tabIndex: {
......@@ -210,8 +213,8 @@ export default {
},
profilesPerPage: 10,
i18n: {
heading: s__('DastProfiles|Manage Profiles'),
newProfileDropdownLabel: s__('DastProfiles|New Profile'),
heading: s__('DastProfiles|Manage DAST scans'),
newProfileDropdownLabel: __('New'),
subHeading: s__(
'DastProfiles|Save commonly used configurations for target sites and scan specifications as profiles. Use these with an on-demand scan.',
),
......
<script>
import ProfilesList from './dast_profiles_list.vue';
import ScanTypeBadge from './dast_scan_type_badge.vue';
export default {
components: {
ProfilesList,
ScanTypeBadge,
},
};
</script>
<template>
<profiles-list v-bind="$attrs" v-on="$listeners">
<!-- eslint-disable-next-line vue/valid-v-slot -->
<template #cell(dastScannerProfile.scanType)="{ value }">
<scan-type-badge :scan-type="value" />
</template>
</profiles-list>
</template>
<script>
import { GlBadge } from '@gitlab/ui';
import {
SCAN_TYPE,
SCAN_TYPE_LABEL,
} from 'ee/security_configuration/dast_scanner_profiles/constants';
const scanTypeToBadgeVariantMap = {
[SCAN_TYPE.ACTIVE]: 'warning',
[SCAN_TYPE.PASSIVE]: 'neutral',
};
export default {
name: 'DastScanTypeBadge',
components: {
GlBadge,
},
props: {
scanType: {
type: String,
required: true,
validator: (value) => Boolean(SCAN_TYPE[value]),
},
},
computed: {
variant() {
return scanTypeToBadgeVariantMap[this.scanType];
},
label() {
return SCAN_TYPE_LABEL[this.scanType].toLowerCase();
},
},
};
</script>
<template>
<gl-badge size="sm" :variant="variant">
{{ label }}
</gl-badge>
</template>
......@@ -10,11 +10,17 @@ export default () => {
}
const {
dataset: { newDastScannerProfilePath, newDastSiteProfilePath, projectFullPath },
dataset: {
newDastSavedScanPath,
newDastScannerProfilePath,
newDastSiteProfilePath,
projectFullPath,
},
} = el;
const props = {
createNewProfilePaths: {
savedScan: newDastSavedScanPath,
scannerProfile: newDastScannerProfilePath,
siteProfile: newDastSiteProfilePath,
},
......
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query DastSavedScans($fullPath: ID!, $after: String, $before: String, $first: Int, $last: Int) {
project @client {
savedScans(after: $after, before: $before, first: $first, last: $last) {
pageInfo {
...PageInfo
}
edges {
node {
id
name
dastSiteProfile {
id
targetUrl
}
dastScannerProfile {
id
scanType
}
editPath
}
}
}
}
}
mutation dastSavedScansDelete($input: DastSavedScansDeleteInput!) {
savedScansDelete(input: $input) @client {
errors
}
}
/* eslint-disable @gitlab/require-i18n-strings */
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { range } from 'lodash';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export default new VueApollo({
defaultClient: createDefaultClient(
{},
{
assumeImmutableResults: true,
// NOTE: We currently mock some fake DAST scans while the feature is feature-flagged and the
// backend is being worked on.
// This will be cleaned up as part of https://gitlab.com/gitlab-org/gitlab/-/issues/295248.
let id = 0;
const generateFakeDastScan = () => {
id += 1;
return {
node: {
id,
name: `My daily scan #${id}`,
description: 'Tests for SQL injection',
dastSiteProfile: {
id,
targetUrl: 'http://example.com ',
__typename: 'DastSiteProfile',
},
dastScannerProfile: {
id,
scanType: Math.random() < 0.5 ? 'PASSIVE' : 'ACTIVE',
__typename: 'DastScannerProfile',
},
editPath: '/on_demand_scans/1/edit',
__typename: 'DastSavedScan',
},
),
__typename: 'DastSavedScanEdge',
};
};
const resolvers = {
Query: {
project: () => ({
__typename: 'Project',
savedScans: {
pageInfo: {
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'startCursor',
endCursor: 'endCursor',
__typename: 'PageInfo',
},
edges: range(10).map(generateFakeDastScan),
__typename: 'DastSavedScanConnection',
},
}),
},
};
export default new VueApollo({
defaultClient: createDefaultClient(resolvers, {
assumeImmutableResults: true,
}),
});
import dastSavedScansQuery from 'ee/security_configuration/dast_profiles/graphql/dast_saved_scans.query.graphql';
import dastSavedScansDelete from 'ee/security_configuration/dast_profiles/graphql/dast_saved_scans_delete.mutation.graphql';
import dastSiteProfilesQuery from 'ee/security_configuration/dast_profiles/graphql/dast_site_profiles.query.graphql';
import dastSiteProfilesDelete from 'ee/security_configuration/dast_profiles/graphql/dast_site_profiles_delete.mutation.graphql';
import dastScannerProfilesQuery from 'ee/security_configuration/dast_profiles/graphql/dast_scanner_profiles.query.graphql';
import dastScannerProfilesDelete from 'ee/security_configuration/dast_profiles/graphql/dast_scanner_profiles_delete.mutation.graphql';
import { dastProfilesDeleteResponse } from 'ee/security_configuration/dast_profiles/graphql/cache_utils';
import DastSavedScansList from 'ee/security_configuration/dast_profiles/components/dast_saved_scans_list.vue';
import DastSiteProfileList from 'ee/security_configuration/dast_profiles/components/dast_site_profiles_list.vue';
import DastScannerProfileList from 'ee/security_configuration/dast_profiles/components/dast_scanner_profiles_list.vue';
import { s__ } from '~/locale';
export const getProfileSettings = ({ createNewProfilePaths }) => ({
export const getProfileSettings = ({ createNewProfilePaths, isDastSavedScansEnabled }) => ({
...(isDastSavedScansEnabled
? {
savedScans: {
profileType: 'savedScans',
createNewProfilePath: createNewProfilePaths.savedScan,
graphQL: {
query: dastSavedScansQuery,
deletion: {
mutation: dastSavedScansDelete,
optimisticResponse: dastProfilesDeleteResponse({
mutationName: 'savedScanDelete',
payloadTypeName: 'DastSavedScanDeletePayload',
}),
},
},
component: DastSavedScansList,
tableFields: [
{
label: s__('DastProfiles|Scan'),
key: 'name',
},
{
label: s__('DastProfiles|Target'),
key: 'dastSiteProfile.targetUrl',
},
{
label: s__('DastProfiles|Scan mode'),
key: 'dastScannerProfile.scanType',
},
],
i18n: {
createNewLinkText: s__('DastProfiles|DAST Scan'),
name: s__('DastProfiles|Saved Scans'),
errorMessages: {
fetchNetworkError: s__(
'DastProfiles|Could not fetch saved scans. Please refresh the page, or try again later.',
),
deletionNetworkError: s__(
'DastProfiles|Could not delete saved scan. Please refresh the page, or try again later.',
),
deletionBackendError: s__('DastProfiles|Could not delete saved scans:'),
},
},
},
}
: {}),
siteProfiles: {
profileType: 'siteProfiles',
createNewProfilePath: createNewProfilePaths.siteProfile,
......
......@@ -34,8 +34,8 @@ export default {
ThreatMonitoringSection,
NetworkPolicyList,
},
inject: ['documentationPath'],
mixins: [glFeatureFlagsMixin()],
inject: ['documentationPath'],
props: {
defaultEnvironmentId: {
type: Number,
......
......@@ -6,6 +6,7 @@ module Projects
before_action do
authorize_read_on_demand_scans!
push_frontend_feature_flag(:security_on_demand_scans_site_validation, @project, default_enabled: :yaml)
push_frontend_feature_flag(:dast_saved_scans, @project, default_enabled: :yaml)
end
feature_category :dynamic_application_security_testing
......
......@@ -347,8 +347,9 @@ module EE
def sso_enforcement_prevents_access?
return false unless subject.persisted?
return false if user&.admin?
return false if user&.auditor?
::Gitlab::Auth::GroupSaml::SsoEnforcer.group_access_restricted?(subject)
::Gitlab::Auth::GroupSaml::SsoEnforcer.group_access_restricted?(subject, user: user)
end
# Available in Core for self-managed but only paid, non-trial for .com to prevent abuse
......
......@@ -324,7 +324,7 @@ module EE
.default_project_deletion_protection
end
rule { needs_new_sso_session & ~admin }.policy do
rule { needs_new_sso_session & ~admin & ~auditor }.policy do
prevent :guest_access
prevent :reporter_access
prevent :developer_access
......
- return unless send_emails_from_admin_area_feature_available?
= link_to s_('AdminUsers|Send email to users'), admin_email_path, class: 'btn gl-button'
= link_to admin_email_path, { class: 'gl-button btn btn-default', data: { toggle: "tooltip", placement: "top", container: "body" }, title: s_("AdminUsers|Send email to users") } do
.gl-button-icon
= sprite_icon('mail')
- return unless current_user&.can?(:export_user_permissions)
= link_to admin_user_permission_exports_path(format: :csv), { class: 'gl-button btn btn-default', data: { toggle: "tooltip", placement: "top", container: "body" }, title: s_("AdminUsers|Export permissions as CSV") } do
.gl-button-icon
= sprite_icon('upload')
- if ::Feature.enabled?(:whats_new_dropdown, current_user)
- if ::Feature.enabled?(:whats_new_drawer, current_user)
%li
%button.js-whats-new-trigger{ type: 'button', data: { storage_key: whats_new_storage_key } }
%button.gl-justify-content-space-between.gl-align-items-center.js-whats-new-trigger{ type: 'button', data: { storage_key: whats_new_storage_key }, class: 'gl-display-flex!' }
= _("See what's new at GitLab")
%span.js-whats-new-notification-count.whats-new-notification-count.gl-ml-4
%span.js-whats-new-notification-count.whats-new-notification-count
= whats_new_most_recent_release_items_count
- else
%li
......
- add_to_breadcrumbs _('Security Configuration'), project_security_configuration_path(@project)
- breadcrumb_title s_('DastProfiles|Manage profiles')
- page_title s_('DastProfiles|Manage profiles')
- breadcrumb_title s_('DastProfiles|Manage DAST scans')
- page_title s_('DastProfiles|Manage DAST scans')
.js-dast-profiles{ data: { new_dast_site_profile_path: new_project_security_configuration_dast_profiles_dast_site_profile_path(@project),
.js-dast-profiles{ data: { new_dast_saved_scan_path: new_project_on_demand_scan_path(@project),
new_dast_site_profile_path: new_project_security_configuration_dast_profiles_dast_site_profile_path(@project),
new_dast_scanner_profile_path: new_project_security_configuration_dast_profiles_dast_scanner_profile_path(@project),
project_full_path: @project.path_with_namespace } }
---
title: BUFIX - whats new dropdown text wraps on ubuntu
merge_request: 51510
author:
type: fixed
......@@ -28,13 +28,14 @@ module Gitlab
saml_enforced? && !active_session?
end
def self.group_access_restricted?(group)
def self.group_access_restricted?(group, user: nil)
return false unless group
return false unless group.root_ancestor
saml_provider = group.root_ancestor.saml_provider
return false unless saml_provider
return false if user_authorized?(user, group)
new(saml_provider).access_restricted?
end
......@@ -48,6 +49,10 @@ module Gitlab
def group
saml_provider&.group
end
def self.user_authorized?(user, group)
return true if !group.has_parent? && group.owned_by?(user)
end
end
end
end
......
......@@ -27,7 +27,7 @@ RSpec.describe "Admin::Users" do
it "shows the 'Send email to users' link" do
visit admin_users_path
expect(page).to have_link('Send email to users', href: admin_email_path)
expect(page).to have_link(href: admin_email_path)
end
end
......@@ -39,7 +39,33 @@ RSpec.describe "Admin::Users" do
it "does not show the 'Send email to users' link" do
visit admin_users_path
expect(page).not_to have_link('Send email to users', href: admin_email_path)
expect(page).not_to have_link(href: admin_email_path)
end
end
end
describe 'user permission export' do
context 'when `export_user_permissions` feature is available' do
before do
stub_licensed_features(export_user_permissions: true)
end
it "shows the 'Export Permissions' link" do
visit admin_users_path
expect(page).to have_link(href: admin_user_permission_exports_path(format: :csv))
end
end
context 'when `export_user_permissions` feature is disabled' do
before do
stub_licensed_features(export_user_permissions: false)
end
it "does not show the 'Export Permissions' link" do
visit admin_users_path
expect(page).not_to have_link(href: admin_user_permission_exports_path(format: :csv))
end
end
end
......
......@@ -5,6 +5,7 @@ import { merge } from 'lodash';
import DastProfiles from 'ee/security_configuration/dast_profiles/components/dast_profiles.vue';
import setWindowLocation from 'helpers/set_window_location_helper';
const TEST_NEW_DAST_SAVED_SCAN_PATH = '/-/on_demand_scans/new';
const TEST_NEW_DAST_SCANNER_PROFILE_PATH = '/-/on_demand_scans/scanner_profiles/new';
const TEST_NEW_DAST_SITE_PROFILE_PATH = '/-/on_demand_scans/site_profiles/new';
const TEST_PROJECT_FULL_PATH = '/namespace/project';
......@@ -15,6 +16,7 @@ describe('EE - DastProfiles', () => {
const createComponentFactory = (mountFn = shallowMount) => (options = {}) => {
const defaultProps = {
createNewProfilePaths: {
savedScan: TEST_NEW_DAST_SAVED_SCAN_PATH,
scannerProfile: TEST_NEW_DAST_SCANNER_PROFILE_PATH,
siteProfile: TEST_NEW_DAST_SITE_PROFILE_PATH,
},
......@@ -24,6 +26,9 @@ describe('EE - DastProfiles', () => {
const defaultMocks = {
$apollo: {
queries: {
savedScans: {
fetchMore: jest.fn().mockResolvedValue(),
},
siteProfiles: {
fetchMore: jest.fn().mockResolvedValue(),
},
......@@ -43,6 +48,11 @@ describe('EE - DastProfiles', () => {
{
propsData: defaultProps,
mocks: defaultMocks,
provide: {
glFeatures: {
dastSavedScans: true,
},
},
},
options,
),
......@@ -72,31 +82,26 @@ describe('EE - DastProfiles', () => {
it('shows a heading that describes the purpose of the page', () => {
createFullComponent();
const heading = withinComponent().getByRole('heading', { name: /manage profiles/i });
const heading = withinComponent().getByRole('heading', { name: /manage dast scans/i });
expect(heading).not.toBe(null);
});
it('has a "New Profile" dropdown menu', () => {
it('has a "New" dropdown menu', () => {
createComponent();
expect(getDropdownComponent().props('text')).toBe('New Profile');
expect(getDropdownComponent().props('text')).toBe('New');
});
it(`shows a "Site Profile" dropdown item that links to ${TEST_NEW_DAST_SITE_PROFILE_PATH}`, () => {
createComponent();
expect(getSiteProfilesDropdownItem('Site Profile').getAttribute('href')).toBe(
TEST_NEW_DAST_SITE_PROFILE_PATH,
);
});
it(`shows a "Scanner Profile" dropdown item that links to ${TEST_NEW_DAST_SCANNER_PROFILE_PATH}`, () => {
it.each`
itemName | href
${'DAST Scan'} | ${TEST_NEW_DAST_SAVED_SCAN_PATH}
${'Site Profile'} | ${TEST_NEW_DAST_SITE_PROFILE_PATH}
${'Scanner Profile'} | ${TEST_NEW_DAST_SCANNER_PROFILE_PATH}
`('shows a "$itemName" dropdown item that links to $href', ({ itemName, href }) => {
createComponent();
expect(getSiteProfilesDropdownItem('Scanner Profile').getAttribute('href')).toBe(
TEST_NEW_DAST_SCANNER_PROFILE_PATH,
);
expect(getSiteProfilesDropdownItem(itemName).getAttribute('href')).toBe(href);
});
});
......@@ -116,7 +121,8 @@ describe('EE - DastProfiles', () => {
it.each`
tabName | shouldBeSelectedByDefault
${'Site Profiles'} | ${true}
${'Saved Scans'} | ${true}
${'Site Profiles'} | ${false}
${'Scanner Profiles'} | ${false}
`(
'shows a "$tabName" tab which has "selected" set to "$shouldBeSelectedByDefault"',
......@@ -133,8 +139,9 @@ describe('EE - DastProfiles', () => {
describe.each`
tabName | index | givenLocationHash
${'Site Profiles'} | ${0} | ${'site-profiles'}
${'Scanner Profiles'} | ${1} | ${'scanner-profiles'}
${'Saved Scans'} | ${0} | ${'saved-scans'}
${'Site Profiles'} | ${1} | ${'site-profiles'}
${'Scanner Profiles'} | ${2} | ${'scanner-profiles'}
`('with location hash set to "$givenLocationHash"', ({ tabName, index, givenLocationHash }) => {
beforeEach(() => {
setWindowLocation(`http://foo.com/index#${givenLocationHash}`);
......@@ -166,6 +173,7 @@ describe('EE - DastProfiles', () => {
describe.each`
description | profileType
${'Saved Scans List'} | ${'savedScans'}
${'Site Profiles List'} | ${'siteProfiles'}
${'Scanner Profiles List'} | ${'scannerProfiles'}
`('$description', ({ profileType }) => {
......@@ -220,4 +228,33 @@ describe('EE - DastProfiles', () => {
expect(mutate).toHaveBeenCalledTimes(1);
});
});
describe('dastSavedScans feature flag disabled', () => {
beforeEach(() => {
createFullComponent({
provide: {
glFeatures: {
dastSavedScans: false,
},
},
});
});
it('does not show a "DAST Scan" item in the dropdown', () => {
expect(getSiteProfilesDropdownItem('DAST Scan')).toBe(null);
});
it('shows only 2 tabs', () => {
expect(withinComponent().getAllByRole('tab')).toHaveLength(2);
});
it('"Site Profile" tab should be selected by default', () => {
const tab = getTab({
tabName: 'Site Profiles',
selected: true,
});
expect(tab).not.toBe(null);
});
});
});
import { mount, shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
import Component from 'ee/security_configuration/dast_profiles/components/dast_saved_scans_list.vue';
import ProfilesList from 'ee/security_configuration/dast_profiles/components/dast_profiles_list.vue';
import { savedScans } from '../mocks/mock_data';
describe('EE - DastSavedScansList', () => {
let wrapper;
const defaultProps = {
profiles: [],
tableLabel: 'Saved scans',
fields: [
{ key: 'name' },
{ key: 'dastSiteProfile.targetUrl' },
{ key: 'dastScannerProfile.scanType' },
],
profilesPerPage: 10,
errorMessage: '',
errorDetails: [],
fullPath: '/namespace/project',
hasMoreProfilesToLoad: false,
isLoading: false,
};
const wrapperFactory = (mountFn = shallowMount) => (options = {}) => {
wrapper = mountFn(
Component,
merge(
{
propsData: defaultProps,
},
options,
),
);
};
const createFullComponent = wrapperFactory(mount);
const findProfileList = () => wrapper.find(ProfilesList);
afterEach(() => {
wrapper.destroy();
});
it('renders profile list properly', () => {
createFullComponent({
propsData: { profiles: savedScans },
});
expect(findProfileList()).toExist();
});
it('passes down the props properly', () => {
createFullComponent();
expect(findProfileList().props()).toEqual(defaultProps);
});
it('sets listeners on profile list component', () => {
const inputHandler = jest.fn();
createFullComponent({
listeners: {
input: inputHandler,
},
});
findProfileList().vm.$emit('input');
expect(inputHandler).toHaveBeenCalled();
});
});
import { shallowMount } from '@vue/test-utils';
import { GlBadge } from '@gitlab/ui';
import { SCAN_TYPE } from 'ee/security_configuration/dast_scanner_profiles/constants';
import DastScanTypeBadge from 'ee/security_configuration/dast_profiles/components/dast_scan_type_badge.vue';
describe('EE - DastScanTypeBadge', () => {
let wrapper;
const findBadge = () => wrapper.find(GlBadge);
const wrapperFactory = (mountFn = shallowMount) => (options = {}) => {
wrapper = mountFn(DastScanTypeBadge, options);
};
const createComponent = wrapperFactory();
afterEach(() => {
wrapper.destroy();
});
it.each`
scanType | variant
${SCAN_TYPE.ACTIVE} | ${'warning'}
${SCAN_TYPE.PASSIVE} | ${'neutral'}
`('renders a $variant badge for $scanType scans', ({ scanType, variant }) => {
createComponent({
propsData: {
scanType,
},
});
expect(findBadge().props('variant')).toBe(variant);
});
});
......@@ -61,3 +61,13 @@ export const scannerProfiles = [
showDebugMessages: true,
},
];
export const savedScans = [
{
id: 'gid://gitlab/DastScan/1',
name: 'Scan 1',
dastSiteProfile: siteProfiles[0],
dastScannerProfile: scannerProfiles[0],
editPath: '/1/edit',
},
];
......@@ -112,11 +112,28 @@ RSpec.describe Gitlab::Auth::GroupSaml::SsoEnforcer do
expect(described_class).to be_group_access_restricted(sub_group)
end
end
it 'for a project' do
project = create(:project, group: root_group)
context 'for group owner' do
let(:user) { create(:user) }
expect(described_class).to be_group_access_restricted(project)
before do
create(:group_saml_identity, user: user, saml_provider: root_group.saml_provider)
root_group.add_owner(user)
end
context 'for a root group' do
it 'is not restricted' do
expect(described_class).not_to be_group_access_restricted(root_group, user: user)
end
end
context 'for a subgroup' do
it 'is restricted' do
sub_group = create(:group, parent: root_group)
expect(described_class).to be_group_access_restricted(sub_group, user: user)
end
end
end
......
......@@ -464,11 +464,34 @@ RSpec.describe GroupPolicy do
context 'as a group owner' do
before do
create(:group_saml_identity, user: current_user, saml_provider: saml_provider)
group.add_owner(current_user)
end
it 'prevents access without a SAML session' do
is_expected.not_to allow_action(:read_group)
it 'allows access without a SAML session' do
is_expected.to allow_action(:read_group)
end
it 'prevents access without a SAML session for subgroup' do
subgroup = create(:group, :private, parent: group)
expect(described_class.new(current_user, subgroup)).not_to allow_action(:read_group)
end
end
context 'as an admin' do
let(:current_user) { admin }
it 'allows access without a SAML session' do
is_expected.to allow_action(:read_group)
end
end
context 'as an auditor' do
let(:current_user) { create(:user, :auditor) }
it 'allows access without a SAML session' do
is_expected.to allow_action(:read_group)
end
end
......
......@@ -358,6 +358,24 @@ RSpec.describe ProjectPolicy do
end
end
context 'as a group maintainer' do
before do
group.add_maintainer(current_user)
end
it 'prevents access without a SAML session' do
is_expected.not_to allow_action(:read_project)
end
end
context 'as an auditor' do
let(:current_user) { create(:user, :auditor) }
it 'allows access without a SAML session' do
is_expected.to allow_action(:read_project)
end
end
context 'with public access' do
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, group: saml_provider.group) }
......
......@@ -16,14 +16,14 @@ RSpec.describe 'admin/users/index' do
end
it 'includes "Send email to users" link' do
expect(rendered).to have_link 'Send email to users', href: admin_email_path
expect(rendered).to have_link href: admin_email_path
end
context 'when Gitlab::CurrentSettings.should_check_namespace_plan is true' do
let(:should_check_namespace_plan) { true }
it 'includes "Send email to users" link' do
expect(rendered).to have_link 'Send email to users', href: admin_email_path
expect(rendered).to have_link href: admin_email_path
end
end
end
......@@ -12,6 +12,10 @@ RSpec.describe "projects/security/dast_profiles/show", type: :view do
expect(rendered).to have_selector('.js-dast-profiles')
end
it 'passes new dast saved scan path' do
expect(rendered).to include '/on_demand_scans/new'
end
it 'passes new dast site profile path' do
expect(rendered).to include '/security/configuration/dast_profiles/dast_site_profiles/new'
end
......
......@@ -2215,6 +2215,9 @@ msgstr ""
msgid "AdminUsers|Delete user and contributions"
msgstr ""
msgid "AdminUsers|Export permissions as CSV"
msgstr ""
msgid "AdminUsers|External"
msgstr ""
......@@ -8797,6 +8800,12 @@ msgstr ""
msgid "DastProfiles|Could not create the site profile. Please try again."
msgstr ""
msgid "DastProfiles|Could not delete saved scan. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not delete saved scans:"
msgstr ""
msgid "DastProfiles|Could not delete scanner profile. Please refresh the page, or try again later."
msgstr ""
......@@ -8809,6 +8818,9 @@ msgstr ""
msgid "DastProfiles|Could not delete site profiles:"
msgstr ""
msgid "DastProfiles|Could not fetch saved scans. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not fetch scanner profiles. Please refresh the page, or try again later."
msgstr ""
......@@ -8821,6 +8833,9 @@ msgstr ""
msgid "DastProfiles|Could not update the site profile. Please try again."
msgstr ""
msgid "DastProfiles|DAST Scan"
msgstr ""
msgid "DastProfiles|Debug messages"
msgstr ""
......@@ -8863,7 +8878,7 @@ msgstr ""
msgid "DastProfiles|Include debug messages in the DAST console output."
msgstr ""
msgid "DastProfiles|Manage Profiles"
msgid "DastProfiles|Manage DAST scans"
msgstr ""
msgid "DastProfiles|Manage profiles"
......@@ -8875,9 +8890,6 @@ msgstr ""
msgid "DastProfiles|Minimum = 1 second, Maximum = 3600 seconds"
msgstr ""
msgid "DastProfiles|New Profile"
msgstr ""
msgid "DastProfiles|New scanner profile"
msgstr ""
......@@ -8917,6 +8929,12 @@ msgstr ""
msgid "DastProfiles|Save profile"
msgstr ""
msgid "DastProfiles|Saved Scans"
msgstr ""
msgid "DastProfiles|Scan"
msgstr ""
msgid "DastProfiles|Scan mode"
msgstr ""
......@@ -8944,6 +8962,9 @@ msgstr ""
msgid "DastProfiles|Spider timeout"
msgstr ""
msgid "DastProfiles|Target"
msgstr ""
msgid "DastProfiles|Target URL"
msgstr ""
......@@ -17658,9 +17679,6 @@ msgstr ""
msgid "MergeRequests|Failed to squash. Should be done manually."
msgstr ""
msgid "MergeRequests|Jump to next unresolved thread"
msgstr ""
msgid "MergeRequests|Reply..."
msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`JumpToNextDiscussionButton matches the snapshot 1`] = `
<div
class="btn-group"
role="group"
>
<button
class="btn btn-default discussion-next-btn"
data-track-event="click_button"
data-track-label="mr_next_unresolved_thread"
data-track-property="click_next_unresolved_thread"
title="Jump to next unresolved thread"
>
<gl-icon-stub
name="comment-next"
size="16"
/>
</button>
</div>
`;
......@@ -4,7 +4,6 @@ import DiscussionActions from '~/notes/components/discussion_actions.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import ResolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue';
import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
import JumpToNextDiscussionButton from '~/notes/components/discussion_jump_to_next_button.vue';
import createStore from '~/notes/stores';
// NOTE: clone mock_data so that it is not accidentally mutated
......@@ -21,7 +20,7 @@ const createUnallowedNote = () =>
describe('DiscussionActions', () => {
let wrapper;
const createComponentFactory = (shallow = true) => (props, options) => {
const createComponentFactory = (shallow = true) => (props) => {
const store = createStore();
const mountFn = shallow ? shallowMount : mount;
......@@ -35,11 +34,6 @@ describe('DiscussionActions', () => {
shouldShowJumpToNextDiscussion: true,
...props,
},
provide: {
glFeatures: {
hideJumpToNextUnresolvedInThreads: options?.hideJumpToNextUnresolvedInThreads,
},
},
});
};
......@@ -55,7 +49,6 @@ describe('DiscussionActions', () => {
expect(wrapper.find(ReplyPlaceholder).exists()).toBe(true);
expect(wrapper.find(ResolveDiscussionButton).exists()).toBe(true);
expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(true);
expect(wrapper.find(JumpToNextDiscussionButton).exists()).toBe(true);
});
it('only renders reply placholder if disccusion is not resolvable', () => {
......@@ -66,7 +59,6 @@ describe('DiscussionActions', () => {
expect(wrapper.find(ReplyPlaceholder).exists()).toBe(true);
expect(wrapper.find(ResolveDiscussionButton).exists()).toBe(false);
expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(false);
expect(wrapper.find(JumpToNextDiscussionButton).exists()).toBe(false);
});
it('does not render resolve with issue button if resolveWithIssuePath is falsy', () => {
......@@ -75,12 +67,6 @@ describe('DiscussionActions', () => {
expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(false);
});
it('does not render jump to next discussion button if shouldShowJumpToNextDiscussion is false', () => {
createComponent({ shouldShowJumpToNextDiscussion: false });
expect(wrapper.find(JumpToNextDiscussionButton).exists()).toBe(false);
});
describe.each`
desc | notes | shouldRender
${'with no notes'} | ${[]} | ${true}
......@@ -101,13 +87,6 @@ describe('DiscussionActions', () => {
});
});
it('does not render jump to next discussion button if feature flag is enabled', () => {
const createComponent = createComponentFactory();
createComponent({}, { hideJumpToNextUnresolvedInThreads: true });
expect(wrapper.find(JumpToNextDiscussionButton).exists()).toBe(false);
});
describe('events handling', () => {
const createComponent = createComponentFactory(false);
......
import { shallowMount } from '@vue/test-utils';
import JumpToNextDiscussionButton from '~/notes/components/discussion_jump_to_next_button.vue';
import { mockTracking } from '../../helpers/tracking_helper';
describe('JumpToNextDiscussionButton', () => {
const fromDiscussionId = 'abc123';
let wrapper;
let trackingSpy;
let jumpFn;
beforeEach(() => {
jumpFn = jest.fn();
wrapper = shallowMount(JumpToNextDiscussionButton, {
propsData: { fromDiscussionId },
});
jest.spyOn(wrapper.vm, 'jumpToNextRelativeDiscussion').mockImplementation(jumpFn);
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
});
afterEach(() => {
wrapper.destroy();
});
it('matches the snapshot', () => {
expect(wrapper.vm.$el).toMatchSnapshot();
});
it('calls jumpToNextRelativeDiscussion when clicked', () => {
wrapper.find({ ref: 'button' }).trigger('click');
expect(jumpFn).toHaveBeenCalledWith(fromDiscussionId);
});
it('sends the correct tracking event when clicked', () => {
wrapper.find({ ref: 'button' }).trigger('click');
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', {
label: 'mr_next_unresolved_thread',
property: 'click_next_unresolved_thread',
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment