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
end
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")
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
# rubocop: disable Gitlab/Union
extend FromSetOperator
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
end
end
......@@ -96,6 +96,8 @@ class Member < ApplicationRecord
scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
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 :with_source_id, ->(source_id) { where(source_id: source_id) }
......
......@@ -393,7 +393,6 @@ class Namespace < ApplicationRecord
end
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)
if shared_runners_enabled && has_parent? && parent.shared_runners_setting == 'disabled_and_unoverridable'
......@@ -402,7 +401,6 @@ class Namespace < ApplicationRecord
end
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)
if shared_runners_enabled && !new_record?
......
......@@ -1195,7 +1195,6 @@ class Project < ApplicationRecord
end
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)
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
def execute
if @tag_name.present?
@tag_name.match(Gitlab::Regex.composer_package_version_regex).captures[0]
@tag_name.delete_prefix('v')
elsif @branch_name.present?
branch_sufix_or_prefix(@branch_name.match(Gitlab::Regex.composer_package_version_regex))
end
......
......@@ -16,6 +16,7 @@ module Search
Gitlab::SearchResults.new(current_user,
params[:search],
projects,
order_by: params[:order_by],
sort: params[:sort],
filters: { state: params[:state], confidential: params[:confidential] })
end
......
......@@ -16,6 +16,7 @@ module Search
params[:search],
projects,
group: group,
order_by: params[:order_by],
sort: params[:sort],
filters: { state: params[:state], confidential: params[:confidential] }
)
......
......@@ -17,6 +17,7 @@ module Search
params[:search],
project: project,
repository_ref: params[:repository_ref],
order_by: params[:order_by],
sort: params[:sort],
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
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(: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
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 (
id bigint 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
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_identifiers ALTER COLUMN id SET DEFAULT nextval('vulnerability_identifiers_id_seq'::regclass);
......@@ -19646,6 +19668,9 @@ ALTER TABLE ONLY vulnerability_exports
ALTER TABLE ONLY vulnerability_feedback
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
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
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_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
ALTER TABLE ONLY board_group_recent_visits
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
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:
| [Events](events.md) | `/projects/:id/events` (also available for users and standalone) |
| [Feature Flags](feature_flags.md) | `/projects/:id/feature_flags` |
| [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 Statistics](issues_statistics.md) | `/projects/:id/issues_statistics` (also available for groups and standalone) |
| [Issue boards](boards.md) | `/projects/:id/boards` |
......@@ -108,6 +109,7 @@ The following API resources are available in the group context:
| [Group labels](group_labels.md) | `/groups/:id/labels` |
| [Group-level variables](group_level_variables.md) | `/groups/:id/variables` |
| [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 Statistics](issues_statistics.md) | `/groups/:id/issues_statistics` (also available for projects and standalone) |
| [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
| `search` | string | yes | The search query |
| `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. |
| `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.
......@@ -436,6 +438,8 @@ GET /groups/:id/search
| `search` | string | yes | The search query |
| `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. |
| `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.
......@@ -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. |
| `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. |
| `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.
......
......@@ -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`.
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
Custom validators are a great way to validate parameters before sending
......
......@@ -258,6 +258,7 @@ Table description links:
| [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 |
| [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](#pgbouncer) | Database connection pooling, failover | ⚙ | ❌ | ❌ | ✅ | ❌ | ❌ | EE Only |
| [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
[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
- [Project page](https://github.com/pgbouncer/pgbouncer/blob/master/README.md)
......
......@@ -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. Click **Save changes**.
![Domain restriction by IP address](img/restrict-by-ip.gif)
#### 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.
......@@ -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. 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.
#### Group file templates **(PREMIUM)**
......
This diff is collapsed.
......@@ -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).
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
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
to your project using your project ID, then adding a section to your `package.json` file with a similar URL.
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
[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
......
......@@ -9,7 +9,7 @@ export default {
i18n: {
editPermissions: s__('Members|Edit permissions'),
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.'),
},
......
......@@ -75,7 +75,7 @@ module Security
def normalize_report_findings(report_findings, vulnerabilities)
report_findings.map do |report_finding|
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)
# assigning Vulnerabilities to Findings to enable the computed state
......@@ -84,6 +84,9 @@ module Security
finding.project = pipeline.project
finding.sha = pipeline.sha
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|
Vulnerabilities::Identifier.new(identifier.to_hash)
end
......
......@@ -26,6 +26,8 @@ module Vulnerabilities
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 :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 :pipelines, through: :finding_pipelines, class_name: 'Ci::Pipeline'
......@@ -256,7 +258,9 @@ module Vulnerabilities
end
def links
metadata.fetch('links', [])
return metadata.fetch('links', []) if finding_links.load.empty?
finding_links.as_json(only: [:name, :url])
end
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
params[:search],
elastic_projects,
public_and_internal_projects: elastic_global,
order_by: params[:order_by],
sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] }
)
......
......@@ -30,6 +30,7 @@ module EE
elastic_projects,
group: group,
public_and_internal_projects: elastic_global,
order_by: params[:order_by],
sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] }
)
......
......@@ -15,6 +15,7 @@ module EE
params[:search],
project: project,
repository_ref: repository_ref,
order_by: params[:order_by],
sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] }
)
......
......@@ -47,7 +47,7 @@ module Security
return
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)
update_vulnerability_scanner(finding)
......@@ -60,6 +60,8 @@ module Security
create_or_update_vulnerability_identifier_object(vulnerability_finding, identifier)
end
create_or_update_vulnerability_links(finding, vulnerability_finding)
create_vulnerability_pipeline_object(vulnerability_finding, pipeline)
create_vulnerability(vulnerability_finding, pipeline)
......@@ -125,6 +127,15 @@ module Security
rescue ActiveRecord::RecordNotUnique
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)
vulnerability_finding.finding_pipelines.find_or_create_by!(pipeline: pipeline)
rescue ActiveRecord::RecordNotUnique
......
---
title: Add Vulnerabilities::FindingLink model
merge_request: 46555
author:
type: added
......@@ -137,14 +137,16 @@ module Elastic
end
def apply_sort(query_hash, options)
case options[:sort]
when 'created_asc'
# Due to different uses of sort param we prefer order_by when
# present
case ::Gitlab::Search::SortOptions.sort_and_direction(options[:order_by], options[:sort])
when :created_at_asc
query_hash.merge(sort: {
created_at: {
order: 'asc'
}
})
when 'created_desc'
when :created_at_desc
query_hash.merge(sort: {
created_at: {
order: 'desc'
......
......@@ -55,6 +55,7 @@ module Gitlab
def create_vulnerability(report, data, version)
identifiers = create_identifiers(report, data['identifiers'])
links = create_links(report, data['links'])
report.add_finding(
::Gitlab::Ci::Reports::Security::Finding.new(
uuid: SecureRandom.uuid,
......@@ -67,6 +68,7 @@ module Gitlab
scanner: create_scanner(report, data['scanner']),
scan: report&.scan,
identifiers: identifiers,
links: links,
raw_metadata: data.to_json,
metadata_version: version))
end
......@@ -106,6 +108,22 @@ module Gitlab
url: identifier['url']))
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)
return input if ::Vulnerabilities::Finding::SEVERITY_LEVELS.key?(input)
......
......@@ -10,6 +10,7 @@ module Gitlab
attr_reader :compare_key
attr_reader :confidence
attr_reader :identifiers
attr_reader :links
attr_reader :location
attr_reader :metadata_version
attr_reader :name
......@@ -24,10 +25,11 @@ module Gitlab
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
@confidence = confidence
@identifiers = identifiers
@links = links
@location = location
@metadata_version = metadata_version
@name = name
......@@ -46,6 +48,7 @@ module Gitlab
compare_key
confidence
identifiers
links
location
metadata_version
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
class GroupSearchResults < Gitlab::Elastic::SearchResults
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
@default_project_filter = default_project_filter
@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
# rubocop:enable Metrics/ParameterLists
end
end
end
......@@ -8,11 +8,11 @@ module Gitlab
class ProjectSearchResults < Gitlab::Elastic::SearchResults
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
@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
private
......
......@@ -7,17 +7,18 @@ module Gitlab
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
# It allows us to search only for projects user has access to
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
@query = query
@limit_project_ids = limit_project_ids
@public_and_internal_projects = public_and_internal_projects
@order_by = order_by
@sort = sort
@filters = filters
end
......@@ -202,6 +203,7 @@ module Gitlab
current_user: current_user,
project_ids: limit_project_ids,
public_and_internal_projects: public_and_internal_projects,
order_by: order_by,
sort: sort
}
end
......@@ -214,7 +216,7 @@ module Gitlab
def issues
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)
end
......@@ -235,7 +237,7 @@ module Gitlab
def merge_requests
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)
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 @@
require 'spec_helper'
RSpec.describe 'Groups > Audit Events', :js do
include Spec::Support::Helpers::Features::MembersHelpers
let(:user) { create(:user) }
let(:alex) { create(:user, name: 'Alex') }
let(:group) { create(:group) }
before do
stub_feature_flags(vue_group_members_list: false)
group.add_owner(user)
group.add_developer(alex)
sign_in(user)
......@@ -47,11 +47,9 @@ RSpec.describe 'Groups > Audit Events', :js do
wait_for_requests
group_member = group.members.find_by(user_id: alex)
page.within "#group_member_#{group_member.id}" do
page.within first_row do
click_button 'Developer'
click_link 'Maintainer'
click_button 'Maintainer'
end
find(:link, text: 'Settings').click
......
......@@ -2,14 +2,12 @@
require 'spec_helper'
RSpec.describe 'Groups > Members > List members' do
include Spec::Support::Helpers::Features::MembersHelpers
let(:user1) { create(:user, name: 'John Doe') }
let(:user2) { create(:user, name: 'Mary Jane') }
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
let(:saml_provider) { create(:saml_provider) }
let(:group) { saml_provider.group }
......@@ -23,12 +21,10 @@ RSpec.describe 'Groups > Members > List members' do
extern_uid: 'user2@example.com')
end
it 'shows user with SSO status badge' do
it 'shows user with SSO status badge', :js do
visit group_group_members_path(group)
member = GroupMember.find_by(user: user2, group: group)
expect(find("#group_member_#{member.id}").find('.badge-info')).to have_content('SAML')
expect(second_row).to have_content('SAML')
end
end
......@@ -40,12 +36,10 @@ RSpec.describe 'Groups > Members > List members' do
managed_group.add_guest(managed_user)
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)
member = GroupMember.find_by(user: managed_user, group: managed_group)
expect(page).to have_selector("#group_member_#{member.id} .badge-info", text: 'Managed Account')
expect(first_row).to have_content('Managed Account')
end
end
......
......@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access levels' do
include WaitForRequests
include Spec::Support::Helpers::Features::MembersHelpers
let(:johndoe) { create(:user, name: 'John Doe') }
let(:maryjane) { create(:user, name: 'Mary Jane') }
......@@ -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) }
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!
allow(Gitlab.config.ldap).to receive_messages(enabled: true)
......@@ -35,7 +34,7 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev
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_button 'Guest'
expect(page).not_to have_button 'Edit permissions'
......@@ -47,7 +46,7 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev
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_button 'Guest', disabled: true
expect(page).to have_button 'Edit permissions'
......@@ -55,29 +54,26 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev
click_button 'Edit permissions'
end
expect(page).to have_content ldap_override_message
click_button 'Change permissions'
page.within('[role="dialog"]') do
expect(page).to have_content ldap_override_message
click_button 'Edit permissions'
end
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).to have_button 'Guest', disabled: false
end
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).to have_button 'Guest', disabled: false
click_button 'Guest'
within '.dropdown-menu' do
click_link 'Revert to LDAP group sync settings'
end
click_button 'Revert to LDAP group sync settings'
wait_for_requests
......@@ -85,16 +81,14 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev
expect(page).to have_button 'Edit permissions'
end
within "#group_member_#{regular_member.id}" do
within third_row do
expect(page).not_to have_content 'LDAP'
expect(page).not_to have_button 'Edit permissions'
expect(page).to have_button 'Guest', disabled: false
click_button 'Guest'
within '.dropdown-menu' do
expect(page).not_to have_content 'Revert to LDAP group sync settings'
end
expect(page).not_to have_content 'Revert to LDAP group sync settings'
end
end
end
......@@ -16,7 +16,7 @@
"identifiers": [],
"links": [
{
"url": ""
"url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020"
}
]
},
......@@ -37,7 +37,8 @@
"identifiers": [],
"links": [
{
"url": ""
"name": "CVE-1030",
"url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030"
}
]
},
......@@ -56,12 +57,6 @@
"location": {},
"identifiers": [],
"links": [
{
"url": ""
},
{
"url": ""
}
]
}
],
......@@ -122,4 +117,4 @@
"end_time": "placeholder-value",
"status": "success"
}
}
\ No newline at end of file
}
......@@ -81,7 +81,7 @@ describe('LdapOverrideConfirmationModal', () => {
it('displays modal body', () => {
expect(
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(),
).toBe(true);
});
......
......@@ -16,6 +16,8 @@ RSpec.describe Mutations::DastOnDemandScans::Create do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -33,52 +35,6 @@ RSpec.describe Mutations::DastOnDemandScans::Create do
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
before do
project.add_developer(user)
......@@ -152,14 +108,6 @@ RSpec.describe Mutations::DastOnDemandScans::Create do
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
......
......@@ -16,6 +16,8 @@ RSpec.describe Mutations::DastScannerProfiles::Create do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -35,12 +37,6 @@ RSpec.describe Mutations::DastScannerProfiles::Create do
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
before do
group.add_owner(user)
......@@ -83,14 +79,6 @@ RSpec.describe Mutations::DastScannerProfiles::Create do
expect(response[:errors]).to include('Name has already been taken')
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
......@@ -15,6 +15,8 @@ RSpec.describe Mutations::DastScannerProfiles::Delete do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -54,14 +56,6 @@ RSpec.describe Mutations::DastScannerProfiles::Delete do
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
it 'returns an error' do
allow_next_instance_of(::DastScannerProfiles::DestroyService) do |service|
......
......@@ -22,6 +22,8 @@ RSpec.describe Mutations::DastScannerProfiles::Update do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -47,20 +49,6 @@ RSpec.describe Mutations::DastScannerProfiles::Update do
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
before do
project.add_developer(user)
......@@ -108,14 +96,6 @@ RSpec.describe Mutations::DastScannerProfiles::Update do
expect(subject[:errors]).to include('Scanner profile not found for given parameters')
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
......
......@@ -17,6 +17,8 @@ RSpec.describe Mutations::DastSiteProfiles::Create do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -35,28 +37,6 @@ RSpec.describe Mutations::DastSiteProfiles::Create do
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
before do
project.add_developer(user)
......@@ -89,14 +69,6 @@ RSpec.describe Mutations::DastSiteProfiles::Create do
expect(response[:errors]).to include('Name has already been taken')
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
......
......@@ -15,6 +15,8 @@ RSpec.describe Mutations::DastSiteProfiles::Delete do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -32,52 +34,6 @@ RSpec.describe Mutations::DastSiteProfiles::Delete do
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
before do
project.add_developer(user)
......@@ -96,14 +52,6 @@ RSpec.describe Mutations::DastSiteProfiles::Delete do
expect(subject[:errors]).to include('Name is weird')
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
......
......@@ -18,6 +18,8 @@ RSpec.describe Mutations::DastSiteProfiles::Update do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -37,52 +39,6 @@ RSpec.describe Mutations::DastSiteProfiles::Update do
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
before do
project.add_developer(user)
......@@ -96,14 +52,6 @@ RSpec.describe Mutations::DastSiteProfiles::Update do
expect(dast_site_profile.dast_site.url).to eq(new_target_url)
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
......
......@@ -18,6 +18,8 @@ RSpec.describe Mutations::DastSiteTokens::Create do
allow(SecureRandom).to receive(:uuid).and_return(uuid)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -35,28 +37,6 @@ RSpec.describe Mutations::DastSiteTokens::Create do
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
before do
project.add_developer(user)
......@@ -94,14 +74,6 @@ RSpec.describe Mutations::DastSiteTokens::Create do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
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
......
......@@ -17,6 +17,8 @@ RSpec.describe Mutations::DastSiteValidations::Create do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -36,28 +38,6 @@ RSpec.describe Mutations::DastSiteValidations::Create do
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
before do
project.add_developer(user)
......@@ -78,14 +58,6 @@ RSpec.describe Mutations::DastSiteValidations::Create do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
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
......
......@@ -78,5 +78,16 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
expect(empty_report.scan).to be(nil)
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
......@@ -10,6 +10,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
let(:primary_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(:location) { create(:ci_reports_security_locations_sast) }
......@@ -18,6 +19,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
compare_key: 'this_is_supposed_to_be_a_unique_value',
confidence: :medium,
identifiers: [primary_identifier, other_identifier],
links: [link],
location: location,
metadata_version: 'sast:1.0',
name: 'Cipher with no integrity',
......@@ -39,6 +41,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
confidence: :medium,
project_fingerprint: '9a73f32d58d87d94e3dc61c4c1a94803f6014258',
identifiers: [primary_identifier, other_identifier],
links: [link],
location: location,
metadata_version: 'sast:1.0',
name: 'Cipher with no integrity',
......@@ -84,6 +87,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
compare_key: occurrence.compare_key,
confidence: occurrence.confidence,
identifiers: occurrence.identifiers,
links: occurrence.links,
location: occurrence.location,
metadata_version: occurrence.metadata_version,
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
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(: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
describe 'validations' do
......@@ -405,6 +406,33 @@ RSpec.describe Vulnerabilities::Finding do
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
let_it_be(:project) { create(:project) }
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 @@
require 'spec_helper'
RSpec.describe DastSiteProfilePolicy do
describe 'create_on_demand_dast_scan' do
let(:dast_site_profile) { create(:dast_site_profile) }
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
it_behaves_like 'a dast on-demand scan policy' do
let_it_be(:record) { create(:dast_site_profile, project: project) }
end
end
......@@ -3,43 +3,7 @@
require 'spec_helper'
RSpec.describe DastSiteValidationPolicy do
describe 'create_on_demand_dast_scan' do
let_it_be(:dast_site_validation, reload: true) { create(:dast_site_validation) }
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
it_behaves_like 'a dast on-demand scan policy' do
let_it_be(:record) { create(:dast_site_validation, dast_site_token: create(:dast_site_token, project: project)) }
end
end
......@@ -22,6 +22,7 @@ RSpec.describe API::Search, factory_default: :keep do
it 'returns a different result for each page' do
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)
first = json_response.first
......@@ -37,6 +38,30 @@ RSpec.describe API::Search, factory_default: :keep do
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
it 'returns 400 error for wiki_blobs, blobs and commits scope' do
get api(endpoint, user), params: { scope: 'wiki_blobs', search: 'awesome' }
......@@ -61,6 +86,7 @@ RSpec.describe API::Search, factory_default: :keep do
end
it_behaves_like 'pagination', scope: 'merge_requests'
it_behaves_like 'orderable by created_at', scope: 'merge_requests'
it 'avoids N+1 queries' do
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
end
it_behaves_like 'pagination', scope: 'issues'
it_behaves_like 'orderable by created_at', scope: 'issues'
end
unless level == :project
......
......@@ -24,11 +24,11 @@ RSpec.describe Security::StoreReportService, '#execute' do
using RSpec::Parameterized::TableSyntax
where(:case_name, :trait, :scanners, :identifiers, :findings, :finding_identifiers, :finding_pipelines) do
'with SAST report' | :sast | 3 | 17 | 33 | 39 | 33
'with exceeding identifiers' | :with_exceeding_identifiers | 1 | 20 | 1 | 20 | 1
'with Dependency Scanning report' | :dependency_scanning | 2 | 7 | 4 | 7 | 4
'with Container Scanning report' | :container_scanning | 1 | 8 | 8 | 8 | 8
where(:case_name, :trait, :scanners, :identifiers, :findings, :finding_identifiers, :finding_pipelines, :finding_links) do
'with SAST report' | :sast | 3 | 17 | 33 | 39 | 33 | 0
'with exceeding identifiers' | :with_exceeding_identifiers | 1 | 20 | 1 | 20 | 1 | 0
'with Dependency Scanning report' | :dependency_scanning | 2 | 7 | 4 | 7 | 4 | 6
'with Container Scanning report' | :container_scanning | 1 | 8 | 8 | 8 | 8 | 8
end
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
mount ::API::ImportBitbucketServer
mount ::API::ImportGithub
mount ::API::IssueLinks
mount ::API::Invitations
mount ::API::Issues
mount ::API::JobArtifacts
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
snippets: snippets?,
basic_search: params[:basic_search],
page: params[:page],
per_page: params[:per_page]
per_page: params[:per_page],
order_by: params[:order_by],
sort: params[:sort]
}.merge(additional_params)
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
# We are ignoring connections and built in types for now,
# they should be added when queries are generated.
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]["Edge"] &&
!object_type[:name]["__"]
end
object_types.each do |type|
type[:fields] += type[:connections]
end
end
# We ignore the built-in enum types.
......
......@@ -4,10 +4,10 @@ module Gitlab
class GroupSearchResults < SearchResults
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
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
# rubocop:disable CodeReuse/ActiveRecord
......
......@@ -4,11 +4,11 @@ module Gitlab
class ProjectSearchResults < SearchResults
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
@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
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
DEFAULT_PAGE = 1
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
# It allows us to search only for projects user has access to
......@@ -19,11 +19,12 @@ module Gitlab
# query
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
@query = query
@limit_projects = limit_projects || Project.all
@default_project_filter = default_project_filter
@order_by = order_by
@sort = sort
@filters = filters
end
......@@ -128,10 +129,12 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def apply_sort(scope)
case sort
when 'created_asc'
# Due to different uses of sort param we prefer order_by when
# present
case ::Gitlab::Search::SortOptions.sort_and_direction(order_by, sort)
when :created_at_asc
scope.reorder('created_at ASC')
when 'created_desc'
when :created_at_desc
scope.reorder('created_at DESC')
else
scope.reorder('created_at DESC')
......
......@@ -9866,6 +9866,9 @@ msgstr ""
msgid "Email Notification"
msgstr ""
msgid "Email cannot be blank"
msgstr ""
msgid "Email could not be sent"
msgstr ""
......@@ -16545,7 +16548,7 @@ msgstr ""
msgid "Members|%{time} by %{user}"
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 ""
msgid "Members|An error occurred while trying to enable LDAP override, please try again."
......@@ -28202,6 +28205,9 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
msgid "Too many users specified (limit is %{user_limit})"
msgstr ""
msgid "Too much data"
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 @@
require 'spec_helper'
RSpec.describe FromUnion do
[true, false].each do |sql_set_operator|
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
it_behaves_like 'from set operator', Gitlab::SQL::Union
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
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: ''|
it 'returns a different result for each page' do
get api(endpoint, user), params: { scope: scope, search: search, page: 1, per_page: 1 }
......@@ -121,6 +163,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :issues
it_behaves_like 'issues orderable by created_at'
describe 'pagination' do
before do
create(:issue, project: project, title: 'another issue')
......@@ -181,6 +225,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :merge_requests
it_behaves_like 'merge_requests orderable by created_at'
describe 'pagination' do
before do
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
......@@ -354,6 +400,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :issues
it_behaves_like 'issues orderable by created_at'
describe 'pagination' do
before do
create(:issue, project: project, title: 'another issue')
......@@ -374,6 +422,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :merge_requests
it_behaves_like 'merge_requests orderable by created_at'
describe 'pagination' do
before do
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
......@@ -506,6 +556,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :issues
it_behaves_like 'issues orderable by created_at'
describe 'pagination' do
before do
create(:issue, project: project, title: 'another issue')
......@@ -536,6 +588,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :merge_requests
it_behaves_like 'merge_requests orderable by created_at'
describe 'pagination' do
before do
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