Commit 612269d4 authored by Russell Dickenson's avatar Russell Dickenson

Merge branch 'whaber-master-patch-65557' into 'master'

Add DAST recommendation about the importance of authentication

See merge request gitlab-org/gitlab!46913
parents f16f05ff e1e7da41
...@@ -52,7 +52,7 @@ class Projects::RunnersController < Projects::ApplicationController ...@@ -52,7 +52,7 @@ class Projects::RunnersController < Projects::ApplicationController
end end
def toggle_shared_runners def toggle_shared_runners
if Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true) && !project.shared_runners_enabled && project.group && project.group.shared_runners_setting == 'disabled_and_unoverridable' if !project.shared_runners_enabled && project.group && project.group.shared_runners_setting == 'disabled_and_unoverridable'
return redirect_to project_runners_path(@project), alert: _("Cannot enable shared runners because parent group does not allow it") return redirect_to project_runners_path(@project), alert: _("Cannot enable shared runners because parent group does not allow it")
end end
......
# frozen_string_literal: true
module Types
class GroupInvitationType < BaseObject
expose_permissions Types::PermissionTypes::Group
authorize :read_group
implements InvitationInterface
graphql_name 'GroupInvitation'
description 'Represents a Group Invitation'
field :group, Types::GroupType, null: true,
description: 'Group that a User is invited to',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.source_id).find }
end
end
# frozen_string_literal: true
module Types
module InvitationInterface
include BaseInterface
field :email, GraphQL::STRING_TYPE, null: false,
description: 'Email of the member to invite'
field :access_level, Types::AccessLevelType, null: true,
description: 'GitLab::Access level'
field :created_by, Types::UserType, null: true,
description: 'User that authorized membership'
field :created_at, Types::TimeType, null: true,
description: 'Date and time the membership was created'
field :updated_at, Types::TimeType, null: true,
description: 'Date and time the membership was last updated'
field :expires_at, Types::TimeType, null: true,
description: 'Date and time the membership expires'
field :user, Types::UserType, null: true,
description: 'User that is associated with the member object'
definition_methods do
def resolve_type(object, context)
case object
when GroupMember
Types::GroupInvitationType
when ProjectMember
Types::ProjectInvitationType
else
raise ::Gitlab::Graphql::Errors::BaseError, "Unknown member type #{object.class.name}"
end
end
end
end
end
# frozen_string_literal: true
module Types
class ProjectInvitationType < BaseObject
graphql_name 'ProjectInvitation'
description 'Represents a Project Membership Invitation'
expose_permissions Types::PermissionTypes::Project
implements InvitationInterface
authorize :read_project
field :project, Types::ProjectType, null: true,
description: 'Project ID for the project of the invitation'
def project
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.source_id).find
end
end
end
...@@ -37,27 +37,6 @@ module FromUnion ...@@ -37,27 +37,6 @@ module FromUnion
# rubocop: disable Gitlab/Union # rubocop: disable Gitlab/Union
extend FromSetOperator extend FromSetOperator
define_set_operator Gitlab::SQL::Union define_set_operator Gitlab::SQL::Union
alias_method :from_union_set_operator, :from_union
def from_union(members, remove_duplicates: true, alias_as: table_name)
if Feature.enabled?(:sql_set_operators)
from_union_set_operator(members, remove_duplicates: remove_duplicates, alias_as: alias_as)
else
# The original from_union method.
standard_from_union(members, remove_duplicates: remove_duplicates, alias_as: alias_as)
end
end
private
def standard_from_union(members, remove_duplicates: true, alias_as: table_name)
union = Gitlab::SQL::Union
.new(members, remove_duplicates: remove_duplicates)
.to_sql
from(Arel.sql("(#{union}) #{alias_as}"))
end
# rubocop: enable Gitlab/Union # rubocop: enable Gitlab/Union
end end
end end
...@@ -96,6 +96,8 @@ class Member < ApplicationRecord ...@@ -96,6 +96,8 @@ class Member < ApplicationRecord
scope :owners, -> { active.where(access_level: OWNER) } scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) } scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
scope :with_user, -> (user) { where(user: user) } scope :with_user, -> (user) { where(user: user) }
scope :with_user_by_email, -> (email) { left_join_users.where(users: { email: email } ) }
scope :preload_user_and_notification_settings, -> { preload(user: :notification_settings) } scope :preload_user_and_notification_settings, -> { preload(user: :notification_settings) }
scope :with_source_id, ->(source_id) { where(source_id: source_id) } scope :with_source_id, ->(source_id) { where(source_id: source_id) }
......
...@@ -393,7 +393,6 @@ class Namespace < ApplicationRecord ...@@ -393,7 +393,6 @@ class Namespace < ApplicationRecord
end end
def changing_shared_runners_enabled_is_allowed def changing_shared_runners_enabled_is_allowed
return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
return unless new_record? || changes.has_key?(:shared_runners_enabled) return unless new_record? || changes.has_key?(:shared_runners_enabled)
if shared_runners_enabled && has_parent? && parent.shared_runners_setting == 'disabled_and_unoverridable' if shared_runners_enabled && has_parent? && parent.shared_runners_setting == 'disabled_and_unoverridable'
...@@ -402,7 +401,6 @@ class Namespace < ApplicationRecord ...@@ -402,7 +401,6 @@ class Namespace < ApplicationRecord
end end
def changing_allow_descendants_override_disabled_shared_runners_is_allowed def changing_allow_descendants_override_disabled_shared_runners_is_allowed
return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
return unless new_record? || changes.has_key?(:allow_descendants_override_disabled_shared_runners) return unless new_record? || changes.has_key?(:allow_descendants_override_disabled_shared_runners)
if shared_runners_enabled && !new_record? if shared_runners_enabled && !new_record?
......
...@@ -1195,7 +1195,6 @@ class Project < ApplicationRecord ...@@ -1195,7 +1195,6 @@ class Project < ApplicationRecord
end end
def changing_shared_runners_enabled_is_allowed def changing_shared_runners_enabled_is_allowed
return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
return unless new_record? || changes.has_key?(:shared_runners_enabled) return unless new_record? || changes.has_key?(:shared_runners_enabled)
if shared_runners_enabled && group && group.shared_runners_setting == 'disabled_and_unoverridable' if shared_runners_enabled && group && group.shared_runners_setting == 'disabled_and_unoverridable'
......
# frozen_string_literal: true
class InvitationPresenter < Gitlab::View::Presenter::Delegated
presents :invitation
end
# frozen_string_literal: true
module Members
class InviteService < Members::BaseService
DEFAULT_LIMIT = 100
attr_reader :errors
def initialize(current_user, params)
@current_user, @params = current_user, params.dup
@errors = {}
end
def execute(source)
return error(s_('Email cannot be blank')) if params[:email].blank?
emails = params[:email].split(',').uniq.flatten
return error(s_("Too many users specified (limit is %{user_limit})") % { user_limit: user_limit }) if
user_limit && emails.size > user_limit
emails.each do |email|
next if existing_member?(source, email)
next if existing_invite?(source, email)
if existing_user?(email)
add_existing_user_as_member(current_user, source, params, email)
next
end
invite_new_member_and_user(current_user, source, params, email)
end
return success unless errors.any?
error(errors)
end
private
def invite_new_member_and_user(current_user, source, params, email)
new_member = (source.class.name + 'Member').constantize.create(source_id: source.id,
user_id: nil,
access_level: params[:access_level],
invite_email: email,
created_by_id: current_user.id,
expires_at: params[:expires_at],
requested_at: Time.current.utc)
unless new_member.valid? && new_member.persisted?
errors[params[:email]] = new_member.errors.full_messages.to_sentence
end
end
def add_existing_user_as_member(current_user, source, params, email)
new_member = create_member(current_user, existing_user(email), source, params.merge({ invite_email: email }))
unless new_member.valid? && new_member.persisted?
errors[email] = new_member.errors.full_messages.to_sentence
end
end
def create_member(current_user, user, source, params)
source.add_user(user, params[:access_level], current_user: current_user, expires_at: params[:expires_at])
end
def user_limit
limit = params.fetch(:limit, DEFAULT_LIMIT)
limit && limit < 0 ? nil : limit
end
def existing_member?(source, email)
existing_member = source.members.with_user_by_email(email).exists?
if existing_member
errors[email] = "Already a member of #{source.name}"
return true
end
false
end
def existing_invite?(source, email)
existing_invite = source.members.search_invite_email(email).exists?
if existing_invite
errors[email] = "Member already invited to #{source.name}"
return true
end
false
end
def existing_user(email)
User.find_by_email(email)
end
def existing_user?(email)
existing_user(email).present?
end
end
end
...@@ -9,7 +9,7 @@ module Packages ...@@ -9,7 +9,7 @@ module Packages
def execute def execute
if @tag_name.present? if @tag_name.present?
@tag_name.match(Gitlab::Regex.composer_package_version_regex).captures[0] @tag_name.delete_prefix('v')
elsif @branch_name.present? elsif @branch_name.present?
branch_sufix_or_prefix(@branch_name.match(Gitlab::Regex.composer_package_version_regex)) branch_sufix_or_prefix(@branch_name.match(Gitlab::Regex.composer_package_version_regex))
end end
......
...@@ -16,6 +16,7 @@ module Search ...@@ -16,6 +16,7 @@ module Search
Gitlab::SearchResults.new(current_user, Gitlab::SearchResults.new(current_user,
params[:search], params[:search],
projects, projects,
order_by: params[:order_by],
sort: params[:sort], sort: params[:sort],
filters: { state: params[:state], confidential: params[:confidential] }) filters: { state: params[:state], confidential: params[:confidential] })
end end
......
...@@ -16,6 +16,7 @@ module Search ...@@ -16,6 +16,7 @@ module Search
params[:search], params[:search],
projects, projects,
group: group, group: group,
order_by: params[:order_by],
sort: params[:sort], sort: params[:sort],
filters: { state: params[:state], confidential: params[:confidential] } filters: { state: params[:state], confidential: params[:confidential] }
) )
......
...@@ -17,6 +17,7 @@ module Search ...@@ -17,6 +17,7 @@ module Search
params[:search], params[:search],
project: project, project: project,
repository_ref: params[:repository_ref], repository_ref: params[:repository_ref],
order_by: params[:order_by],
sort: params[:sort], sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] } filters: { confidential: params[:confidential], state: params[:state] }
) )
......
---
title: Allow semver versions in composer packages
merge_request: 46301
author:
type: fixed
---
title: Add ability to sort to search API
merge_request: 46646
author:
type: added
---
title: Enable refactored union set operator
merge_request: 46295
author:
type: added
---
title: Add API post /invitations by email
merge_request: 45950
author:
type: added
---
name: disable_shared_runners_on_group
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36080
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258991
type: development
group: group::runner
default_enabled: true
---
name: sql_set_operators
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39786
rollout_issue_url:
group: group::access
type: development
default_enabled: false
...@@ -8,3 +8,4 @@ Grape::Validations.register_validator(:integer_none_any, ::API::Validations::Val ...@@ -8,3 +8,4 @@ Grape::Validations.register_validator(:integer_none_any, ::API::Validations::Val
Grape::Validations.register_validator(:array_none_any, ::API::Validations::Validators::ArrayNoneAny) Grape::Validations.register_validator(:array_none_any, ::API::Validations::Validators::ArrayNoneAny)
Grape::Validations.register_validator(:check_assignees_count, ::API::Validations::Validators::CheckAssigneesCount) Grape::Validations.register_validator(:check_assignees_count, ::API::Validations::Validators::CheckAssigneesCount)
Grape::Validations.register_validator(:untrusted_regexp, ::API::Validations::Validators::UntrustedRegexp) Grape::Validations.register_validator(:untrusted_regexp, ::API::Validations::Validators::UntrustedRegexp)
Grape::Validations.register_validator(:email_or_email_list, ::API::Validations::Validators::EmailOrEmailList)
# frozen_string_literal: true
class CreateVulnerabilityFindingLinks < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
create_table :vulnerability_finding_links, if_not_exists: true do |t|
t.timestamps_with_timezone null: false
t.references :vulnerability_occurrence, index: { name: 'finding_links_on_vulnerability_occurrence_id' }, null: false, foreign_key: { on_delete: :cascade }
t.text :name, limit: 255
t.text :url, limit: 2048, null: false
end
add_text_limit :vulnerability_finding_links, :name, 255
add_text_limit :vulnerability_finding_links, :url, 2048
end
def down
drop_table :vulnerability_finding_links
end
end
50e4e42c804d3abdcfe9ab2bbb890262d4b2ddd93bff1b2af1da1e55a0300cf5
\ No newline at end of file
...@@ -17104,6 +17104,26 @@ CREATE SEQUENCE vulnerability_feedback_id_seq ...@@ -17104,6 +17104,26 @@ CREATE SEQUENCE vulnerability_feedback_id_seq
ALTER SEQUENCE vulnerability_feedback_id_seq OWNED BY vulnerability_feedback.id; ALTER SEQUENCE vulnerability_feedback_id_seq OWNED BY vulnerability_feedback.id;
CREATE TABLE vulnerability_finding_links (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
vulnerability_occurrence_id bigint NOT NULL,
name text,
url text NOT NULL,
CONSTRAINT check_55f0a95439 CHECK ((char_length(name) <= 255)),
CONSTRAINT check_b7fe886df6 CHECK ((char_length(url) <= 2048))
);
CREATE SEQUENCE vulnerability_finding_links_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE vulnerability_finding_links_id_seq OWNED BY vulnerability_finding_links.id;
CREATE TABLE vulnerability_historical_statistics ( CREATE TABLE vulnerability_historical_statistics (
id bigint NOT NULL, id bigint NOT NULL,
created_at timestamp with time zone NOT NULL, created_at timestamp with time zone NOT NULL,
...@@ -18203,6 +18223,8 @@ ALTER TABLE ONLY vulnerability_exports ALTER COLUMN id SET DEFAULT nextval('vuln ...@@ -18203,6 +18223,8 @@ ALTER TABLE ONLY vulnerability_exports ALTER COLUMN id SET DEFAULT nextval('vuln
ALTER TABLE ONLY vulnerability_feedback ALTER COLUMN id SET DEFAULT nextval('vulnerability_feedback_id_seq'::regclass); ALTER TABLE ONLY vulnerability_feedback ALTER COLUMN id SET DEFAULT nextval('vulnerability_feedback_id_seq'::regclass);
ALTER TABLE ONLY vulnerability_finding_links ALTER COLUMN id SET DEFAULT nextval('vulnerability_finding_links_id_seq'::regclass);
ALTER TABLE ONLY vulnerability_historical_statistics ALTER COLUMN id SET DEFAULT nextval('vulnerability_historical_statistics_id_seq'::regclass); ALTER TABLE ONLY vulnerability_historical_statistics ALTER COLUMN id SET DEFAULT nextval('vulnerability_historical_statistics_id_seq'::regclass);
ALTER TABLE ONLY vulnerability_identifiers ALTER COLUMN id SET DEFAULT nextval('vulnerability_identifiers_id_seq'::regclass); ALTER TABLE ONLY vulnerability_identifiers ALTER COLUMN id SET DEFAULT nextval('vulnerability_identifiers_id_seq'::regclass);
...@@ -19646,6 +19668,9 @@ ALTER TABLE ONLY vulnerability_exports ...@@ -19646,6 +19668,9 @@ ALTER TABLE ONLY vulnerability_exports
ALTER TABLE ONLY vulnerability_feedback ALTER TABLE ONLY vulnerability_feedback
ADD CONSTRAINT vulnerability_feedback_pkey PRIMARY KEY (id); ADD CONSTRAINT vulnerability_feedback_pkey PRIMARY KEY (id);
ALTER TABLE ONLY vulnerability_finding_links
ADD CONSTRAINT vulnerability_finding_links_pkey PRIMARY KEY (id);
ALTER TABLE ONLY vulnerability_historical_statistics ALTER TABLE ONLY vulnerability_historical_statistics
ADD CONSTRAINT vulnerability_historical_statistics_pkey PRIMARY KEY (id); ADD CONSTRAINT vulnerability_historical_statistics_pkey PRIMARY KEY (id);
...@@ -19870,6 +19895,8 @@ CREATE UNIQUE INDEX epic_user_mentions_on_epic_id_and_note_id_index ON epic_user ...@@ -19870,6 +19895,8 @@ CREATE UNIQUE INDEX epic_user_mentions_on_epic_id_and_note_id_index ON epic_user
CREATE UNIQUE INDEX epic_user_mentions_on_epic_id_index ON epic_user_mentions USING btree (epic_id) WHERE (note_id IS NULL); CREATE UNIQUE INDEX epic_user_mentions_on_epic_id_index ON epic_user_mentions USING btree (epic_id) WHERE (note_id IS NULL);
CREATE INDEX finding_links_on_vulnerability_occurrence_id ON vulnerability_finding_links USING btree (vulnerability_occurrence_id);
CREATE INDEX idx_audit_events_on_entity_id_desc_author_id_created_at ON audit_events USING btree (entity_id, entity_type, id DESC, author_id, created_at); CREATE INDEX idx_audit_events_on_entity_id_desc_author_id_created_at ON audit_events USING btree (entity_id, entity_type, id DESC, author_id, created_at);
CREATE INDEX idx_ci_pipelines_artifacts_locked ON ci_pipelines USING btree (ci_ref_id, id) WHERE (locked = 1); CREATE INDEX idx_ci_pipelines_artifacts_locked ON ci_pipelines USING btree (ci_ref_id, id) WHERE (locked = 1);
...@@ -24195,6 +24222,9 @@ ALTER TABLE ONLY gpg_signatures ...@@ -24195,6 +24222,9 @@ ALTER TABLE ONLY gpg_signatures
ALTER TABLE ONLY board_group_recent_visits ALTER TABLE ONLY board_group_recent_visits
ADD CONSTRAINT fk_rails_ca04c38720 FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_ca04c38720 FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE;
ALTER TABLE ONLY vulnerability_finding_links
ADD CONSTRAINT fk_rails_cbdfde27ce FOREIGN KEY (vulnerability_occurrence_id) REFERENCES vulnerability_occurrences(id) ON DELETE CASCADE;
ALTER TABLE ONLY issues_self_managed_prometheus_alert_events ALTER TABLE ONLY issues_self_managed_prometheus_alert_events
ADD CONSTRAINT fk_rails_cc5d88bbb0 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_cc5d88bbb0 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
......
This diff is collapsed.
...@@ -40,6 +40,7 @@ The following API resources are available in the project context: ...@@ -40,6 +40,7 @@ The following API resources are available in the project context:
| [Events](events.md) | `/projects/:id/events` (also available for users and standalone) | | [Events](events.md) | `/projects/:id/events` (also available for users and standalone) |
| [Feature Flags](feature_flags.md) | `/projects/:id/feature_flags` | | [Feature Flags](feature_flags.md) | `/projects/:id/feature_flags` |
| [Feature Flag User Lists](feature_flag_user_lists.md) | `/projects/:id/feature_flags_user_lists` | | [Feature Flag User Lists](feature_flag_user_lists.md) | `/projects/:id/feature_flags_user_lists` |
| [Invitations](invitations.md) | `/projects/:id/invitations` (also available for groups) |
| [Issues](issues.md) | `/projects/:id/issues` (also available for groups and standalone) | | [Issues](issues.md) | `/projects/:id/issues` (also available for groups and standalone) |
| [Issues Statistics](issues_statistics.md) | `/projects/:id/issues_statistics` (also available for groups and standalone) | | [Issues Statistics](issues_statistics.md) | `/projects/:id/issues_statistics` (also available for groups and standalone) |
| [Issue boards](boards.md) | `/projects/:id/boards` | | [Issue boards](boards.md) | `/projects/:id/boards` |
...@@ -108,6 +109,7 @@ The following API resources are available in the group context: ...@@ -108,6 +109,7 @@ The following API resources are available in the group context:
| [Group labels](group_labels.md) | `/groups/:id/labels` | | [Group labels](group_labels.md) | `/groups/:id/labels` |
| [Group-level variables](group_level_variables.md) | `/groups/:id/variables` | | [Group-level variables](group_level_variables.md) | `/groups/:id/variables` |
| [Group milestones](group_milestones.md) | `/groups/:id/milestones` | | [Group milestones](group_milestones.md) | `/groups/:id/milestones` |
| [Invitations](invitations.md) | `/groups/:id/invitations` (also available for projects) |
| [Issues](issues.md) | `/groups/:id/issues` (also available for projects and standalone) | | [Issues](issues.md) | `/groups/:id/issues` (also available for projects and standalone) |
| [Issues Statistics](issues_statistics.md) | `/groups/:id/issues_statistics` (also available for projects and standalone) | | [Issues Statistics](issues_statistics.md) | `/groups/:id/issues_statistics` (also available for projects and standalone) |
| [Members](members.md) | `/groups/:id/members` (also available for projects) | | [Members](members.md) | `/groups/:id/members` (also available for projects) |
......
This diff is collapsed.
---
stage: Growth
group: Expansion
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/#designated-technical-writers
---
# Invitations API
Use the Invitations API to send email to users you want to join a group or project.
## Valid access levels
To send an invitation, you must have access to the project or group you are sending email for. Valid access
levels are defined in the `Gitlab::Access` module. Currently, these levels are valid:
- No access (`0`)
- Guest (`10`)
- Reporter (`20`)
- Developer (`30`)
- Maintainer (`40`)
- Owner (`50`) - Only valid to set for groups
CAUTION: **Caution:**
Due to [an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/219299),
projects in personal namespaces will not show owner (`50`) permission.
## Invite by email to group or project
Invites a new user by email to join a group or project.
```plaintext
POST /groups/:id/invitations
POST /projects/:id/invitations
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `email` | integer/string | yes | The email of the new member or multiple emails separated by commas |
| `access_level` | integer | yes | A valid access level |
| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --data "email=test@example.com&access_level=30" "https://gitlab.example.com/api/v4/groups/:id/invitations"
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --data "email=test@example.com&access_level=30" "https://gitlab.example.com/api/v4/projects/:id/invitations"
```
Example responses:
When all emails were successfully sent:
```json
{ "status": "success" }
```
When there was any error sending the email:
```json
{
"status": "error",
"message": {
"test@example.com": "Already invited",
"test2@example.com": "Member already exsists"
}
}
```
...@@ -26,6 +26,8 @@ GET /search ...@@ -26,6 +26,8 @@ GET /search
| `search` | string | yes | The search query | | `search` | string | yes | The search query |
| `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. | | `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. |
| `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. | | `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. |
| `order_by` | string | no | Allowed values are `created_at` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
| `sort` | string | no | Allowed values are `asc` or `desc` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, users. Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, users.
...@@ -436,6 +438,8 @@ GET /groups/:id/search ...@@ -436,6 +438,8 @@ GET /groups/:id/search
| `search` | string | yes | The search query | | `search` | string | yes | The search query |
| `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. | | `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. |
| `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. | | `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. |
| `order_by` | string | no | Allowed values are `created_at` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
| `sort` | string | no | Allowed values are `asc` or `desc` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, users. Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, users.
...@@ -816,6 +820,8 @@ GET /projects/:id/search ...@@ -816,6 +820,8 @@ GET /projects/:id/search
| `ref` | string | no | The name of a repository branch or tag to search on. The project's default branch is used by default. This is only applicable for scopes: commits, blobs, and wiki_blobs. | | `ref` | string | no | The name of a repository branch or tag to search on. The project's default branch is used by default. This is only applicable for scopes: commits, blobs, and wiki_blobs. |
| `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. | | `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. |
| `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. | | `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. |
| `order_by` | string | no | Allowed values are `created_at` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
| `sort` | string | no | Allowed values are `asc` or `desc` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
Search the expression within the specified scope. Currently these scopes are supported: issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs, users. Search the expression within the specified scope. Currently these scopes are supported: issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs, users.
......
...@@ -207,6 +207,12 @@ guide on how you can add a new custom validator. ...@@ -207,6 +207,12 @@ guide on how you can add a new custom validator.
checks if the value of the given parameter is either an `Array`, `None`, or `Any`. checks if the value of the given parameter is either an `Array`, `None`, or `Any`.
It allows only either of these mentioned values to move forward in the request. It allows only either of these mentioned values to move forward in the request.
- `EmailOrEmailList`:
The [`EmailOrEmailList` validator](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/api/validations/validators/email_or_email_list.rb)
checks if the value of a string or a list of strings contains only valid
email addresses. It allows only lists with all valid email addresses to move forward in the request.
### Adding a new custom validator ### Adding a new custom validator
Custom validators are a great way to validate parameters before sending Custom validators are a great way to validate parameters before sending
......
...@@ -258,6 +258,7 @@ Table description links: ...@@ -258,6 +258,7 @@ Table description links:
| [NGINX](#nginx) | Routes requests to appropriate components, terminates SSL | ✅ | ✅ | ⚙ | ✅ | ⤓ | ❌ | CE & EE | | [NGINX](#nginx) | Routes requests to appropriate components, terminates SSL | ✅ | ✅ | ⚙ | ✅ | ⤓ | ❌ | CE & EE |
| [Node Exporter](#node-exporter) | Prometheus endpoint with system metrics | ✅ | N/A | N/A | ✅ | ❌ | ❌ | CE & EE | | [Node Exporter](#node-exporter) | Prometheus endpoint with system metrics | ✅ | N/A | N/A | ✅ | ❌ | ❌ | CE & EE |
| [Outbound email (SMTP)](#outbound-email) | Send email messages to users | ⤓ | ⚙ | ⤓ | ✅ | ⤓ | ⤓ | CE & EE | | [Outbound email (SMTP)](#outbound-email) | Send email messages to users | ⤓ | ⚙ | ⤓ | ✅ | ⤓ | ⤓ | CE & EE |
| [Patroni](#patroni) | Manage PostgreSQL HA cluster leader selection and replication | ⚙ | ❌ | ❌ | ✅ | ❌ | ❌ | EE Only |
| [PgBouncer Exporter](#pgbouncer-exporter) | Prometheus endpoint with PgBouncer metrics | ⚙ | ❌ | ❌ | ✅ | ❌ | ❌ | CE & EE | | [PgBouncer Exporter](#pgbouncer-exporter) | Prometheus endpoint with PgBouncer metrics | ⚙ | ❌ | ❌ | ✅ | ❌ | ❌ | CE & EE |
| [PgBouncer](#pgbouncer) | Database connection pooling, failover | ⚙ | ❌ | ❌ | ✅ | ❌ | ❌ | EE Only | | [PgBouncer](#pgbouncer) | Database connection pooling, failover | ⚙ | ❌ | ❌ | ✅ | ❌ | ❌ | EE Only |
| [PostgreSQL Exporter](#postgresql-exporter) | Prometheus endpoint with PostgreSQL metrics | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | CE & EE | | [PostgreSQL Exporter](#postgresql-exporter) | Prometheus endpoint with PostgreSQL metrics | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | CE & EE |
...@@ -545,6 +546,15 @@ NGINX has an Ingress port for all HTTP requests and routes them to the appropria ...@@ -545,6 +546,15 @@ NGINX has an Ingress port for all HTTP requests and routes them to the appropria
[Node Exporter](https://github.com/prometheus/node_exporter) is a Prometheus tool that gives us metrics on the underlying machine (think CPU/Disk/Load). It's just a packaged version of the common open source offering from the Prometheus project. [Node Exporter](https://github.com/prometheus/node_exporter) is a Prometheus tool that gives us metrics on the underlying machine (think CPU/Disk/Load). It's just a packaged version of the common open source offering from the Prometheus project.
#### Patroni
- [Project Page](https://github.com/zalando/patroni)
- Configuration:
- [Omnibus](../administration/postgresql/replication_and_failover.md#patroni)
- Layer: Core Service (Data)
- Process: `patroni`
- GitLab.com: [Database Architecture](https://about.gitlab.com/handbook/engineering/infrastructure/production/architecture/#database-architecture)
#### PgBouncer #### PgBouncer
- [Project page](https://github.com/pgbouncer/pgbouncer/blob/master/README.md) - [Project page](https://github.com/pgbouncer/pgbouncer/blob/master/README.md)
......
...@@ -633,6 +633,8 @@ To enable this feature: ...@@ -633,6 +633,8 @@ To enable this feature:
1. Expand the **Permissions, LFS, 2FA** section, and enter IP address ranges into **Allow access to the following IP addresses** field. 1. Expand the **Permissions, LFS, 2FA** section, and enter IP address ranges into **Allow access to the following IP addresses** field.
1. Click **Save changes**. 1. Click **Save changes**.
![Domain restriction by IP address](img/restrict-by-ip.gif)
#### Allowed domain restriction **(PREMIUM)** #### Allowed domain restriction **(PREMIUM)**
>- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7297) in [GitLab Premium and Silver](https://about.gitlab.com/pricing/) 12.2. >- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7297) in [GitLab Premium and Silver](https://about.gitlab.com/pricing/) 12.2.
...@@ -661,6 +663,8 @@ To enable this feature: ...@@ -661,6 +663,8 @@ To enable this feature:
1. Expand the **Permissions, LFS, 2FA** section, and enter the domain names into **Restrict membership by email** field. 1. Expand the **Permissions, LFS, 2FA** section, and enter the domain names into **Restrict membership by email** field.
1. Click **Save changes**. 1. Click **Save changes**.
![Domain restriction by email](img/restrict-by-email.gif)
This will enable the domain-checking for all new users added to the group from this moment on. This will enable the domain-checking for all new users added to the group from this moment on.
#### Group file templates **(PREMIUM)** #### Group file templates **(PREMIUM)**
......
This diff is collapsed.
...@@ -31,7 +31,7 @@ authenticate with GitLab by using the `CI_JOB_TOKEN`. ...@@ -31,7 +31,7 @@ authenticate with GitLab by using the `CI_JOB_TOKEN`.
CI/CD templates, which you can use to get started, are in [this repo](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates). CI/CD templates, which you can use to get started, are in [this repo](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates).
Learn more about [using CI/CD to build Maven packages](../maven_repository/index.md#create-maven-packages-with-gitlab-cicd), [NPM packages](../npm_registry/index.md#publishing-a-package-with-cicd), [Composer packages](../composer_repository/index.md#publish-a-composer-package-by-using-cicd), [NuGet Packages](../nuget_repository/index.md#publishing-a-nuget-package-with-cicd), [Conan Packages](../conan_repository/index.md#publish-a-conan-package-by-using-cicd), [PyPI packages](../pypi_repository/index.md#using-gitlab-ci-with-pypi-packages), and [generic packages](../generic_packages/index.md#publish-a-generic-package-by-using-cicd). Learn more about [using CI/CD to build Maven packages](../maven_repository/index.md#create-maven-packages-with-gitlab-cicd), [NPM packages](../npm_registry/index.md#publish-an-npm-package-by-using-cicd), [Composer packages](../composer_repository/index.md#publish-a-composer-package-by-using-cicd), [NuGet Packages](../nuget_repository/index.md#publishing-a-nuget-package-with-cicd), [Conan Packages](../conan_repository/index.md#publish-a-conan-package-by-using-cicd), [PyPI packages](../pypi_repository/index.md#using-gitlab-ci-with-pypi-packages), and [generic packages](../generic_packages/index.md#publish-a-generic-package-by-using-cicd).
If you use CI/CD to build a package, extended activity If you use CI/CD to build a package, extended activity
information is displayed when you view the package details: information is displayed when you view the package details:
......
...@@ -67,9 +67,9 @@ If you are using NPM, this involves creating an `.npmrc` file and adding the app ...@@ -67,9 +67,9 @@ If you are using NPM, this involves creating an `.npmrc` file and adding the app
to your project using your project ID, then adding a section to your `package.json` file with a similar URL. to your project using your project ID, then adding a section to your `package.json` file with a similar URL.
Follow Follow
the instructions in the [GitLab NPM Registry documentation](../npm_registry/index.md#authenticating-to-the-gitlab-npm-registry). After the instructions in the [GitLab NPM Registry documentation](../npm_registry/index.md#authenticate-to-the-package-registry). After
you do this, you can push your NPM package to your project using `npm publish`, as described in the you do this, you can push your NPM package to your project using `npm publish`, as described in the
[uploading packages](../npm_registry/index.md#uploading-packages) section of the docs. [publishing packages](../npm_registry/index.md#publish-an-npm-package) section of the docs.
#### Maven #### Maven
......
...@@ -9,7 +9,7 @@ export default { ...@@ -9,7 +9,7 @@ export default {
i18n: { i18n: {
editPermissions: s__('Members|Edit permissions'), editPermissions: s__('Members|Edit permissions'),
modalBody: s__( modalBody: s__(
'Members|%{userName} is currently a LDAP user. Editing their permissions will override the settings from the LDAP group sync.', 'Members|%{userName} is currently an LDAP user. Editing their permissions will override the settings from the LDAP group sync.',
), ),
toastMessage: s__('Members|LDAP override enabled.'), toastMessage: s__('Members|LDAP override enabled.'),
}, },
......
...@@ -75,7 +75,7 @@ module Security ...@@ -75,7 +75,7 @@ module Security
def normalize_report_findings(report_findings, vulnerabilities) def normalize_report_findings(report_findings, vulnerabilities)
report_findings.map do |report_finding| report_findings.map do |report_finding|
finding_hash = report_finding.to_hash finding_hash = report_finding.to_hash
.except(:compare_key, :identifiers, :location, :scanner) .except(:compare_key, :identifiers, :location, :scanner, :links)
finding = Vulnerabilities::Finding.new(finding_hash) finding = Vulnerabilities::Finding.new(finding_hash)
# assigning Vulnerabilities to Findings to enable the computed state # assigning Vulnerabilities to Findings to enable the computed state
...@@ -84,6 +84,9 @@ module Security ...@@ -84,6 +84,9 @@ module Security
finding.project = pipeline.project finding.project = pipeline.project
finding.sha = pipeline.sha finding.sha = pipeline.sha
finding.build_scanner(report_finding.scanner&.to_hash) finding.build_scanner(report_finding.scanner&.to_hash)
finding.finding_links = report_finding.links.map do |link|
Vulnerabilities::FindingLink.new(link.to_hash)
end
finding.identifiers = report_finding.identifiers.map do |identifier| finding.identifiers = report_finding.identifiers.map do |identifier|
Vulnerabilities::Identifier.new(identifier.to_hash) Vulnerabilities::Identifier.new(identifier.to_hash)
end end
......
...@@ -26,6 +26,8 @@ module Vulnerabilities ...@@ -26,6 +26,8 @@ module Vulnerabilities
has_many :finding_identifiers, class_name: 'Vulnerabilities::FindingIdentifier', inverse_of: :finding, foreign_key: 'occurrence_id' has_many :finding_identifiers, class_name: 'Vulnerabilities::FindingIdentifier', inverse_of: :finding, foreign_key: 'occurrence_id'
has_many :identifiers, through: :finding_identifiers, class_name: 'Vulnerabilities::Identifier' has_many :identifiers, through: :finding_identifiers, class_name: 'Vulnerabilities::Identifier'
has_many :finding_links, class_name: 'Vulnerabilities::FindingLink', inverse_of: :finding, foreign_key: 'vulnerability_occurrence_id'
has_many :finding_pipelines, class_name: 'Vulnerabilities::FindingPipeline', inverse_of: :finding, foreign_key: 'occurrence_id' has_many :finding_pipelines, class_name: 'Vulnerabilities::FindingPipeline', inverse_of: :finding, foreign_key: 'occurrence_id'
has_many :pipelines, through: :finding_pipelines, class_name: 'Ci::Pipeline' has_many :pipelines, through: :finding_pipelines, class_name: 'Ci::Pipeline'
...@@ -256,7 +258,9 @@ module Vulnerabilities ...@@ -256,7 +258,9 @@ module Vulnerabilities
end end
def links def links
metadata.fetch('links', []) return metadata.fetch('links', []) if finding_links.load.empty?
finding_links.as_json(only: [:name, :url])
end end
def remediations def remediations
......
# frozen_string_literal: true
module Vulnerabilities
class FindingLink < ApplicationRecord
self.table_name = 'vulnerability_finding_links'
belongs_to :finding, class_name: 'Vulnerabilities::Finding', inverse_of: :finding_identifiers, foreign_key: 'vulnerability_occurrence_id'
validates :finding, presence: true
validates :url, presence: true, length: { maximum: 255 }
validates :name, length: { maximum: 2048 }
end
end
...@@ -16,6 +16,7 @@ module EE ...@@ -16,6 +16,7 @@ module EE
params[:search], params[:search],
elastic_projects, elastic_projects,
public_and_internal_projects: elastic_global, public_and_internal_projects: elastic_global,
order_by: params[:order_by],
sort: params[:sort], sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] } filters: { confidential: params[:confidential], state: params[:state] }
) )
......
...@@ -30,6 +30,7 @@ module EE ...@@ -30,6 +30,7 @@ module EE
elastic_projects, elastic_projects,
group: group, group: group,
public_and_internal_projects: elastic_global, public_and_internal_projects: elastic_global,
order_by: params[:order_by],
sort: params[:sort], sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] } filters: { confidential: params[:confidential], state: params[:state] }
) )
......
...@@ -15,6 +15,7 @@ module EE ...@@ -15,6 +15,7 @@ module EE
params[:search], params[:search],
project: project, project: project,
repository_ref: repository_ref, repository_ref: repository_ref,
order_by: params[:order_by],
sort: params[:sort], sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] } filters: { confidential: params[:confidential], state: params[:state] }
) )
......
...@@ -47,7 +47,7 @@ module Security ...@@ -47,7 +47,7 @@ module Security
return return
end end
vulnerability_params = finding.to_hash.except(:compare_key, :identifiers, :location, :scanner, :scan) vulnerability_params = finding.to_hash.except(:compare_key, :identifiers, :location, :scanner, :scan, :links)
vulnerability_finding = create_or_find_vulnerability_finding(finding, vulnerability_params) vulnerability_finding = create_or_find_vulnerability_finding(finding, vulnerability_params)
update_vulnerability_scanner(finding) update_vulnerability_scanner(finding)
...@@ -60,6 +60,8 @@ module Security ...@@ -60,6 +60,8 @@ module Security
create_or_update_vulnerability_identifier_object(vulnerability_finding, identifier) create_or_update_vulnerability_identifier_object(vulnerability_finding, identifier)
end end
create_or_update_vulnerability_links(finding, vulnerability_finding)
create_vulnerability_pipeline_object(vulnerability_finding, pipeline) create_vulnerability_pipeline_object(vulnerability_finding, pipeline)
create_vulnerability(vulnerability_finding, pipeline) create_vulnerability(vulnerability_finding, pipeline)
...@@ -125,6 +127,15 @@ module Security ...@@ -125,6 +127,15 @@ module Security
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
end end
def create_or_update_vulnerability_links(finding, vulnerability_finding)
return if finding.links.blank?
finding.links.each do |link|
vulnerability_finding.finding_links.safe_find_or_create_by!(link.to_hash)
end
rescue ActiveRecord::RecordNotUnique
end
def create_vulnerability_pipeline_object(vulnerability_finding, pipeline) def create_vulnerability_pipeline_object(vulnerability_finding, pipeline)
vulnerability_finding.finding_pipelines.find_or_create_by!(pipeline: pipeline) vulnerability_finding.finding_pipelines.find_or_create_by!(pipeline: pipeline)
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
......
---
title: Add Vulnerabilities::FindingLink model
merge_request: 46555
author:
type: added
...@@ -137,14 +137,16 @@ module Elastic ...@@ -137,14 +137,16 @@ module Elastic
end end
def apply_sort(query_hash, options) def apply_sort(query_hash, options)
case options[:sort] # Due to different uses of sort param we prefer order_by when
when 'created_asc' # present
case ::Gitlab::Search::SortOptions.sort_and_direction(options[:order_by], options[:sort])
when :created_at_asc
query_hash.merge(sort: { query_hash.merge(sort: {
created_at: { created_at: {
order: 'asc' order: 'asc'
} }
}) })
when 'created_desc' when :created_at_desc
query_hash.merge(sort: { query_hash.merge(sort: {
created_at: { created_at: {
order: 'desc' order: 'desc'
......
...@@ -55,6 +55,7 @@ module Gitlab ...@@ -55,6 +55,7 @@ module Gitlab
def create_vulnerability(report, data, version) def create_vulnerability(report, data, version)
identifiers = create_identifiers(report, data['identifiers']) identifiers = create_identifiers(report, data['identifiers'])
links = create_links(report, data['links'])
report.add_finding( report.add_finding(
::Gitlab::Ci::Reports::Security::Finding.new( ::Gitlab::Ci::Reports::Security::Finding.new(
uuid: SecureRandom.uuid, uuid: SecureRandom.uuid,
...@@ -67,6 +68,7 @@ module Gitlab ...@@ -67,6 +68,7 @@ module Gitlab
scanner: create_scanner(report, data['scanner']), scanner: create_scanner(report, data['scanner']),
scan: report&.scan, scan: report&.scan,
identifiers: identifiers, identifiers: identifiers,
links: links,
raw_metadata: data.to_json, raw_metadata: data.to_json,
metadata_version: version)) metadata_version: version))
end end
...@@ -106,6 +108,22 @@ module Gitlab ...@@ -106,6 +108,22 @@ module Gitlab
url: identifier['url'])) url: identifier['url']))
end end
def create_links(report, links)
return [] unless links.is_a?(Array)
links
.map { |link| create_link(report, link) }
.compact
end
def create_link(report, link)
return unless link.is_a?(Hash)
::Gitlab::Ci::Reports::Security::Link.new(
name: link['name'],
url: link['url'])
end
def parse_severity_level(input) def parse_severity_level(input)
return input if ::Vulnerabilities::Finding::SEVERITY_LEVELS.key?(input) return input if ::Vulnerabilities::Finding::SEVERITY_LEVELS.key?(input)
......
...@@ -10,6 +10,7 @@ module Gitlab ...@@ -10,6 +10,7 @@ module Gitlab
attr_reader :compare_key attr_reader :compare_key
attr_reader :confidence attr_reader :confidence
attr_reader :identifiers attr_reader :identifiers
attr_reader :links
attr_reader :location attr_reader :location
attr_reader :metadata_version attr_reader :metadata_version
attr_reader :name attr_reader :name
...@@ -24,10 +25,11 @@ module Gitlab ...@@ -24,10 +25,11 @@ module Gitlab
delegate :file_path, :start_line, :end_line, to: :location delegate :file_path, :start_line, :end_line, to: :location
def initialize(compare_key:, identifiers:, location:, metadata_version:, name:, raw_metadata:, report_type:, scanner:, scan:, uuid:, confidence: nil, severity: nil) # rubocop:disable Metrics/ParameterLists def initialize(compare_key:, identifiers:, links: [], location:, metadata_version:, name:, raw_metadata:, report_type:, scanner:, scan:, uuid:, confidence: nil, severity: nil) # rubocop:disable Metrics/ParameterLists
@compare_key = compare_key @compare_key = compare_key
@confidence = confidence @confidence = confidence
@identifiers = identifiers @identifiers = identifiers
@links = links
@location = location @location = location
@metadata_version = metadata_version @metadata_version = metadata_version
@name = name @name = name
...@@ -46,6 +48,7 @@ module Gitlab ...@@ -46,6 +48,7 @@ module Gitlab
compare_key compare_key
confidence confidence
identifiers identifiers
links
location location
metadata_version metadata_version
name name
......
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module Security
class Link
attr_accessor :name, :url
def initialize(name: nil, url: nil)
@name = name
@url = url
end
def to_hash
{
name: name,
url: url
}.compact
end
end
end
end
end
end
...@@ -8,13 +8,15 @@ module Gitlab ...@@ -8,13 +8,15 @@ module Gitlab
class GroupSearchResults < Gitlab::Elastic::SearchResults class GroupSearchResults < Gitlab::Elastic::SearchResults
attr_reader :group, :default_project_filter, :filters attr_reader :group, :default_project_filter, :filters
def initialize(current_user, query, limit_project_ids = nil, group:, public_and_internal_projects: false, default_project_filter: false, sort: nil, filters: {}) # rubocop:disable Metrics/ParameterLists
def initialize(current_user, query, limit_project_ids = nil, group:, public_and_internal_projects: false, default_project_filter: false, order_by: nil, sort: nil, filters: {})
@group = group @group = group
@default_project_filter = default_project_filter @default_project_filter = default_project_filter
@filters = filters @filters = filters
super(current_user, query, limit_project_ids, public_and_internal_projects: public_and_internal_projects, sort: sort, filters: filters) super(current_user, query, limit_project_ids, public_and_internal_projects: public_and_internal_projects, order_by: order_by, sort: sort, filters: filters)
end end
# rubocop:enable Metrics/ParameterLists
end end
end end
end end
...@@ -8,11 +8,11 @@ module Gitlab ...@@ -8,11 +8,11 @@ module Gitlab
class ProjectSearchResults < Gitlab::Elastic::SearchResults class ProjectSearchResults < Gitlab::Elastic::SearchResults
attr_reader :project, :repository_ref, :filters attr_reader :project, :repository_ref, :filters
def initialize(current_user, query, project:, repository_ref: nil, sort: nil, filters: {}) def initialize(current_user, query, project:, repository_ref: nil, order_by: nil, sort: nil, filters: {})
@project = project @project = project
@repository_ref = repository_ref.presence || project.default_branch @repository_ref = repository_ref.presence || project.default_branch
super(current_user, query, [project.id], public_and_internal_projects: false, sort: sort, filters: filters) super(current_user, query, [project.id], public_and_internal_projects: false, order_by: order_by, sort: sort, filters: filters)
end end
private private
......
...@@ -7,17 +7,18 @@ module Gitlab ...@@ -7,17 +7,18 @@ module Gitlab
DEFAULT_PER_PAGE = Gitlab::SearchResults::DEFAULT_PER_PAGE DEFAULT_PER_PAGE = Gitlab::SearchResults::DEFAULT_PER_PAGE
attr_reader :current_user, :query, :public_and_internal_projects, :sort, :filters attr_reader :current_user, :query, :public_and_internal_projects, :order_by, :sort, :filters
# Limit search results by passed projects # Limit search results by passed projects
# It allows us to search only for projects user has access to # It allows us to search only for projects user has access to
attr_reader :limit_project_ids attr_reader :limit_project_ids
def initialize(current_user, query, limit_project_ids = nil, public_and_internal_projects: true, sort: nil, filters: {}) def initialize(current_user, query, limit_project_ids = nil, public_and_internal_projects: true, order_by: nil, sort: nil, filters: {})
@current_user = current_user @current_user = current_user
@query = query @query = query
@limit_project_ids = limit_project_ids @limit_project_ids = limit_project_ids
@public_and_internal_projects = public_and_internal_projects @public_and_internal_projects = public_and_internal_projects
@order_by = order_by
@sort = sort @sort = sort
@filters = filters @filters = filters
end end
...@@ -202,6 +203,7 @@ module Gitlab ...@@ -202,6 +203,7 @@ module Gitlab
current_user: current_user, current_user: current_user,
project_ids: limit_project_ids, project_ids: limit_project_ids,
public_and_internal_projects: public_and_internal_projects, public_and_internal_projects: public_and_internal_projects,
order_by: order_by,
sort: sort sort: sort
} }
end end
...@@ -214,7 +216,7 @@ module Gitlab ...@@ -214,7 +216,7 @@ module Gitlab
def issues def issues
strong_memoize(:issues) do strong_memoize(:issues) do
options = base_options.merge(filters.slice(:sort, :confidential, :state)) options = base_options.merge(filters.slice(:order_by, :sort, :confidential, :state))
Issue.elastic_search(query, options: options) Issue.elastic_search(query, options: options)
end end
...@@ -235,7 +237,7 @@ module Gitlab ...@@ -235,7 +237,7 @@ module Gitlab
def merge_requests def merge_requests
strong_memoize(:merge_requests) do strong_memoize(:merge_requests) do
options = base_options.merge(filters.slice(:sort, :state)) options = base_options.merge(filters.slice(:order_by, :sort, :state))
MergeRequest.elastic_search(query, options: options) MergeRequest.elastic_search(query, options: options)
end end
......
# frozen_string_literal: true
FactoryBot.define do
factory :ci_reports_security_link, class: '::Gitlab::Ci::Reports::Security::Link' do
name { 'CVE-2020-0202' }
url { 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-0202' }
skip_create
initialize_with do
::Gitlab::Ci::Reports::Security::Link.new(**attributes)
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :finding_link, class: 'Vulnerabilities::FindingLink' do
finding factory: :vulnerabilities_finding
name { 'CVE-2018-1234' }
url { 'http://cve.mitre.org/cgi-bin/cvename.cgi?name=2018-1234' }
end
end
...@@ -3,13 +3,13 @@ ...@@ -3,13 +3,13 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Groups > Audit Events', :js do RSpec.describe 'Groups > Audit Events', :js do
include Spec::Support::Helpers::Features::MembersHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:alex) { create(:user, name: 'Alex') } let(:alex) { create(:user, name: 'Alex') }
let(:group) { create(:group) } let(:group) { create(:group) }
before do before do
stub_feature_flags(vue_group_members_list: false)
group.add_owner(user) group.add_owner(user)
group.add_developer(alex) group.add_developer(alex)
sign_in(user) sign_in(user)
...@@ -47,11 +47,9 @@ RSpec.describe 'Groups > Audit Events', :js do ...@@ -47,11 +47,9 @@ RSpec.describe 'Groups > Audit Events', :js do
wait_for_requests wait_for_requests
group_member = group.members.find_by(user_id: alex) page.within first_row do
page.within "#group_member_#{group_member.id}" do
click_button 'Developer' click_button 'Developer'
click_link 'Maintainer' click_button 'Maintainer'
end end
find(:link, text: 'Settings').click find(:link, text: 'Settings').click
......
...@@ -2,14 +2,12 @@ ...@@ -2,14 +2,12 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Groups > Members > List members' do RSpec.describe 'Groups > Members > List members' do
include Spec::Support::Helpers::Features::MembersHelpers
let(:user1) { create(:user, name: 'John Doe') } let(:user1) { create(:user, name: 'John Doe') }
let(:user2) { create(:user, name: 'Mary Jane') } let(:user2) { create(:user, name: 'Mary Jane') }
let(:group) { create(:group) } let(:group) { create(:group) }
before do
stub_feature_flags(vue_group_members_list: false)
end
context 'with Group SAML identity linked for a user' do context 'with Group SAML identity linked for a user' do
let(:saml_provider) { create(:saml_provider) } let(:saml_provider) { create(:saml_provider) }
let(:group) { saml_provider.group } let(:group) { saml_provider.group }
...@@ -23,12 +21,10 @@ RSpec.describe 'Groups > Members > List members' do ...@@ -23,12 +21,10 @@ RSpec.describe 'Groups > Members > List members' do
extern_uid: 'user2@example.com') extern_uid: 'user2@example.com')
end end
it 'shows user with SSO status badge' do it 'shows user with SSO status badge', :js do
visit group_group_members_path(group) visit group_group_members_path(group)
member = GroupMember.find_by(user: user2, group: group) expect(second_row).to have_content('SAML')
expect(find("#group_member_#{member.id}").find('.badge-info')).to have_content('SAML')
end end
end end
...@@ -40,12 +36,10 @@ RSpec.describe 'Groups > Members > List members' do ...@@ -40,12 +36,10 @@ RSpec.describe 'Groups > Members > List members' do
managed_group.add_guest(managed_user) managed_group.add_guest(managed_user)
end end
it 'shows user with "Managed Account" badge' do it 'shows user with "Managed Account" badge', :js do
visit group_group_members_path(managed_group) visit group_group_members_path(managed_group)
member = GroupMember.find_by(user: managed_user, group: managed_group) expect(first_row).to have_content('Managed Account')
expect(page).to have_selector("#group_member_#{member.id} .badge-info", text: 'Managed Account')
end end
end end
......
...@@ -4,6 +4,7 @@ require 'spec_helper' ...@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access levels' do RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access levels' do
include WaitForRequests include WaitForRequests
include Spec::Support::Helpers::Features::MembersHelpers
let(:johndoe) { create(:user, name: 'John Doe') } let(:johndoe) { create(:user, name: 'John Doe') }
let(:maryjane) { create(:user, name: 'Mary Jane') } let(:maryjane) { create(:user, name: 'Mary Jane') }
...@@ -16,8 +17,6 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev ...@@ -16,8 +17,6 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev
let!(:regular_member) { create(:group_member, :guest, group: group, user: maryjane, ldap: false) } let!(:regular_member) { create(:group_member, :guest, group: group, user: maryjane, ldap: false) }
before do before do
stub_feature_flags(vue_group_members_list: false)
# We need to actually activate the LDAP config otherwise `Group#ldap_synced?` will always be false! # We need to actually activate the LDAP config otherwise `Group#ldap_synced?` will always be false!
allow(Gitlab.config.ldap).to receive_messages(enabled: true) allow(Gitlab.config.ldap).to receive_messages(enabled: true)
...@@ -35,7 +34,7 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev ...@@ -35,7 +34,7 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev
visit group_group_members_path(group) visit group_group_members_path(group)
within "#group_member_#{ldap_member.id}" do within first_row do
expect(page).not_to have_content 'LDAP' expect(page).not_to have_content 'LDAP'
expect(page).not_to have_button 'Guest' expect(page).not_to have_button 'Guest'
expect(page).not_to have_button 'Edit permissions' expect(page).not_to have_button 'Edit permissions'
...@@ -47,7 +46,7 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev ...@@ -47,7 +46,7 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev
visit group_group_members_path(group) visit group_group_members_path(group)
within "#group_member_#{ldap_member.id}" do within first_row do
expect(page).to have_content 'LDAP' expect(page).to have_content 'LDAP'
expect(page).to have_button 'Guest', disabled: true expect(page).to have_button 'Guest', disabled: true
expect(page).to have_button 'Edit permissions' expect(page).to have_button 'Edit permissions'
...@@ -55,29 +54,26 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev ...@@ -55,29 +54,26 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev
click_button 'Edit permissions' click_button 'Edit permissions'
end end
expect(page).to have_content ldap_override_message page.within('[role="dialog"]') do
expect(page).to have_content ldap_override_message
click_button 'Change permissions' click_button 'Edit permissions'
end
expect(page).not_to have_content ldap_override_message expect(page).not_to have_content ldap_override_message
expect(page).not_to have_button 'Change permissions'
within "#group_member_#{ldap_member.id}" do within first_row do
expect(page).not_to have_button 'Edit permissions' expect(page).not_to have_button 'Edit permissions'
expect(page).to have_button 'Guest', disabled: false expect(page).to have_button 'Guest', disabled: false
end end
refresh # controls should still be enabled after a refresh refresh # controls should still be enabled after a refresh
within "#group_member_#{ldap_member.id}" do within first_row do
expect(page).not_to have_button 'Edit permissions' expect(page).not_to have_button 'Edit permissions'
expect(page).to have_button 'Guest', disabled: false expect(page).to have_button 'Guest', disabled: false
click_button 'Guest' click_button 'Guest'
click_button 'Revert to LDAP group sync settings'
within '.dropdown-menu' do
click_link 'Revert to LDAP group sync settings'
end
wait_for_requests wait_for_requests
...@@ -85,16 +81,14 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev ...@@ -85,16 +81,14 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev
expect(page).to have_button 'Edit permissions' expect(page).to have_button 'Edit permissions'
end end
within "#group_member_#{regular_member.id}" do within third_row do
expect(page).not_to have_content 'LDAP' expect(page).not_to have_content 'LDAP'
expect(page).not_to have_button 'Edit permissions' expect(page).not_to have_button 'Edit permissions'
expect(page).to have_button 'Guest', disabled: false expect(page).to have_button 'Guest', disabled: false
click_button 'Guest' click_button 'Guest'
within '.dropdown-menu' do expect(page).not_to have_content 'Revert to LDAP group sync settings'
expect(page).not_to have_content 'Revert to LDAP group sync settings'
end
end end
end end
end end
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
"identifiers": [], "identifiers": [],
"links": [ "links": [
{ {
"url": "" "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020"
} }
] ]
}, },
...@@ -37,7 +37,8 @@ ...@@ -37,7 +37,8 @@
"identifiers": [], "identifiers": [],
"links": [ "links": [
{ {
"url": "" "name": "CVE-1030",
"url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030"
} }
] ]
}, },
...@@ -56,12 +57,6 @@ ...@@ -56,12 +57,6 @@
"location": {}, "location": {},
"identifiers": [], "identifiers": [],
"links": [ "links": [
{
"url": ""
},
{
"url": ""
}
] ]
} }
], ],
...@@ -122,4 +117,4 @@ ...@@ -122,4 +117,4 @@
"end_time": "placeholder-value", "end_time": "placeholder-value",
"status": "success" "status": "success"
} }
} }
\ No newline at end of file
...@@ -81,7 +81,7 @@ describe('LdapOverrideConfirmationModal', () => { ...@@ -81,7 +81,7 @@ describe('LdapOverrideConfirmationModal', () => {
it('displays modal body', () => { it('displays modal body', () => {
expect( expect(
getByText( getByText(
`${member.user.name} is currently a LDAP user. Editing their permissions will override the settings from the LDAP group sync.`, `${member.user.name} is currently an LDAP user. Editing their permissions will override the settings from the LDAP group sync.`,
).exists(), ).exists(),
).toBe(true); ).toBe(true);
}); });
......
...@@ -16,6 +16,8 @@ RSpec.describe Mutations::DastOnDemandScans::Create do ...@@ -16,6 +16,8 @@ RSpec.describe Mutations::DastOnDemandScans::Create do
stub_licensed_features(security_on_demand_scans: true) stub_licensed_features(security_on_demand_scans: true)
end end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do describe '#resolve' do
subject do subject do
mutation.resolve( mutation.resolve(
...@@ -33,52 +35,6 @@ RSpec.describe Mutations::DastOnDemandScans::Create do ...@@ -33,52 +35,6 @@ RSpec.describe Mutations::DastOnDemandScans::Create do
end end
end end
context 'when the user is not associated with the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is an owner' do
it 'has no errors' do
group.add_owner(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a maintainer' do
it 'has no errors' do
project.add_maintainer(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a developer' do
it 'has no errors' do
project.add_developer(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a reporter' do
it 'raises an exception' do
project.add_reporter(user)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is a guest' do
it 'raises an exception' do
project.add_guest(user)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user can run a dast scan' do context 'when the user can run a dast scan' do
before do before do
project.add_developer(user) project.add_developer(user)
...@@ -152,14 +108,6 @@ RSpec.describe Mutations::DastOnDemandScans::Create do ...@@ -152,14 +108,6 @@ RSpec.describe Mutations::DastOnDemandScans::Create do
end end
end end
end end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end end
end end
end end
......
...@@ -16,6 +16,8 @@ RSpec.describe Mutations::DastScannerProfiles::Create do ...@@ -16,6 +16,8 @@ RSpec.describe Mutations::DastScannerProfiles::Create do
stub_licensed_features(security_on_demand_scans: true) stub_licensed_features(security_on_demand_scans: true)
end end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do describe '#resolve' do
subject do subject do
mutation.resolve( mutation.resolve(
...@@ -35,12 +37,6 @@ RSpec.describe Mutations::DastScannerProfiles::Create do ...@@ -35,12 +37,6 @@ RSpec.describe Mutations::DastScannerProfiles::Create do
end end
end end
context 'when the user is not associated with the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user can run a dast scan' do context 'when the user can run a dast scan' do
before do before do
group.add_owner(user) group.add_owner(user)
...@@ -83,14 +79,6 @@ RSpec.describe Mutations::DastScannerProfiles::Create do ...@@ -83,14 +79,6 @@ RSpec.describe Mutations::DastScannerProfiles::Create do
expect(response[:errors]).to include('Name has already been taken') expect(response[:errors]).to include('Name has already been taken')
end end
end end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end end
end end
end end
...@@ -15,6 +15,8 @@ RSpec.describe Mutations::DastScannerProfiles::Delete do ...@@ -15,6 +15,8 @@ RSpec.describe Mutations::DastScannerProfiles::Delete do
stub_licensed_features(security_on_demand_scans: true) stub_licensed_features(security_on_demand_scans: true)
end end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do describe '#resolve' do
subject do subject do
mutation.resolve( mutation.resolve(
...@@ -54,14 +56,6 @@ RSpec.describe Mutations::DastScannerProfiles::Delete do ...@@ -54,14 +56,6 @@ RSpec.describe Mutations::DastScannerProfiles::Delete do
end end
end end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when deletion fails' do context 'when deletion fails' do
it 'returns an error' do it 'returns an error' do
allow_next_instance_of(::DastScannerProfiles::DestroyService) do |service| allow_next_instance_of(::DastScannerProfiles::DestroyService) do |service|
......
...@@ -22,6 +22,8 @@ RSpec.describe Mutations::DastScannerProfiles::Update do ...@@ -22,6 +22,8 @@ RSpec.describe Mutations::DastScannerProfiles::Update do
stub_licensed_features(security_on_demand_scans: true) stub_licensed_features(security_on_demand_scans: true)
end end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do describe '#resolve' do
subject do subject do
mutation.resolve( mutation.resolve(
...@@ -47,20 +49,6 @@ RSpec.describe Mutations::DastScannerProfiles::Update do ...@@ -47,20 +49,6 @@ RSpec.describe Mutations::DastScannerProfiles::Update do
end end
end end
context 'when the user is not associated with the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when user can not run a DAST scan' do
it 'raises an exception' do
project.add_guest(user)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user can run a DAST scan' do context 'when the user can run a DAST scan' do
before do before do
project.add_developer(user) project.add_developer(user)
...@@ -108,14 +96,6 @@ RSpec.describe Mutations::DastScannerProfiles::Update do ...@@ -108,14 +96,6 @@ RSpec.describe Mutations::DastScannerProfiles::Update do
expect(subject[:errors]).to include('Scanner profile not found for given parameters') expect(subject[:errors]).to include('Scanner profile not found for given parameters')
end end
end end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end end
end end
end end
......
...@@ -17,6 +17,8 @@ RSpec.describe Mutations::DastSiteProfiles::Create do ...@@ -17,6 +17,8 @@ RSpec.describe Mutations::DastSiteProfiles::Create do
stub_licensed_features(security_on_demand_scans: true) stub_licensed_features(security_on_demand_scans: true)
end end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do describe '#resolve' do
subject do subject do
mutation.resolve( mutation.resolve(
...@@ -35,28 +37,6 @@ RSpec.describe Mutations::DastSiteProfiles::Create do ...@@ -35,28 +37,6 @@ RSpec.describe Mutations::DastSiteProfiles::Create do
end end
end end
context 'when the user is not associated with the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is an owner' do
it 'returns the dast_site_profile id' do
group.add_owner(user)
expect(subject[:id]).to eq(dast_site_profile.to_global_id)
end
end
context 'when the user is a maintainer' do
it 'returns the dast_site_profile id' do
project.add_maintainer(user)
expect(subject[:id]).to eq(dast_site_profile.to_global_id)
end
end
context 'when the user can run a dast scan' do context 'when the user can run a dast scan' do
before do before do
project.add_developer(user) project.add_developer(user)
...@@ -89,14 +69,6 @@ RSpec.describe Mutations::DastSiteProfiles::Create do ...@@ -89,14 +69,6 @@ RSpec.describe Mutations::DastSiteProfiles::Create do
expect(response[:errors]).to include('Name has already been taken') expect(response[:errors]).to include('Name has already been taken')
end end
end end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end end
end end
end end
......
...@@ -15,6 +15,8 @@ RSpec.describe Mutations::DastSiteProfiles::Delete do ...@@ -15,6 +15,8 @@ RSpec.describe Mutations::DastSiteProfiles::Delete do
stub_licensed_features(security_on_demand_scans: true) stub_licensed_features(security_on_demand_scans: true)
end end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do describe '#resolve' do
subject do subject do
mutation.resolve( mutation.resolve(
...@@ -32,52 +34,6 @@ RSpec.describe Mutations::DastSiteProfiles::Delete do ...@@ -32,52 +34,6 @@ RSpec.describe Mutations::DastSiteProfiles::Delete do
end end
end end
context 'when the user is not associated with the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is an owner' do
it 'has no errors' do
group.add_owner(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a maintainer' do
it 'has no errors' do
project.add_maintainer(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a developer' do
it 'has no errors' do
project.add_developer(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a reporter' do
it 'raises an exception' do
project.add_reporter(user)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is a guest' do
it 'raises an exception' do
project.add_guest(user)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user can run a dast scan' do context 'when the user can run a dast scan' do
before do before do
project.add_developer(user) project.add_developer(user)
...@@ -96,14 +52,6 @@ RSpec.describe Mutations::DastSiteProfiles::Delete do ...@@ -96,14 +52,6 @@ RSpec.describe Mutations::DastSiteProfiles::Delete do
expect(subject[:errors]).to include('Name is weird') expect(subject[:errors]).to include('Name is weird')
end end
end end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end end
end end
end end
......
...@@ -18,6 +18,8 @@ RSpec.describe Mutations::DastSiteProfiles::Update do ...@@ -18,6 +18,8 @@ RSpec.describe Mutations::DastSiteProfiles::Update do
stub_licensed_features(security_on_demand_scans: true) stub_licensed_features(security_on_demand_scans: true)
end end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do describe '#resolve' do
subject do subject do
mutation.resolve( mutation.resolve(
...@@ -37,52 +39,6 @@ RSpec.describe Mutations::DastSiteProfiles::Update do ...@@ -37,52 +39,6 @@ RSpec.describe Mutations::DastSiteProfiles::Update do
end end
end end
context 'when the user is not associated with the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is an owner' do
it 'has no errors' do
group.add_owner(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a maintainer' do
it 'has no errors' do
project.add_maintainer(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a developer' do
it 'has no errors' do
project.add_developer(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a reporter' do
it 'raises an exception' do
project.add_reporter(user)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is a guest' do
it 'raises an exception' do
project.add_guest(user)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user can run a dast scan' do context 'when the user can run a dast scan' do
before do before do
project.add_developer(user) project.add_developer(user)
...@@ -96,14 +52,6 @@ RSpec.describe Mutations::DastSiteProfiles::Update do ...@@ -96,14 +52,6 @@ RSpec.describe Mutations::DastSiteProfiles::Update do
expect(dast_site_profile.dast_site.url).to eq(new_target_url) expect(dast_site_profile.dast_site.url).to eq(new_target_url)
end end
end end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end end
end end
end end
......
...@@ -18,6 +18,8 @@ RSpec.describe Mutations::DastSiteTokens::Create do ...@@ -18,6 +18,8 @@ RSpec.describe Mutations::DastSiteTokens::Create do
allow(SecureRandom).to receive(:uuid).and_return(uuid) allow(SecureRandom).to receive(:uuid).and_return(uuid)
end end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do describe '#resolve' do
subject do subject do
mutation.resolve( mutation.resolve(
...@@ -35,28 +37,6 @@ RSpec.describe Mutations::DastSiteTokens::Create do ...@@ -35,28 +37,6 @@ RSpec.describe Mutations::DastSiteTokens::Create do
end end
end end
context 'when the user is not associated with the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is an owner' do
it 'returns the dast_site_token id' do
group.add_owner(user)
expect(subject[:id]).to eq(dast_site_token.to_global_id)
end
end
context 'when the user is a maintainer' do
it 'returns the dast_site_token id' do
project.add_maintainer(user)
expect(subject[:id]).to eq(dast_site_token.to_global_id)
end
end
context 'when the user can run a dast scan' do context 'when the user can run a dast scan' do
before do before do
project.add_developer(user) project.add_developer(user)
...@@ -94,14 +74,6 @@ RSpec.describe Mutations::DastSiteTokens::Create do ...@@ -94,14 +74,6 @@ RSpec.describe Mutations::DastSiteTokens::Create do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end end
end end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end end
end end
end end
......
...@@ -17,6 +17,8 @@ RSpec.describe Mutations::DastSiteValidations::Create do ...@@ -17,6 +17,8 @@ RSpec.describe Mutations::DastSiteValidations::Create do
stub_licensed_features(security_on_demand_scans: true) stub_licensed_features(security_on_demand_scans: true)
end end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do describe '#resolve' do
subject do subject do
mutation.resolve( mutation.resolve(
...@@ -36,28 +38,6 @@ RSpec.describe Mutations::DastSiteValidations::Create do ...@@ -36,28 +38,6 @@ RSpec.describe Mutations::DastSiteValidations::Create do
end end
end end
context 'when the user is not associated with the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is an owner' do
it 'returns the dast_site_validation id' do
group.add_owner(user)
expect(subject[:id]).to eq(dast_site_validation.to_global_id)
end
end
context 'when the user is a maintainer' do
it 'returns the dast_site_validation id' do
project.add_maintainer(user)
expect(subject[:id]).to eq(dast_site_validation.to_global_id)
end
end
context 'when the user can run a dast scan' do context 'when the user can run a dast scan' do
before do before do
project.add_developer(user) project.add_developer(user)
...@@ -78,14 +58,6 @@ RSpec.describe Mutations::DastSiteValidations::Create do ...@@ -78,14 +58,6 @@ RSpec.describe Mutations::DastSiteValidations::Create do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end end
end end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end end
end end
end end
......
...@@ -78,5 +78,16 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do ...@@ -78,5 +78,16 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
expect(empty_report.scan).to be(nil) expect(empty_report.scan).to be(nil)
end end
end end
context 'parsing links' do
it 'returns links object for each finding', :aggregate_failures do
links = report.findings.flat_map(&:links)
expect(links.map(&:url)).to match_array(['https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020', 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030'])
expect(links.map(&:name)).to match_array([nil, 'CVE-1030'])
expect(links.size).to eq(2)
expect(links.first).to be_a(::Gitlab::Ci::Reports::Security::Link)
end
end
end end
end end
...@@ -10,6 +10,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do ...@@ -10,6 +10,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
let(:primary_identifier) { create(:ci_reports_security_identifier) } let(:primary_identifier) { create(:ci_reports_security_identifier) }
let(:other_identifier) { create(:ci_reports_security_identifier) } let(:other_identifier) { create(:ci_reports_security_identifier) }
let(:link) { create(:ci_reports_security_link) }
let(:scanner) { create(:ci_reports_security_scanner) } let(:scanner) { create(:ci_reports_security_scanner) }
let(:location) { create(:ci_reports_security_locations_sast) } let(:location) { create(:ci_reports_security_locations_sast) }
...@@ -18,6 +19,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do ...@@ -18,6 +19,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
compare_key: 'this_is_supposed_to_be_a_unique_value', compare_key: 'this_is_supposed_to_be_a_unique_value',
confidence: :medium, confidence: :medium,
identifiers: [primary_identifier, other_identifier], identifiers: [primary_identifier, other_identifier],
links: [link],
location: location, location: location,
metadata_version: 'sast:1.0', metadata_version: 'sast:1.0',
name: 'Cipher with no integrity', name: 'Cipher with no integrity',
...@@ -39,6 +41,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do ...@@ -39,6 +41,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
confidence: :medium, confidence: :medium,
project_fingerprint: '9a73f32d58d87d94e3dc61c4c1a94803f6014258', project_fingerprint: '9a73f32d58d87d94e3dc61c4c1a94803f6014258',
identifiers: [primary_identifier, other_identifier], identifiers: [primary_identifier, other_identifier],
links: [link],
location: location, location: location,
metadata_version: 'sast:1.0', metadata_version: 'sast:1.0',
name: 'Cipher with no integrity', name: 'Cipher with no integrity',
...@@ -84,6 +87,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do ...@@ -84,6 +87,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
compare_key: occurrence.compare_key, compare_key: occurrence.compare_key,
confidence: occurrence.confidence, confidence: occurrence.confidence,
identifiers: occurrence.identifiers, identifiers: occurrence.identifiers,
links: occurrence.links,
location: occurrence.location, location: occurrence.location,
metadata_version: occurrence.metadata_version, metadata_version: occurrence.metadata_version,
name: occurrence.name, name: occurrence.name,
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::Security::Link do
subject(:security_link) { described_class.new(name: 'CVE-2020-0202', url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-0202') }
describe '#initialize' do
context 'when all params are given' do
it 'initializes an instance' do
expect { subject }.not_to raise_error
expect(subject).to have_attributes(
name: 'CVE-2020-0202',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-0202'
)
end
end
describe '#to_hash' do
it 'returns expected hash' do
expect(security_link.to_hash).to eq(
{
name: 'CVE-2020-0202',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-0202'
}
)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Vulnerabilities::FindingLink do
describe 'associations' do
it { is_expected.to belong_to(:finding).class_name('Vulnerabilities::Finding') }
end
describe 'validations' do
let_it_be(:link) { create(:finding_link) }
it { is_expected.to validate_presence_of(:url) }
it { is_expected.to validate_length_of(:url).is_at_most(255) }
it { is_expected.to validate_length_of(:name).is_at_most(2048) }
it { is_expected.to validate_presence_of(:finding) }
end
end
...@@ -16,6 +16,7 @@ RSpec.describe Vulnerabilities::Finding do ...@@ -16,6 +16,7 @@ RSpec.describe Vulnerabilities::Finding do
it { is_expected.to have_many(:finding_pipelines).class_name('Vulnerabilities::FindingPipeline').with_foreign_key('occurrence_id') } it { is_expected.to have_many(:finding_pipelines).class_name('Vulnerabilities::FindingPipeline').with_foreign_key('occurrence_id') }
it { is_expected.to have_many(:identifiers).class_name('Vulnerabilities::Identifier') } it { is_expected.to have_many(:identifiers).class_name('Vulnerabilities::Identifier') }
it { is_expected.to have_many(:finding_identifiers).class_name('Vulnerabilities::FindingIdentifier').with_foreign_key('occurrence_id') } it { is_expected.to have_many(:finding_identifiers).class_name('Vulnerabilities::FindingIdentifier').with_foreign_key('occurrence_id') }
it { is_expected.to have_many(:finding_links).class_name('Vulnerabilities::FindingLink').with_foreign_key('vulnerability_occurrence_id') }
end end
describe 'validations' do describe 'validations' do
...@@ -405,6 +406,33 @@ RSpec.describe Vulnerabilities::Finding do ...@@ -405,6 +406,33 @@ RSpec.describe Vulnerabilities::Finding do
end end
end end
describe '#links' do
let_it_be(:finding, reload: true) do
create(
:vulnerabilities_finding,
raw_metadata: {
links: [{ url: 'https://raw.gitlab.com', name: 'raw_metadata_link' }]
}.to_json
)
end
subject(:links) { finding.links }
context 'when there are no finding links' do
it 'returns links from raw_metadata' do
expect(links).to eq([{ 'url' => 'https://raw.gitlab.com', 'name' => 'raw_metadata_link' }])
end
end
context 'when there are finding links assigned to given finding' do
let_it_be(:finding_link) { create(:finding_link, name: 'finding_link', url: 'https://link.gitlab.com', finding: finding) }
it 'returns links from finding link' do
expect(links).to eq([{ 'url' => 'https://link.gitlab.com', 'name' => 'finding_link' }])
end
end
end
describe 'feedback' do describe 'feedback' do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let(:finding) do let(:finding) do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DastScannerProfilePolicy do
it_behaves_like 'a dast on-demand scan policy' do
let_it_be(:record) { create(:dast_scanner_profile, project: project) }
end
end
...@@ -3,43 +3,7 @@ ...@@ -3,43 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe DastSiteProfilePolicy do RSpec.describe DastSiteProfilePolicy do
describe 'create_on_demand_dast_scan' do it_behaves_like 'a dast on-demand scan policy' do
let(:dast_site_profile) { create(:dast_site_profile) } let_it_be(:record) { create(:dast_site_profile, project: project) }
let(:project) { dast_site_profile.project }
let(:user) { create(:user) }
subject { described_class.new(user, dast_site_profile) }
before do
stub_licensed_features(security_on_demand_scans: true)
end
context 'when a user does not have access to the project' do
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
context 'when a user does not have access to dast_site_profiles' do
before do
project.add_guest(user)
end
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
context 'when a user has access dast_site_profiles' do
before do
project.add_developer(user)
end
it { is_expected.to be_allowed(:create_on_demand_dast_scan) }
context 'when on demand scan licensed feature is not available' do
before do
stub_licensed_features(security_on_demand_scans: false)
end
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
end
end end
end end
...@@ -3,43 +3,7 @@ ...@@ -3,43 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe DastSiteValidationPolicy do RSpec.describe DastSiteValidationPolicy do
describe 'create_on_demand_dast_scan' do it_behaves_like 'a dast on-demand scan policy' do
let_it_be(:dast_site_validation, reload: true) { create(:dast_site_validation) } let_it_be(:record) { create(:dast_site_validation, dast_site_token: create(:dast_site_token, project: project)) }
let_it_be(:project) { dast_site_validation.dast_site_token.project }
let_it_be(:user) { create(:user) }
subject { described_class.new(user, dast_site_validation) }
before do
stub_licensed_features(security_on_demand_scans: true)
end
context 'when a user does not have access to the project' do
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
context 'when a user does not have access to dast_site_validations' do
before do
project.add_guest(user)
end
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
context 'when a user has access dast_site_validations' do
before do
project.add_developer(user)
end
it { is_expected.to be_allowed(:create_on_demand_dast_scan) }
context 'when on demand scan licensed feature is not available' do
before do
stub_licensed_features(security_on_demand_scans: false)
end
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
end
end end
end end
...@@ -22,6 +22,7 @@ RSpec.describe API::Search, factory_default: :keep do ...@@ -22,6 +22,7 @@ RSpec.describe API::Search, factory_default: :keep do
it 'returns a different result for each page' do it 'returns a different result for each page' do
get api(endpoint, user), params: { scope: scope, search: search, page: 1, per_page: 1 } get api(endpoint, user), params: { scope: scope, search: search, page: 1, per_page: 1 }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.count).to eq(1) expect(json_response.count).to eq(1)
first = json_response.first first = json_response.first
...@@ -37,6 +38,30 @@ RSpec.describe API::Search, factory_default: :keep do ...@@ -37,6 +38,30 @@ RSpec.describe API::Search, factory_default: :keep do
end end
end end
shared_examples 'orderable by created_at' do |scope:|
it 'allows ordering results by created_at asc' do
get api(endpoint, user), params: { scope: scope, search: '*', order_by: 'created_at', sort: 'asc' }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.count).to be > 1
created_ats = json_response.map { |r| Time.parse(r['created_at']) }
expect(created_ats).to eq(created_ats.sort)
end
it 'allows ordering results by created_at desc' do
get api(endpoint, user), params: { scope: scope, search: '*', order_by: 'created_at', sort: 'desc' }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.count).to be > 1
created_ats = json_response.map { |r| Time.parse(r['created_at']) }
expect(created_ats).to eq(created_ats.sort.reverse)
end
end
shared_examples 'elasticsearch disabled' do shared_examples 'elasticsearch disabled' do
it 'returns 400 error for wiki_blobs, blobs and commits scope' do it 'returns 400 error for wiki_blobs, blobs and commits scope' do
get api(endpoint, user), params: { scope: 'wiki_blobs', search: 'awesome' } get api(endpoint, user), params: { scope: 'wiki_blobs', search: 'awesome' }
...@@ -61,6 +86,7 @@ RSpec.describe API::Search, factory_default: :keep do ...@@ -61,6 +86,7 @@ RSpec.describe API::Search, factory_default: :keep do
end end
it_behaves_like 'pagination', scope: 'merge_requests' it_behaves_like 'pagination', scope: 'merge_requests'
it_behaves_like 'orderable by created_at', scope: 'merge_requests'
it 'avoids N+1 queries' do it 'avoids N+1 queries' do
control = ActiveRecord::QueryRecorder.new { get api(endpoint, user), params: { scope: 'merge_requests', search: '*' } } control = ActiveRecord::QueryRecorder.new { get api(endpoint, user), params: { scope: 'merge_requests', search: '*' } }
...@@ -213,6 +239,7 @@ RSpec.describe API::Search, factory_default: :keep do ...@@ -213,6 +239,7 @@ RSpec.describe API::Search, factory_default: :keep do
end end
it_behaves_like 'pagination', scope: 'issues' it_behaves_like 'pagination', scope: 'issues'
it_behaves_like 'orderable by created_at', scope: 'issues'
end end
unless level == :project unless level == :project
......
...@@ -24,11 +24,11 @@ RSpec.describe Security::StoreReportService, '#execute' do ...@@ -24,11 +24,11 @@ RSpec.describe Security::StoreReportService, '#execute' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
where(:case_name, :trait, :scanners, :identifiers, :findings, :finding_identifiers, :finding_pipelines) do where(:case_name, :trait, :scanners, :identifiers, :findings, :finding_identifiers, :finding_pipelines, :finding_links) do
'with SAST report' | :sast | 3 | 17 | 33 | 39 | 33 'with SAST report' | :sast | 3 | 17 | 33 | 39 | 33 | 0
'with exceeding identifiers' | :with_exceeding_identifiers | 1 | 20 | 1 | 20 | 1 'with exceeding identifiers' | :with_exceeding_identifiers | 1 | 20 | 1 | 20 | 1 | 0
'with Dependency Scanning report' | :dependency_scanning | 2 | 7 | 4 | 7 | 4 'with Dependency Scanning report' | :dependency_scanning | 2 | 7 | 4 | 7 | 4 | 6
'with Container Scanning report' | :container_scanning | 1 | 8 | 8 | 8 | 8 'with Container Scanning report' | :container_scanning | 1 | 8 | 8 | 8 | 8 | 8
end end
with_them do with_them do
......
# frozen_string_literal: true
RSpec.shared_examples 'a dast on-demand scan policy' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }
subject { described_class.new(user, record) }
before do
stub_licensed_features(security_on_demand_scans: true)
end
describe 'create_on_demand_dast_scan' do
context 'when a user does not have access to the project' do
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
context 'when the user is a guest' do
before do
project.add_guest(user)
end
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
context 'when the user is a reporter' do
before do
project.add_reporter(user)
end
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
context 'when the user is a developer' do
before do
project.add_developer(user)
end
it { is_expected.to be_allowed(:create_on_demand_dast_scan) }
end
context 'when the user is a maintainer' do
before do
project.add_maintainer(user)
end
it { is_expected.to be_allowed(:create_on_demand_dast_scan) }
end
context 'when the user is an owner' do
before do
group.add_owner(user)
end
it { is_expected.to be_allowed(:create_on_demand_dast_scan) }
end
context 'when the user is allowed' do
before do
project.add_developer(user)
end
context 'when on demand scan licensed feature is not available' do
let(:project) { create(:project, group: group) } # allows license stub to work correctly
before do
stub_licensed_features(security_on_demand_scans: false)
end
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
end
end
end
...@@ -186,6 +186,7 @@ module API ...@@ -186,6 +186,7 @@ module API
mount ::API::ImportBitbucketServer mount ::API::ImportBitbucketServer
mount ::API::ImportGithub mount ::API::ImportGithub
mount ::API::IssueLinks mount ::API::IssueLinks
mount ::API::Invitations
mount ::API::Issues mount ::API::Issues
mount ::API::JobArtifacts mount ::API::JobArtifacts
mount ::API::Jobs mount ::API::Jobs
......
# frozen_string_literal: true
module API
module Entities
class Invitation < Grape::Entity
expose :access_level
expose :requested_at
expose :expires_at
expose :invite_email
expose :invite_token
expose :user_id
end
end
end
# frozen_string_literal: true
module API
class Invitations < ::API::Base
feature_category :users
before { authenticate! }
helpers ::API::Helpers::MembersHelpers
%w[group project].each do |source_type|
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Invite non-members by email address to a group or project.' do
detail 'This feature was introduced in GitLab 13.6'
success Entities::Invitation
end
params do
requires :email, types: [String, Array[String]], email_or_email_list: true, desc: 'The email address to invite, or multiple emails separated by comma'
requires :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)'
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
end
post ":id/invitations" do
source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source)
::Members::InviteService.new(current_user, params).execute(source)
end
end
end
end
end
...@@ -39,7 +39,9 @@ module API ...@@ -39,7 +39,9 @@ module API
snippets: snippets?, snippets: snippets?,
basic_search: params[:basic_search], basic_search: params[:basic_search],
page: params[:page], page: params[:page],
per_page: params[:per_page] per_page: params[:per_page],
order_by: params[:order_by],
sort: params[:sort]
}.merge(additional_params) }.merge(additional_params)
results = SearchService.new(current_user, search_params).search_objects(preload_method) results = SearchService.new(current_user, search_params).search_objects(preload_method)
......
# frozen_string_literal: true
module API
module Validations
module Validators
class EmailOrEmailList < Grape::Validations::Base
def validate_param!(attr_name, params)
value = params[attr_name]
return unless value
return if value.split(',').map { |v| ValidateEmail.valid?(v) }.all?
raise Grape::Exceptions::Validation,
params: [@scope.full_name(attr_name)],
message: "contains an invalid email address"
end
end
end
end
end
...@@ -81,11 +81,15 @@ module Gitlab ...@@ -81,11 +81,15 @@ module Gitlab
# We are ignoring connections and built in types for now, # We are ignoring connections and built in types for now,
# they should be added when queries are generated. # they should be added when queries are generated.
def objects def objects
graphql_object_types.select do |object_type| object_types = graphql_object_types.select do |object_type|
!object_type[:name]["Connection"] && !object_type[:name]["Connection"] &&
!object_type[:name]["Edge"] && !object_type[:name]["Edge"] &&
!object_type[:name]["__"] !object_type[:name]["__"]
end end
object_types.each do |type|
type[:fields] += type[:connections]
end
end end
# We ignore the built-in enum types. # We ignore the built-in enum types.
......
...@@ -4,10 +4,10 @@ module Gitlab ...@@ -4,10 +4,10 @@ module Gitlab
class GroupSearchResults < SearchResults class GroupSearchResults < SearchResults
attr_reader :group attr_reader :group
def initialize(current_user, query, limit_projects = nil, group:, default_project_filter: false, sort: nil, filters: {}) def initialize(current_user, query, limit_projects = nil, group:, default_project_filter: false, order_by: nil, sort: nil, filters: {})
@group = group @group = group
super(current_user, query, limit_projects, default_project_filter: default_project_filter, sort: sort, filters: filters) super(current_user, query, limit_projects, default_project_filter: default_project_filter, order_by: order_by, sort: sort, filters: filters)
end end
# rubocop:disable CodeReuse/ActiveRecord # rubocop:disable CodeReuse/ActiveRecord
......
...@@ -4,11 +4,11 @@ module Gitlab ...@@ -4,11 +4,11 @@ module Gitlab
class ProjectSearchResults < SearchResults class ProjectSearchResults < SearchResults
attr_reader :project, :repository_ref attr_reader :project, :repository_ref
def initialize(current_user, query, project:, repository_ref: nil, sort: nil, filters: {}) def initialize(current_user, query, project:, repository_ref: nil, order_by: nil, sort: nil, filters: {})
@project = project @project = project
@repository_ref = repository_ref.presence @repository_ref = repository_ref.presence
super(current_user, query, [project], sort: sort, filters: filters) super(current_user, query, [project], order_by: order_by, sort: sort, filters: filters)
end end
def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil) def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil)
......
# frozen_string_literal: true
module Gitlab
module Search
module SortOptions
def sort_and_direction(order_by, sort)
# Due to different uses of sort param in web vs. API requests we prefer
# order_by when present
case [order_by, sort]
when %w[created_at asc], [nil, 'created_asc']
:created_at_asc
when %w[created_at desc], [nil, 'created_desc']
:created_at_desc
else
:unknown
end
end
module_function :sort_and_direction # rubocop: disable Style/AccessModifierDeclarations
end
end
end
...@@ -7,7 +7,7 @@ module Gitlab ...@@ -7,7 +7,7 @@ module Gitlab
DEFAULT_PAGE = 1 DEFAULT_PAGE = 1
DEFAULT_PER_PAGE = 20 DEFAULT_PER_PAGE = 20
attr_reader :current_user, :query, :sort, :filters attr_reader :current_user, :query, :order_by, :sort, :filters
# Limit search results by passed projects # Limit search results by passed projects
# It allows us to search only for projects user has access to # It allows us to search only for projects user has access to
...@@ -19,11 +19,12 @@ module Gitlab ...@@ -19,11 +19,12 @@ module Gitlab
# query # query
attr_reader :default_project_filter attr_reader :default_project_filter
def initialize(current_user, query, limit_projects = nil, sort: nil, default_project_filter: false, filters: {}) def initialize(current_user, query, limit_projects = nil, order_by: nil, sort: nil, default_project_filter: false, filters: {})
@current_user = current_user @current_user = current_user
@query = query @query = query
@limit_projects = limit_projects || Project.all @limit_projects = limit_projects || Project.all
@default_project_filter = default_project_filter @default_project_filter = default_project_filter
@order_by = order_by
@sort = sort @sort = sort
@filters = filters @filters = filters
end end
...@@ -128,10 +129,12 @@ module Gitlab ...@@ -128,10 +129,12 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def apply_sort(scope) def apply_sort(scope)
case sort # Due to different uses of sort param we prefer order_by when
when 'created_asc' # present
case ::Gitlab::Search::SortOptions.sort_and_direction(order_by, sort)
when :created_at_asc
scope.reorder('created_at ASC') scope.reorder('created_at ASC')
when 'created_desc' when :created_at_desc
scope.reorder('created_at DESC') scope.reorder('created_at DESC')
else else
scope.reorder('created_at DESC') scope.reorder('created_at DESC')
......
...@@ -9866,6 +9866,9 @@ msgstr "" ...@@ -9866,6 +9866,9 @@ msgstr ""
msgid "Email Notification" msgid "Email Notification"
msgstr "" msgstr ""
msgid "Email cannot be blank"
msgstr ""
msgid "Email could not be sent" msgid "Email could not be sent"
msgstr "" msgstr ""
...@@ -16545,7 +16548,7 @@ msgstr "" ...@@ -16545,7 +16548,7 @@ msgstr ""
msgid "Members|%{time} by %{user}" msgid "Members|%{time} by %{user}"
msgstr "" msgstr ""
msgid "Members|%{userName} is currently a LDAP user. Editing their permissions will override the settings from the LDAP group sync." msgid "Members|%{userName} is currently an LDAP user. Editing their permissions will override the settings from the LDAP group sync."
msgstr "" msgstr ""
msgid "Members|An error occurred while trying to enable LDAP override, please try again." msgid "Members|An error occurred while trying to enable LDAP override, please try again."
...@@ -28202,6 +28205,9 @@ msgstr "" ...@@ -28202,6 +28205,9 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API." msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr "" msgstr ""
msgid "Too many users specified (limit is %{user_limit})"
msgstr ""
msgid "Too much data" msgid "Too much data"
msgstr "" msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::GroupInvitationType do
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Group) }
specify { expect(described_class.graphql_name).to eq('GroupInvitation') }
specify { expect(described_class).to require_graphql_authorizations(:read_group) }
it 'has the expected fields' do
expected_fields = %w[
email access_level created_by created_at updated_at expires_at group
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::InvitationInterface do
it 'exposes the expected fields' do
expected_fields = %i[
email
access_level
created_by
created_at
updated_at
expires_at
user
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
describe '.resolve_type' do
subject { described_class.resolve_type(object, {}) }
context 'for project member' do
let(:object) { build(:project_member) }
it { is_expected.to be Types::ProjectInvitationType }
end
context 'for group member' do
let(:object) { build(:group_member) }
it { is_expected.to be Types::GroupInvitationType }
end
context 'for an unknown type' do
let(:object) { build(:user) }
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::BaseError)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::ProjectInvitationType do
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Project) }
specify { expect(described_class.graphql_name).to eq('ProjectInvitation') }
specify { expect(described_class).to require_graphql_authorizations(:read_project) }
it 'has the expected fields' do
expected_fields = %w[
access_level created_by created_at updated_at expires_at project user
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Validations::Validators::EmailOrEmailList do
include ApiValidatorsHelpers
subject do
described_class.new(['email'], {}, false, scope.new)
end
context 'with valid email addresses' do
it 'does not raise a validation error' do
expect_no_validation_error('test' => 'test@example.org')
expect_no_validation_error('test' => 'test1@example.com,test2@example.org')
expect_no_validation_error('test' => 'test1@example.com,test2@example.org,test3@example.co.uk')
end
end
context 'including any invalid email address' do
it 'raises a validation error' do
expect_validation_error('test' => 'not')
expect_validation_error('test' => '@example.com')
expect_validation_error('test' => 'test1@example.com,asdf')
expect_validation_error('test' => 'asdf,testa1@example.com,asdf')
end
end
end
# frozen_string_literal: true
require 'fast_spec_helper'
require 'gitlab/search/sort_options'
RSpec.describe ::Gitlab::Search::SortOptions do
describe '.sort_and_direction' do
context 'using order_by and sort' do
it 'returns matched options' do
expect(described_class.sort_and_direction('created_at', 'asc')).to eq(:created_at_asc)
expect(described_class.sort_and_direction('created_at', 'desc')).to eq(:created_at_desc)
end
end
context 'using just sort' do
it 'returns matched options' do
expect(described_class.sort_and_direction(nil, 'created_asc')).to eq(:created_at_asc)
expect(described_class.sort_and_direction(nil, 'created_desc')).to eq(:created_at_desc)
end
end
context 'when unknown option' do
it 'returns unknown' do
expect(described_class.sort_and_direction(nil, 'foo_asc')).to eq(:unknown)
expect(described_class.sort_and_direction(nil, 'bar_desc')).to eq(:unknown)
expect(described_class.sort_and_direction(nil, 'created_bar')).to eq(:unknown)
expect(described_class.sort_and_direction('created_at', 'foo')).to eq(:unknown)
expect(described_class.sort_and_direction('foo', 'desc')).to eq(:unknown)
expect(described_class.sort_and_direction('created_at', nil)).to eq(:unknown)
end
end
end
end
...@@ -3,13 +3,5 @@ ...@@ -3,13 +3,5 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe FromUnion do RSpec.describe FromUnion do
[true, false].each do |sql_set_operator| it_behaves_like 'from set operator', Gitlab::SQL::Union
context "when sql-set-operators feature flag is #{sql_set_operator}" do
before do
stub_feature_flags(sql_set_operators: sql_set_operator)
end
it_behaves_like 'from set operator', Gitlab::SQL::Union
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Invitations do
let(:maintainer) { create(:user, username: 'maintainer_user') }
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
let(:stranger) { create(:user) }
let(:email) { 'email@example.org' }
let(:project) do
create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
project.add_developer(developer)
project.add_maintainer(maintainer)
project.request_access(access_requester)
end
end
let!(:group) do
create(:group, :public) do |group|
group.add_developer(developer)
group.add_owner(maintainer)
group.request_access(access_requester)
end
end
def invitations_url(source, user)
api("/#{source.model_name.plural}/#{source.id}/invitations", user)
end
shared_examples 'POST /:source_type/:id/invitations' do |source_type|
context "with :source_type == #{source_type.pluralize}" do
it_behaves_like 'a 404 response when source is private' do
let(:route) do
post invitations_url(source, stranger),
params: { email: email, access_level: Member::MAINTAINER }
end
end
context 'when authenticated as a non-member or member with insufficient rights' do
%i[access_requester stranger developer].each do |type|
context "as a #{type}" do
it 'returns 403' do
user = public_send(type)
post invitations_url(source, user), params: { email: email, access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
context 'when authenticated as a maintainer/owner' do
context 'and new member is already a requester' do
it 'does not transform the requester into a proper member' do
expect do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: email, access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:created)
end.not_to change { source.members.count }
end
end
it 'invites a new member' do
expect do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: email, access_level: Member::DEVELOPER }
expect(response).to have_gitlab_http_status(:created)
end.to change { source.requesters.count }.by(1)
end
it 'invites a list of new email addresses' do
expect do
email_list = 'email1@example.com,email2@example.com'
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: email_list, access_level: Member::DEVELOPER }
expect(response).to have_gitlab_http_status(:created)
end.to change { source.requesters.count }.by(2)
end
end
context 'access levels' do
it 'does not create the member if group level is higher' do
parent = create(:group)
group.update!(parent: parent)
project.update!(group: group)
parent.add_developer(stranger)
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: stranger.email, access_level: Member::REPORTER }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['message'][stranger.email]).to eq("Access level should be greater than or equal to Developer inherited membership from group #{parent.name}")
end
it 'creates the member if group level is lower' do
parent = create(:group)
group.update!(parent: parent)
project.update!(group: group)
parent.add_developer(stranger)
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: stranger.email, access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:created)
end
end
context 'access expiry date' do
subject do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: email, access_level: Member::DEVELOPER, expires_at: expires_at }
end
context 'when set to a date in the past' do
let(:expires_at) { 2.days.ago.to_date }
it 'does not create a member' do
expect do
subject
end.not_to change { source.members.count }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['message'][email]).to eq('Expires at cannot be a date in the past')
end
end
context 'when set to a date in the future' do
let(:expires_at) { 2.days.from_now.to_date }
it 'invites a member' do
expect do
subject
end.to change { source.requesters.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
end
end
end
it "returns a message if member already exists" do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: maintainer.email, access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['message'][maintainer.email]).to eq("Already a member of #{source.name}")
end
it 'returns 404 when the email is not valid' do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: '', access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['message']).to eq('Email cannot be blank')
end
it 'returns 404 when the email list is not a valid format' do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: 'email1@example.com,not-an-email', access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('email contains an invalid email address')
end
it 'returns 400 when email is not given' do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'returns 400 when access_level is not given' do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: email }
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'returns 400 when access_level is not valid' do
post invitations_url(source, maintainer),
params: { email: email, access_level: non_existing_record_access_level }
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
describe 'POST /projects/:id/invitations' do
it_behaves_like 'POST /:source_type/:id/invitations', 'project' do
let(:source) { project }
end
end
describe 'POST /groups/:id/invitations' do
it_behaves_like 'POST /:source_type/:id/invitations', 'group' do
let(:source) { group }
end
end
end
...@@ -23,6 +23,48 @@ RSpec.describe API::Search do ...@@ -23,6 +23,48 @@ RSpec.describe API::Search do
end end
end end
shared_examples 'orderable by created_at' do |scope:|
it 'allows ordering results by created_at asc' do
get api(endpoint, user), params: { scope: scope, search: 'sortable', order_by: 'created_at', sort: 'asc' }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.count).to be > 1
created_ats = json_response.map { |r| Time.parse(r['created_at']) }
expect(created_ats.uniq.count).to be > 1
expect(created_ats).to eq(created_ats.sort)
end
it 'allows ordering results by created_at desc' do
get api(endpoint, user), params: { scope: scope, search: 'sortable', order_by: 'created_at', sort: 'desc' }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.count).to be > 1
created_ats = json_response.map { |r| Time.parse(r['created_at']) }
expect(created_ats.uniq.count).to be > 1
expect(created_ats).to eq(created_ats.sort.reverse)
end
end
shared_examples 'issues orderable by created_at' do
before do
create_list(:issue, 3, title: 'sortable item', project: project)
end
it_behaves_like 'orderable by created_at', scope: :issues
end
shared_examples 'merge_requests orderable by created_at' do
before do
create_list(:merge_request, 3, :unique_branches, title: 'sortable item', target_project: repo_project, source_project: repo_project)
end
it_behaves_like 'orderable by created_at', scope: :merge_requests
end
shared_examples 'pagination' do |scope:, search: ''| shared_examples 'pagination' do |scope:, search: ''|
it 'returns a different result for each page' do it 'returns a different result for each page' do
get api(endpoint, user), params: { scope: scope, search: search, page: 1, per_page: 1 } get api(endpoint, user), params: { scope: scope, search: search, page: 1, per_page: 1 }
...@@ -121,6 +163,8 @@ RSpec.describe API::Search do ...@@ -121,6 +163,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :issues it_behaves_like 'ping counters', scope: :issues
it_behaves_like 'issues orderable by created_at'
describe 'pagination' do describe 'pagination' do
before do before do
create(:issue, project: project, title: 'another issue') create(:issue, project: project, title: 'another issue')
...@@ -181,6 +225,8 @@ RSpec.describe API::Search do ...@@ -181,6 +225,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :merge_requests it_behaves_like 'ping counters', scope: :merge_requests
it_behaves_like 'merge_requests orderable by created_at'
describe 'pagination' do describe 'pagination' do
before do before do
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch') create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
...@@ -354,6 +400,8 @@ RSpec.describe API::Search do ...@@ -354,6 +400,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :issues it_behaves_like 'ping counters', scope: :issues
it_behaves_like 'issues orderable by created_at'
describe 'pagination' do describe 'pagination' do
before do before do
create(:issue, project: project, title: 'another issue') create(:issue, project: project, title: 'another issue')
...@@ -374,6 +422,8 @@ RSpec.describe API::Search do ...@@ -374,6 +422,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :merge_requests it_behaves_like 'ping counters', scope: :merge_requests
it_behaves_like 'merge_requests orderable by created_at'
describe 'pagination' do describe 'pagination' do
before do before do
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch') create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
...@@ -506,6 +556,8 @@ RSpec.describe API::Search do ...@@ -506,6 +556,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :issues it_behaves_like 'ping counters', scope: :issues
it_behaves_like 'issues orderable by created_at'
describe 'pagination' do describe 'pagination' do
before do before do
create(:issue, project: project, title: 'another issue') create(:issue, project: project, title: 'another issue')
...@@ -536,6 +588,8 @@ RSpec.describe API::Search do ...@@ -536,6 +588,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :merge_requests it_behaves_like 'ping counters', scope: :merge_requests
it_behaves_like 'merge_requests orderable by created_at'
describe 'pagination' do describe 'pagination' do
before do before do
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch') create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
......
This diff is collapsed.
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