Commit e5c1f23e authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch 'ps-fix-fake-date' into 'master'

Fix fake date when called as function

See merge request gitlab-org/gitlab!46921
parents df7b6de8 89ae6833
......@@ -81,7 +81,7 @@ export default {
})
.then(({ data }) => {
if (data.updateIssue.errors.length) {
createFlash(data.updateIssue.errors.join('. '));
createFlash({ message: data.updateIssue.errors.join('. ') });
return;
}
......@@ -95,7 +95,7 @@ export default {
// Dispatch event which updates open/close state, shared among the issue show page
document.dispatchEvent(new CustomEvent('issuable_vue_app:change', payload));
})
.catch(() => createFlash(__('Update failed. Please try again.')))
.catch(() => createFlash({ message: __('Update failed. Please try again.') }))
.finally(() => {
this.isUpdatingState = false;
});
......
......@@ -7,11 +7,11 @@ document.addEventListener('DOMContentLoaded', () => {
if (gon?.features?.ciLintVue) {
import(/* webpackChunkName: 'ciLintIndex' */ '~/ci_lint/index')
.then(module => module.default())
.catch(() => createFlash(ERROR));
.catch(() => createFlash({ message: ERROR }));
} else {
import(/* webpackChunkName: 'ciLintEditor' */ '../ci_lint_editor')
// eslint-disable-next-line new-cap
.then(module => new module.default())
.catch(() => createFlash(ERROR));
.catch(() => createFlash({ message: ERROR }));
}
});
......@@ -7,11 +7,11 @@ document.addEventListener('DOMContentLoaded', () => {
if (gon?.features?.ciLintVue) {
import(/* webpackChunkName: 'ciLintIndex' */ '~/ci_lint/index')
.then(module => module.default())
.catch(() => createFlash(ERROR));
.catch(() => createFlash({ message: ERROR }));
} else {
import(/* webpackChunkName: 'ciLintEditor' */ '../ci_lint_editor')
// eslint-disable-next-line new-cap
.then(module => new module.default())
.catch(() => createFlash(ERROR));
.catch(() => createFlash({ message: ERROR }));
}
});
......@@ -40,7 +40,7 @@ document.addEventListener('DOMContentLoaded', () => {
new Diff();
})
.catch(() => {
flash(__('An error occurred while retrieving diff files'));
flash({ message: __('An error occurred while retrieving diff files') });
});
} else {
new Diff();
......
......@@ -65,7 +65,7 @@ export default {
.then(({ data }) => {
this.milestones = data;
})
.catch(() => createFlash(__('There was a problem fetching milestones.')))
.catch(() => createFlash({ message: __('There was a problem fetching milestones.') }))
.finally(() => {
this.loading = false;
});
......
......@@ -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
......@@ -160,7 +160,7 @@ module IssuesHelper
can_report_spam: issue.submittable_as_spam_by?(current_user).to_s,
can_update_issue: can?(current_user, :update_issue, issue).to_s,
iid: issue.iid,
is_issue_author: issue.author == current_user,
is_issue_author: (issue.author == current_user).to_s,
new_issue_path: new_project_issue_path(project),
project_path: project.full_path,
report_abuse_path: new_abuse_report_path(user_id: issue.author.id, ref_url: issue_url(issue)),
......
......@@ -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
......@@ -79,8 +79,6 @@ class Deployment < ApplicationRecord
after_transition any => :running do |deployment|
deployment.run_after_commit do
next unless Feature.enabled?(:ci_send_deployment_hook_when_start, deployment.project)
Deployments::ExecuteHooksWorker.perform_async(id)
end
end
......
......@@ -16,6 +16,7 @@ class Discussion
:commit_id,
:confidential?,
:for_commit?,
:for_design?,
:for_merge_request?,
:noteable_ability_name,
:to_ability_name,
......
......@@ -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'
......
......@@ -7,13 +7,15 @@ class NotePolicy < BasePolicy
delegate { @subject.noteable if DeclarativePolicy.has_policy?(@subject.noteable) }
condition(:is_author) { @user && @subject.author == @user }
condition(:is_noteable_author) { @user && @subject.noteable.author_id == @user.id }
condition(:is_noteable_author) { @user && @subject.noteable.try(:author_id) == @user.id }
condition(:editable, scope: :subject) { @subject.editable? }
condition(:can_read_noteable) { can?(:"read_#{@subject.noteable_ability_name}") }
condition(:commit_is_deleted) { @subject.for_commit? && @subject.noteable.blank? }
condition(:for_design) { @subject.for_design? }
condition(:is_visible) { @subject.system_note_with_references_visible_for?(@user) }
condition(:confidential, scope: :subject) { @subject.confidential? }
......@@ -28,6 +30,7 @@ class NotePolicy < BasePolicy
rule { ~can_read_noteable }.policy do
prevent :admin_note
prevent :resolve_note
prevent :reposition_note
prevent :award_emoji
end
......@@ -46,6 +49,7 @@ class NotePolicy < BasePolicy
prevent :read_note
prevent :admin_note
prevent :resolve_note
prevent :reposition_note
prevent :award_emoji
end
......@@ -57,9 +61,14 @@ class NotePolicy < BasePolicy
prevent :read_note
prevent :admin_note
prevent :resolve_note
prevent :reposition_note
prevent :award_emoji
end
rule { can?(:admin_note) | (for_design & can?(:create_note)) }.policy do
enable :reposition_note
end
def parent_namespace
strong_memoize(:parent_namespace) do
next if @subject.is_a?(PersonalSnippet)
......
# 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] }
)
......
......@@ -19,7 +19,7 @@ module ApplicationWorker
def structured_payload(payload = {})
context = Labkit::Context.current.to_h.merge(
'class' => self.class,
'class' => self.class.name,
'job_status' => 'running',
'queue' => self.class.queue,
'jid' => jid
......
---
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: Add metric count for projects with alerts created
merge_request: 46636
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
---
title: Forbid top level route sitemap
merge_request: 46677
author:
type: changed
---
name: ci_send_deployment_hook_when_start
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41214
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247137
group: group::progressive delivery
type: development
default_enabled: false
---
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
# frozen_string_literal: true
class RenameSitemapNamespace < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
include Gitlab::Database::RenameReservedPathsMigration::V1
DOWNTIME = false
disable_ddl_transaction!
# We're taking over the /sitemap namespace
# since it's necessary for the default behavior of Sitemaps
def up
disable_statement_timeout do
rename_root_paths(['sitemap'])
end
end
def down
disable_statement_timeout do
revert_renames
end
end
end
50e4e42c804d3abdcfe9ab2bbb890262d4b2ddd93bff1b2af1da1e55a0300cf5
\ No newline at end of file
a861c91ebc7f7892020ba10a151df761b38bf69d5e02bcdf72a965eb266e6aff
\ 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;
......
......@@ -6,11 +6,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# API Docs
Automate GitLab via a simple and powerful API.
Automate GitLab by using a simple and powerful API.
The main GitLab API is a [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) API. Therefore, documentation in this section assumes knowledge of REST concepts.
The main GitLab API is a [REST](http://spec.openapis.org/oas/v3.0.3)
API. Because of this, the documentation in this section assumes that you're
familiar with REST concepts.
There is also a partial [OpenAPI definition](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/api/openapi/openapi.yaml), which allows you to test the API directly from the GitLab user interface. Contributions are welcome.
There's also a partial [OpenAPI definition](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/api/openapi/openapi.yaml),
which allows you to test the API directly from the GitLab user interface.
Contributions are welcome.
## Available API resources
......@@ -19,21 +23,22 @@ For a list of the available resources and their endpoints, see
## SCIM **(SILVER ONLY)**
[GitLab.com Silver and above](https://about.gitlab.com/pricing/) provides an [SCIM API](scim.md) that implements [the RFC7644 protocol](https://tools.ietf.org/html/rfc7644) and provides
the `/Users` endpoint. The base URL is: `/api/scim/v2/groups/:group_path/Users/`.
[GitLab.com Silver and higher](https://about.gitlab.com/pricing/) provides an
[SCIM API](scim.md) that both implements [the RFC7644 protocol](https://tools.ietf.org/html/rfc7644)
and provides the `/Users` endpoint. The base URL is: `/api/scim/v2/groups/:group_path/Users/`.
## Road to GraphQL
[GraphQL](graphql/index.md) is available in GitLab, which will
allow deprecation of controller-specific endpoints.
[GraphQL](graphql/index.md) is available in GitLab, which allows for the
deprecation of controller-specific endpoints.
GraphQL has a number of benefits:
GraphQL has several benefits, including:
1. We avoid having to maintain two different APIs.
1. Callers of the API can request only what they need.
1. It is versioned by default.
- We avoid having to maintain two different APIs.
- Callers of the API can request only what they need.
- It's versioned by default.
It will co-exist with the current v4 REST API. If we have a v5 API, this should
GraphQL co-exists with the current v4 REST API. If we have a v5 API, this should
be a compatibility layer on top of GraphQL.
Although there were some patenting and licensing concerns with GraphQL, these
......@@ -43,31 +48,31 @@ specification.
## Compatibility guidelines
The HTTP API is versioned using a single number, the current one being 4. This
number symbolizes the same as the major version number as described by
[SemVer](https://semver.org/). This mean that backward incompatible changes
will require this version number to change. However, the minor version is
not explicit. This allows for a stable API endpoint, but also means new
features can be added to the API in the same version number.
The HTTP API is versioned using a single number, (currently _4_). This number
symbolizes the major version number, as described by [SemVer](https://semver.org/).
Because of this, backwards-incompatible changes require this version number to
change. However, the minor version isn't explicit, allowing for a stable API
endpoint. This also means that new features can be added to the API in the same
version number.
New features and bug fixes are released in tandem with a new GitLab, and apart
from incidental patch and security releases, are released on the 22nd of each
month. Backward incompatible changes (e.g. endpoints removal, parameters
removal etc.), as well as removal of entire API versions are done in tandem
with a major point release of GitLab itself. All deprecations and changes
between two versions should be listed in the documentation. For the changes
between v3 and v4; please read the [v3 to v4 documentation](v3_to_v4.md)
month. Backward incompatible changes (for example, endpoints removal and
parameters removal), and removal of entire API versions are done in tandem with
a major point release of GitLab itself. All deprecations and changes between two
versions should be listed in the documentation. For the changes between v3 and
v4, see the [v3 to v4 documentation](v3_to_v4.md).
### Current status
Currently only API version v4 is available. Version v3 was removed in
Only API version v4 is available. Version v3 was removed in
[GitLab 11.0](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/36819).
## Basic usage
API requests should be prefixed with `api` and the API version. The API version
is defined in [`lib/api.rb`](https://gitlab.com/gitlab-org/gitlab/tree/master/lib/api/api.rb). For example, the root of the v4 API
is at `/api/v4`.
API requests should be prefixed with both `api` and the API version. The API
version is defined in [`lib/api.rb`](https://gitlab.com/gitlab-org/gitlab/tree/master/lib/api/api.rb).
For example, the root of the v4 API is at `/api/v4`.
Example of a valid API request using cURL:
......@@ -80,30 +85,31 @@ end of an API URL.
## Authentication
Most API requests require authentication, or will only return public data when
authentication is not provided. For
those cases where it is not required, this will be mentioned in the documentation
for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md#get-single-project).
Most API requests require authentication, or will return public data only when
authentication isn't provided. For cases where it isn't required, this will be
mentioned in the documentation for each individual endpoint (for example, the
[`/projects/:id` endpoint](projects.md#get-single-project)).
There are several ways to authenticate with the GitLab API:
There are several methods you can use to authenticate with the GitLab API:
1. [OAuth2 tokens](#oauth2-tokens)
1. [Personal access tokens](../user/profile/personal_access_tokens.md)
1. [Project access tokens](../user/project/settings/project_access_tokens.md)
- [OAuth2 tokens](#oauth2-tokens)
- [Personal access tokens](../user/profile/personal_access_tokens.md)
- [Project access tokens](../user/project/settings/project_access_tokens.md)
- [Session cookie](#session-cookie)
- [GitLab CI/CD job token](#gitlab-ci-job-token) **(Specific endpoints only)**
NOTE: **Note:**
Project access tokens are supported for self-managed instances on Core and above. They are also supported on GitLab.com Bronze and above.
Project access tokens are supported for self-managed instances on Core and
higher. They're also supported on GitLab.com Bronze and higher.
1. [Session cookie](#session-cookie)
1. [GitLab CI/CD job token](#gitlab-ci-job-token) **(Specific endpoints only)**
For administrators who want to authenticate with the API as a specific user, or who want
to build applications or scripts that do so, the following options are available:
For admins who want to authenticate with the API as a specific user, or who want to build applications or scripts that do so, two options are available:
- [Impersonation tokens](#impersonation-tokens)
- [Sudo](#sudo)
1. [Impersonation tokens](#impersonation-tokens)
1. [Sudo](#sudo)
If authentication information is invalid or omitted, an error message will be
returned with status code `401`:
If authentication information is invalid or omitted, GitLab will return an error
message with a status code of `401`:
```json
{
......@@ -113,8 +119,8 @@ returned with status code `401`:
### OAuth2 tokens
You can use an [OAuth2 token](oauth2.md) to authenticate with the API by passing it in either the
`access_token` parameter or the `Authorization` header.
You can use an [OAuth2 token](oauth2.md) to authenticate with the API by passing
it in either the `access_token` parameter or the `Authorization` header.
Example of using the OAuth2 token in a parameter:
......@@ -132,22 +138,22 @@ Read more about [GitLab as an OAuth2 provider](oauth2.md).
### Personal/project access tokens
Access tokens can be used to authenticate with the API by passing it in either the `private_token` parameter
or the `PRIVATE-TOKEN` header.
You can use access tokens to authenticate with the API by passing it in either
the `private_token` parameter or the `PRIVATE-TOKEN` header.
Example of using the personal/project access token in a parameter:
Example of using the personal or project access token in a parameter:
```shell
curl "https://gitlab.example.com/api/v4/projects?private_token=<your_access_token>"
```
Example of using the personal/project access token in a header:
Example of using the personal or project access token in a header:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects"
```
You can also use personal/project access tokens with OAuth-compliant headers:
You can also use personal or project access tokens with OAuth-compliant headers:
```shell
curl --header "Authorization: Bearer <your_access_token>" "https://gitlab.example.com/api/v4/projects"
......@@ -156,12 +162,12 @@ curl --header "Authorization: Bearer <your_access_token>" "https://gitlab.exampl
### Session cookie
When signing in to the main GitLab application, a `_gitlab_session` cookie is
set. The API will use this cookie for authentication if it is present, but using
the API to generate a new session cookie is currently not supported.
set. The API uses this cookie for authentication if it's present. Using the
API to generate a new session cookie isn't supported.
The primary user of this authentication method is the web frontend of GitLab itself,
which can use the API as the authenticated user to get a list of their projects,
for example, without needing to explicitly pass an access token.
The primary user of this authentication method is the web frontend of GitLab
itself, which can, for example, use the API as the authenticated user to get a
list of their projects without needing to explicitly pass an access token.
### GitLab CI job token
......@@ -171,7 +177,9 @@ to authenticate with the API:
- Packages:
- [Composer Repository](../user/packages/composer_repository/index.md)
- [Conan Repository](../user/packages/conan_repository/index.md)
- [Container Registry](../user/packages/container_registry/index.md) (`$CI_REGISTRY_PASSWORD` is actually `$CI_JOB_TOKEN`, but this may change in the future)
- [Container Registry](../user/packages/container_registry/index.md)
(`$CI_REGISTRY_PASSWORD` is actually `$CI_JOB_TOKEN`, but this may change in
the future)
- [Go Proxy](../user/packages/go_proxy/index.md)
- [Maven Repository](../user/packages/maven_repository/index.md#authenticate-with-a-ci-job-token-in-maven)
- [NPM Repository](../user/packages/npm_registry/index.md#authenticate-with-a-ci-job-token)
......@@ -179,7 +187,7 @@ to authenticate with the API:
- [PyPI Repository](../user/packages/pypi_repository/index.md#using-gitlab-ci-with-pypi-packages)
- [Generic packages](../user/packages/generic_packages/index.md#publish-a-generic-package-by-using-cicd)
- [Get job artifacts](job_artifacts.md#get-job-artifacts)
- [Pipeline triggers](pipeline_triggers.md) (via `token=` parameter)
- [Pipeline triggers](pipeline_triggers.md) (using the `token=` parameter)
- [Release creation](releases/index.md#create-a-release)
- [Terraform plan](../user/infrastructure/index.md)
......@@ -187,21 +195,22 @@ The token is valid as long as the job is running.
### Impersonation tokens
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/9099) in GitLab 9.0. Needs admin permissions.
Impersonation tokens are a type of [personal access token](../user/profile/personal_access_tokens.md)
that can only be created by an admin for a specific user. They are a great fit
if you want to build applications or scripts that authenticate with the API as a specific user.
if you want to build applications or scripts that authenticate with the API as a
specific user.
They are an alternative to directly using the user's password or one of their
personal access tokens, and to using the [Sudo](#sudo) feature, since the user's (or admin's, in the case of Sudo)
password/token may not be known or may change over time.
They're an alternative to directly using the user's password or one of their
personal access tokens, and to using the [Sudo](#sudo) feature, as the user's
(or admin's, in the case of Sudo) password or token may not be known, or may
change over time.
For more information, refer to the
[users API](users.md#create-an-impersonation-token) docs.
For more information, see the [users API](users.md#create-an-impersonation-token)
documentation.
Impersonation tokens are used exactly like regular personal access tokens, and can be passed in either the
`private_token` parameter or the `PRIVATE-TOKEN` header.
Impersonation tokens are used exactly like regular personal access tokens, and
can be passed in either the `private_token` parameter or the `PRIVATE-TOKEN`
header.
#### Disable impersonation
......@@ -220,7 +229,8 @@ By default, impersonation is enabled. To disable impersonation:
1. Save the file and [reconfigure](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure)
GitLab for the changes to take effect.
To re-enable impersonation, remove this configuration and reconfigure GitLab.
To re-enable impersonation, remove this configuration, and then reconfigure
GitLab.
**For installations from source**
......@@ -234,26 +244,22 @@ To re-enable impersonation, remove this configuration and reconfigure GitLab.
1. Save the file and [restart](../administration/restart_gitlab.md#installations-from-source)
GitLab for the changes to take effect.
To re-enable impersonation, remove this configuration and restart GitLab.
To re-enable impersonation, remove this configuration, and then restart GitLab.
### Sudo
NOTE: **Note:**
Only available to [administrators](../user/permissions.md).
All API requests support performing an API call as if you were another user,
provided you are authenticated as an administrator with an OAuth or Personal Access Token that has the `sudo` scope.
The API requests are executed with the permissions of the impersonated user.
provided you're authenticated as an administrator with an OAuth or personal
access token that has the `sudo` scope. The API requests are executed with the
permissions of the impersonated user.
You need to pass the `sudo` parameter either via query string or a header with an ID/username of
the user you want to perform the operation as. If passed as a header, the
header name must be `Sudo`.
As an [administrator](../user/permissions.md), pass the `sudo` parameter either
by using query string or a header with an ID or username (case insensitive) of
the user you want to perform the operation as. If passed as a header, the header
name must be `Sudo`.
NOTE: **Note:**
Usernames are case insensitive.
If a non administrative access token is provided, an error message will
be returned with status code `403`:
If a non administrative access token is provided, GitLab returns an error
message with a status code of `403`:
```json
{
......@@ -262,7 +268,7 @@ be returned with status code `403`:
```
If an access token without the `sudo` scope is provided, an error message will
be returned with status code `403`:
be returned with a status code of `403`:
```json
{
......@@ -273,7 +279,7 @@ be returned with status code `403`:
```
If the sudo user ID or username cannot be found, an error message will be
returned with status code `404`:
returned with a status code of `404`:
```json
{
......@@ -311,27 +317,27 @@ insight into what went wrong.
The following table gives an overview of how the API functions generally behave.
| Request type | Description |
| ------------ | ----------- |
| `GET` | Access one or more resources and return the result as JSON. |
| `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. |
| Request type | Description |
|---------------|-------------|
| `GET` | Access one or more resources and return the result as JSON. |
| `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. |
| `GET` / `PUT` | Return `200 OK` if the resource is accessed or modified successfully. The (modified) result is returned as JSON. |
| `DELETE` | Returns `204 No Content` if the resource was deleted successfully. |
| `DELETE` | Returns `204 No Content` if the resource was deleted successfully. |
The following table shows the possible return codes for API requests.
| Return values | Description |
| ------------------------ | ----------- |
|--------------------------|-------------|
| `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. |
| `204 No Content` | The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. |
| `201 Created` | The `POST` request was successful and the resource is returned as JSON. |
| `304 Not Modified` | Indicates that the resource has not been modified since the last request. |
| `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. |
| `401 Unauthorized` | The user is not authenticated, a valid [user token](#authentication) is necessary. |
| `403 Forbidden` | The request is not allowed, e.g., the user is not allowed to delete a project. |
| `404 Not Found` | A resource could not be accessed, e.g., an ID for a resource could not be found. |
| `403 Forbidden` | The request is not allowed. For example, the user is not allowed to delete a project. |
| `404 Not Found` | A resource could not be accessed. For example, an ID for a resource could not be found. |
| `405 Method Not Allowed` | The request is not supported. |
| `409 Conflict` | A conflicting resource already exists, e.g., creating a project with a name that already exists. |
| `409 Conflict` | A conflicting resource already exists. For example, creating a project with a name that already exists. |
| `412` | Indicates the request was denied. May happen if the `If-Unmodified-Since` header is provided when trying to delete a resource, which was modified in between. |
| `422 Unprocessable` | The entity could not be processed. |
| `429 Too Many Requests` | The user exceeded the [application rate limits](../administration/instance_limits.md#rate-limits). |
......@@ -339,26 +345,26 @@ The following table shows the possible return codes for API requests.
## Pagination
We support two kinds of pagination methods:
GitLab supports the following pagination methods:
- Offset-based pagination. This is the default method and available on all endpoints.
- Keyset-based pagination. Added to selected endpoints but being
[progressively rolled out](https://gitlab.com/groups/gitlab-org/-/epics/2039).
For large collections, we recommend keyset pagination (when available) over offset
pagination for performance reasons.
For large collections, we recommend keyset pagination (when available) instead
of offset pagination for performance reasons.
### Offset-based pagination
Sometimes the returned result will span across many pages. When listing
resources you can pass the following parameters:
Sometimes, the returned result spans many pages. When listing resources, you can
pass the following parameters:
| Parameter | Description |
| --------- | ----------- |
| `page` | Page number (default: `1`) |
| `per_page`| Number of items to list per page (default: `20`, max: `100`) |
|-----------|-------------|
| `page` | Page number (default: `1`). |
| `per_page`| Number of items to list per page (default: `20`, max: `100`). |
In the example below, we list 50 [namespaces](namespaces.md) per page.
In the following example, we list 50 [namespaces](namespaces.md) per page:
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/namespaces?per_page=50"
......@@ -367,15 +373,14 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab
#### Pagination `Link` header
[`Link` headers](https://www.w3.org/wiki/LinkHeader) are returned with each
response. They have `rel` set to `prev`/`next`/`first`/`last` and contain the
relevant URL. Be sure to use these links instead of generating your own URLs.
response. They have `rel` set to `prev`, `next`, `first`, or `last` and contain
the relevant URL. Be sure to use these links instead of generating your own URLs.
NOTE: **Note:**
For GitLab.com users, [some pagination headers may not be returned](../user/gitlab_com/index.md#pagination-response-headers).
In the cURL example below, we limit the output to 3 items per page (`per_page=3`)
and we request the second page (`page=2`) of [comments](notes.md) of the issue
with ID `8` which belongs to the project with ID `9`:
In the following cURL example, we limit the output to three items per page
(`per_page=3`) and we request the second page (`page=2`) of [comments](notes.md)
of the issue with ID `8` which belongs to the project with ID `9`:
```shell
curl --head --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/9/issues/8/notes?per_page=3&page=2"
......@@ -406,31 +411,32 @@ x-total-pages: 3
GitLab also returns the following additional pagination headers:
| Header | Description |
| --------------- | --------------------------------------------- |
| `x-total` | The total number of items |
| `x-total-pages` | The total number of pages |
| `x-per-page` | The number of items per page |
| `x-page` | The index of the current page (starting at 1) |
| `x-next-page` | The index of the next page |
| `X-prev-page` | The index of the previous page |
| Header | Description |
|-----------------|-------------|
| `x-next-page` | The index of the next page. |
| `x-page` | The index of the current page (starting at 1). |
| `x-per-page` | The number of items per page. |
| `X-prev-page` | The index of the previous page. |
| `x-total` | The total number of items. |
| `x-total-pages` | The total number of pages. |
NOTE: **Note:**
For GitLab.com users, [some pagination headers may not be returned](../user/gitlab_com/index.md#pagination-response-headers).
### Keyset-based pagination
Keyset-pagination allows for more efficient retrieval of pages and - in contrast to offset-based pagination - runtime
is independent of the size of the collection.
Keyset-pagination allows for more efficient retrieval of pages and - in contrast
to offset-based pagination - runtime is independent of the size of the
collection.
This method is controlled by the following parameters:
| Parameter | Description |
| ------------ | -------------------------------------- |
| `pagination` | `keyset` (to enable keyset pagination) |
| `per_page` | Number of items to list per page (default: `20`, max: `100`) |
| Parameter | Description |
|--------------| ------------|
| `pagination` | `keyset` (to enable keyset pagination). |
| `per_page` | Number of items to list per page (default: `20`, max: `100`). |
In the example below, we list 50 [projects](projects.md) per page, ordered by `id` ascending.
In the following example, we list 50 [projects](projects.md) per page, ordered
by `id` ascending.
```shell
curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects?pagination=keyset&per_page=50&order_by=id&sort=asc"
......@@ -448,27 +454,34 @@ Status: 200 OK
```
CAUTION: **Deprecation:**
The `Links` header will be removed in GitLab 14.0 to be aligned with the [W3C `Link` specification](https://www.w3.org/wiki/LinkHeader).
The `Link` header was [added in GitLab 13.1](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33714)
The `Links` header will be removed in GitLab 14.0 to be aligned with the
[W3C `Link` specification](https://www.w3.org/wiki/LinkHeader). The `Link`
header was [added in GitLab 13.1](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33714)
and should be used instead.
The link to the next page contains an additional filter `id_after=42` which excludes records we have retrieved already.
Note the type of filter depends on the `order_by` option used and we may have more than one additional filter.
The link to the next page contains an additional filter `id_after=42` that
excludes already-retrieved records. Note the type of filter depends on the
`order_by` option used, and we may have more than one additional filter.
When the end of the collection has been reached and there are no additional records to retrieve, the `Link` header is absent and the resulting array is empty.
When the end of the collection has been reached and there are no additional
records to retrieve, the `Link` header is absent and the resulting array is
empty.
We recommend using only the given link to retrieve the next page instead of building your own URL. Apart from the headers shown,
we don't expose additional pagination headers.
We recommend using only the given link to retrieve the next page instead of
building your own URL. Apart from the headers shown, we don't expose additional
pagination headers.
Keyset-based pagination is only supported for selected resources and ordering options:
Keyset-based pagination is supported only for selected resources and ordering
options:
| Resource | Order |
| ------------------------- | -------------------------- |
| [Projects](projects.md) | `order_by=id` only |
| Resource | Order |
|-------------------------|-------|
| [Projects](projects.md) | `order_by=id` only. |
## Path parameters
If an endpoint has path parameters, the documentation shows them with a preceding colon.
If an endpoint has path parameters, the documentation displays them with a
preceding colon.
For example:
......@@ -476,7 +489,9 @@ For example:
DELETE /projects/:id/share/:group_id
```
The `:id` path parameter needs to be replaced with the project ID, and the `:group_id` needs to be replaced with the ID of the group. The colons `:` should not be included.
The `:id` path parameter needs to be replaced with the project ID, and the
`:group_id` needs to be replaced with the ID of the group. The colons `:`
shouldn't be included.
The resulting cURL call for a project with ID `5` and a group ID of `17` is then:
......@@ -484,11 +499,10 @@ The resulting cURL call for a project with ID `5` and a group ID of `17` is then
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/share/17"
```
NOTE: **Note:**
Path parameters that are required to be URL-encoded must be followed. If not,
it will not match an API endpoint and respond with a 404. If there's something
in front of the API (for example, Apache), ensure that it won't decode the URL-encoded
path parameters.
it won't match an API endpoint, and will respond with a 404. If there's
something in front of the API (for example, Apache), ensure that it won't decode
the URL-encoded path parameters.
## Namespaced path encoding
......@@ -501,10 +515,9 @@ For example, `/` is represented by `%2F`:
GET /api/v4/projects/diaspora%2Fdiaspora
```
NOTE: **Note:**
A project's **path** is not necessarily the same as its **name**. A
project's path can be found in the project's URL or in the project's settings
under **General > Advanced > Change path**.
A project's _path_ isn't necessarily the same as its _name_. A project's path is
found in the project's URL or in the project's settings, under
**General > Advanced > Change path**.
## File path, branches, and tags name encoding
......@@ -522,7 +535,8 @@ GET /api/v4/projects/1/repository/tags/my%2Ftag
API Requests can use parameters sent as [query strings](https://en.wikipedia.org/wiki/Query_string)
or as a [payload body](https://tools.ietf.org/html/draft-ietf-httpbis-p3-payload-14#section-3.2).
GET requests usually send a query string, while PUT/POST requests usually send the payload body:
GET requests usually send a query string, while PUT or POST requests usually
send the payload body:
- Query string:
......@@ -536,13 +550,13 @@ GET requests usually send a query string, while PUT/POST requests usually send t
curl --request POST --header "Content-Type: application/json" --data '{"name":"<example-name>", "description":"<example-description"}' "https://gitlab/api/v4/projects"
```
URL encoded query strings have a length limitation. Requests that are too large will
result in a `414 Request-URI Too Large` error message. This can be resolved by using
a payload body instead.
URL encoded query strings have a length limitation. Requests that are too large
result in a `414 Request-URI Too Large` error message. This can be resolved by
using a payload body instead.
## Encoding API parameters of `array` and `hash` types
We can call the API with `array` and `hash` types parameters as shown below:
We can call the API with `array` and `hash` types parameters as follows:
### `array`
......@@ -571,7 +585,8 @@ https://gitlab.example.com/api/v4/projects/import
### Array of hashes
`variables` is a parameter of type `array` containing hash key/value pairs `[{ 'key': 'UPLOAD_TO_S3', 'value': 'true' }]`:
`variables` is a parameter of type `array` containing hash key/value pairs
`[{ 'key': 'UPLOAD_TO_S3', 'value': 'true' }]`:
```shell
curl --globoff --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
......@@ -585,34 +600,37 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
## `id` vs `iid`
Some resources have two similarly-named fields. For example, [issues](issues.md), [merge requests](merge_requests.md), and [project milestones](merge_requests.md). The fields are:
Some resources have two similarly-named fields. For example, [issues](issues.md),
[merge requests](merge_requests.md), and [project milestones](merge_requests.md).
The fields are:
- `id`: ID that is unique across all projects.
- `iid`: additional, internal ID that is unique in the scope of a single project.
- `iid`: Additional, internal ID (displayed in the web UI) that's unique in the
scope of a single project.
NOTE: **Note:**
The `iid` is displayed in the web UI.
If a resource has the `iid` field and the `id` field, the `iid` field is usually used instead of `id` to fetch the resource.
If a resource has both the `iid` field and the `id` field, the `iid` field is
usually used instead of `id` to fetch the resource.
For example, suppose a project with `id: 42` has an issue with `id: 46` and `iid: 5`. In this case:
For example, suppose a project with `id: 42` has an issue with `id: 46` and
`iid: 5`. In this case:
- A valid API call to retrieve the issue is `GET /projects/42/issues/5`
- A valid API call to retrieve the issue is `GET /projects/42/issues/5`.
- An invalid API call to retrieve the issue is `GET /projects/42/issues/46`.
NOTE: **Note:**
Not all resources with the `iid` field are fetched by `iid`. For guidance on which field to use, see the documentation for the specific resource.
Not all resources with the `iid` field are fetched by `iid`. For guidance
regarding which field to use, see the documentation for the specific resource.
## Data validation and error reporting
When working with the API you may encounter validation errors, in which case
the API will answer with an HTTP `400` status.
the API returns an HTTP `400` error.
Such errors appear in two cases:
Such errors appear in the following cases:
- A required attribute of the API request is missing, e.g., the title of an
issue is not given
- An attribute did not pass the validation, e.g., the user bio is too long
- A required attribute of the API request is missing (for example, the title of
an issue isn't given).
- An attribute did not pass the validation (for example, the user bio is too
long).
When an attribute is missing, you will get something like:
......@@ -624,8 +642,8 @@ Content-Type: application/json
}
```
When a validation error occurs, error messages will be different. They will
hold all details of validation errors:
When a validation error occurs, error messages will be different. They will hold
all details of validation errors:
```http
HTTP/1.1 400 Bad Request
......@@ -663,7 +681,8 @@ follows:
## Unknown route
When you try to access an API URL that does not exist, you will receive 404 Not Found.
When you attempt to access an API URL that doesn't exist, you will receive
404 Not Found message.
```http
HTTP/1.1 404 Not Found
......@@ -675,10 +694,10 @@ Content-Type: application/json
## Encoding `+` in ISO 8601 dates
If you need to include a `+` in a query parameter, you may need to use `%2B` instead due
to a [W3 recommendation](http://www.w3.org/Addressing/URL/4_URI_Recommentations.html) that
causes a `+` to be interpreted as a space. For example, in an ISO 8601 date, you may want to pass
a time in Mountain Standard Time, such as:
If you need to include a `+` in a query parameter, you may need to use `%2B`
instead, due to a [W3 recommendation](http://www.w3.org/Addressing/URL/4_URI_Recommentations.html)
that causes a `+` to be interpreted as a space. For example, in an ISO 8601 date,
you may want to include a specific time in ISO 8601 format, such as:
```plaintext
2017-10-17T23:11:13.000+05:30
......@@ -692,8 +711,8 @@ The correct encoding for the query parameter would be:
## Clients
There are many unofficial GitLab API Clients for most of the popular
programming languages. Visit the [GitLab website](https://about.gitlab.com/partners/#api-clients) for a complete list.
There are many unofficial GitLab API Clients for most of the popular programming
languages. For a complete list, visit the [GitLab website](https://about.gitlab.com/partners/#api-clients).
## Rate limits
......
......@@ -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) |
......
......@@ -72,10 +72,12 @@ Describes an alert from the project's Alert Management.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `assignees` | UserConnection | Assignees of the alert |
| `createdAt` | Time | Timestamp the alert was created |
| `description` | String | Description of the alert |
| `details` | JSON | Alert details |
| `detailsUrl` | String! | The URL of the alert detail page |
| `discussions` | DiscussionConnection! | All discussions on this noteable |
| `endedAt` | Time | Timestamp the alert ended |
| `environment` | Environment | Environment for the alert |
| `eventCount` | Int | Number of events of this alert |
......@@ -84,6 +86,7 @@ Describes an alert from the project's Alert Management.
| `issueIid` | ID | Internal ID of the GitLab issue attached to the alert |
| `metricsDashboardUrl` | String | URL for metrics embed for the alert |
| `monitoringTool` | String | Monitoring tool the alert came from |
| `notes` | NoteConnection! | All notes on this noteable |
| `prometheusAlert` | PrometheusAlert | The alert condition for Prometheus |
| `runbook` | String | Runbook for the alert as defined in alert details |
| `service` | String | Service the alert came from |
......@@ -91,6 +94,7 @@ Describes an alert from the project's Alert Management.
| `startedAt` | Time | Timestamp the alert was raised |
| `status` | AlertManagementStatus | Status of the alert |
| `title` | String | Title of the alert |
| `todos` | TodoConnection | Todos of the current user for the alert |
| `updatedAt` | Time | Timestamp the alert was last updated |
### AlertManagementAlertStatusCountsType
......@@ -231,9 +235,12 @@ Represents a project or group board.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `assignee` | User | The board assignee. |
| `epics` | BoardEpicConnection | Epics associated with board issues. |
| `hideBacklogList` | Boolean | Whether or not backlog list is hidden. |
| `hideClosedList` | Boolean | Whether or not closed list is hidden. |
| `id` | ID! | ID (global ID) of the board |
| `labels` | LabelConnection | Labels of the board |
| `lists` | BoardListConnection | Lists of the board |
| `milestone` | Milestone | The board milestone. |
| `name` | String | Name of the board |
| `weight` | Int | Weight of the board. |
......@@ -245,12 +252,15 @@ Represents an epic on an issue board.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `author` | User! | Author of the epic |
| `children` | EpicConnection | Children (sub-epics) of the epic |
| `closedAt` | Time | Timestamp of when the epic was closed |
| `confidential` | Boolean | Indicates if the epic is confidential |
| `createdAt` | Time | Timestamp of when the epic was created |
| `currentUserTodos` | TodoConnection! | Todos for the current user |
| `descendantCounts` | EpicDescendantCount | Number of open and closed descendant epics and issues |
| `descendantWeightSum` | EpicDescendantWeights | Total weight of open and closed issues in the epic and its descendants |
| `description` | String | Description of the epic |
| `discussions` | DiscussionConnection! | All discussions on this noteable |
| `downvotes` | Int! | Number of downvotes the epic has received |
| `dueDate` | Time | Due date of the epic |
| `dueDateFixed` | Time | Fixed due date of the epic |
......@@ -263,7 +273,11 @@ Represents an epic on an issue board.
| `healthStatus` | EpicHealthStatus | Current health status of the epic |
| `id` | ID! | ID of the epic |
| `iid` | ID! | Internal ID of the epic |
| `issues` | EpicIssueConnection | A list of issues associated with the epic |
| `labels` | LabelConnection | Labels assigned to the epic |
| `notes` | NoteConnection! | All notes on this noteable |
| `parent` | Epic | Parent epic of the epic |
| `participants` | UserConnection | List of participants for the epic |
| `reference` | String! | Internal reference of the epic. Returned in shortened format by default |
| `relationPath` | String | URI path of the epic-issue relationship |
| `relativePosition` | Int | The relative position of the epic in the epic tree |
......@@ -298,6 +312,7 @@ Represents a list for an issue board.
| `assignee` | User | Assignee in the list |
| `collapsed` | Boolean | Indicates if list is collapsed for this user |
| `id` | ID! | ID (global ID) of the list |
| `issues` | IssueConnection | Board issues |
| `issuesCount` | Int | Count of issues in the list |
| `label` | Label | Label of the list |
| `limitMetric` | ListLimitMetric | The current limit metric for the list |
......@@ -353,6 +368,7 @@ Represents the total number of issues and their weights for a particular day.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `detailedStatus` | DetailedStatus | Detailed status of the group |
| `jobs` | CiJobConnection | Jobs in group |
| `name` | String | Name of the job group |
| `size` | Int | Size of the group |
......@@ -362,6 +378,7 @@ Represents the total number of issues and their weights for a particular day.
| ----- | ---- | ----------- |
| `detailedStatus` | DetailedStatus | Detailed status of the job |
| `name` | String | Name of the job |
| `needs` | CiJobConnection | Builds that must complete before the jobs run |
| `scheduledAt` | Time | Schedule for the build |
### CiStage
......@@ -369,6 +386,7 @@ Represents the total number of issues and their weights for a particular day.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `detailedStatus` | DetailedStatus | Detailed status of the stage |
| `groups` | CiGroupConnection | Group of jobs for the stage |
| `name` | String | Name of the stage |
### ClusterAgent
......@@ -379,6 +397,7 @@ Represents the total number of issues and their weights for a particular day.
| `id` | ID! | ID of the cluster agent |
| `name` | String | Name of the cluster agent |
| `project` | Project | The project this cluster agent is associated with |
| `tokens` | ClusterAgentTokenConnection | Tokens associated with the cluster agent |
| `updatedAt` | Time | Timestamp the cluster agent was updated |
### ClusterAgentDeletePayload
......@@ -452,6 +471,7 @@ Represents the code coverage summary for a project.
| `id` | ID! | ID (global ID) of the commit |
| `latestPipeline` **{warning-solid}** | Pipeline | **Deprecated:** Use `pipelines`. Deprecated in 12.5 |
| `message` | String | Raw commit message |
| `pipelines` | PipelineConnection | Pipelines of the commit ordered latest first |
| `sha` | String! | SHA1 ID of the commit |
| `signatureHtml` | String | Rendered HTML of the commit signature |
| `title` | String | Title of the commit message |
......@@ -827,7 +847,9 @@ A single design.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `currentUserTodos` | TodoConnection! | Todos for the current user |
| `diffRefs` | DiffRefs! | The diff refs for this design |
| `discussions` | DiscussionConnection! | All discussions on this noteable |
| `event` | DesignVersionEvent! | How this design was changed in the current version |
| `filename` | String! | The filename of the design |
| `fullPath` | String! | The full path to the design file |
......@@ -835,8 +857,10 @@ A single design.
| `image` | String! | The URL of the full-sized image |
| `imageV432x230` | String | The URL of the design resized to fit within the bounds of 432x230. This will be `null` if the image has not been generated |
| `issue` | Issue! | The issue the design belongs to |
| `notes` | NoteConnection! | All notes on this noteable |
| `notesCount` | Int! | The total count of user-created notes for this design |
| `project` | Project! | The project the design belongs to |
| `versions` | DesignVersionConnection! | All versions related to this design ordered newest first |
### DesignAtVersion
......@@ -866,9 +890,11 @@ A collection of designs.
| `copyState` | DesignCollectionCopyState | Copy state of the design collection |
| `design` | Design | Find a specific design |
| `designAtVersion` | DesignAtVersion | Find a design as of a version |
| `designs` | DesignConnection! | All designs for the design collection |
| `issue` | Issue! | Issue associated with the design collection |
| `project` | Project! | Project associated with the design collection |
| `version` | DesignVersion | A specific version |
| `versions` | DesignVersionConnection! | All versions related to all designs, ordered newest first |
### DesignManagement
......@@ -915,6 +941,8 @@ A specific version in which designs were added, modified or deleted.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `designAtVersion` | DesignAtVersion! | A particular design as of this version, provided it is visible at this version |
| `designs` | DesignConnection! | All designs that were changed in the version |
| `designsAtVersion` | DesignAtVersionConnection! | All designs that are visible at this version, as of this version |
| `id` | ID! | ID of the design version |
| `sha` | ID! | SHA of the design version |
......@@ -1023,6 +1051,7 @@ Aggregated summary of changes.
| ----- | ---- | ----------- |
| `createdAt` | Time! | Timestamp of the discussion's creation |
| `id` | ID! | ID of this discussion |
| `notes` | NoteConnection! | All notes in the discussion |
| `replyId` | ID! | ID used to reply to this discussion |
| `resolvable` | Boolean! | Indicates if the object can be resolved |
| `resolved` | Boolean! | Indicates if the object is resolved |
......@@ -1069,12 +1098,15 @@ Represents an epic.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `author` | User! | Author of the epic |
| `children` | EpicConnection | Children (sub-epics) of the epic |
| `closedAt` | Time | Timestamp of when the epic was closed |
| `confidential` | Boolean | Indicates if the epic is confidential |
| `createdAt` | Time | Timestamp of when the epic was created |
| `currentUserTodos` | TodoConnection! | Todos for the current user |
| `descendantCounts` | EpicDescendantCount | Number of open and closed descendant epics and issues |
| `descendantWeightSum` | EpicDescendantWeights | Total weight of open and closed issues in the epic and its descendants |
| `description` | String | Description of the epic |
| `discussions` | DiscussionConnection! | All discussions on this noteable |
| `downvotes` | Int! | Number of downvotes the epic has received |
| `dueDate` | Time | Due date of the epic |
| `dueDateFixed` | Time | Fixed due date of the epic |
......@@ -1087,7 +1119,11 @@ Represents an epic.
| `healthStatus` | EpicHealthStatus | Current health status of the epic |
| `id` | ID! | ID of the epic |
| `iid` | ID! | Internal ID of the epic |
| `issues` | EpicIssueConnection | A list of issues associated with the epic |
| `labels` | LabelConnection | Labels assigned to the epic |
| `notes` | NoteConnection! | All notes on this noteable |
| `parent` | Epic | Parent epic of the epic |
| `participants` | UserConnection | List of participants for the epic |
| `reference` | String! | Internal reference of the epic. Returned in shortened format by default |
| `relationPath` | String | URI path of the epic-issue relationship |
| `relativePosition` | Int | The relative position of the epic in the epic tree |
......@@ -1152,17 +1188,20 @@ Relationship between an epic and an issue.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `alertManagementAlert` | AlertManagementAlert | Alert associated to this issue |
| `assignees` | UserConnection | Assignees of the issue |
| `author` | User! | User that created the issue |
| `blocked` | Boolean! | Indicates the issue is blocked |
| `blockedByCount` | Int | Count of issues blocking this issue |
| `closedAt` | Time | Timestamp of when the issue was closed |
| `confidential` | Boolean! | Indicates the issue is confidential |
| `createdAt` | Time! | Timestamp of when the issue was created |
| `currentUserTodos` | TodoConnection! | Todos for the current user |
| `description` | String | Description of the issue |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `designCollection` | DesignCollection | Collection of design images associated with this issue |
| `designs` **{warning-solid}** | DesignCollection | **Deprecated:** Use `designCollection`. Deprecated in 12.2 |
| `discussionLocked` | Boolean! | Indicates discussion is locked on the issue |
| `discussions` | DiscussionConnection! | All discussions on this noteable |
| `downvotes` | Int! | Number of downvotes the issue has received |
| `dueDate` | Time | Due date of the issue |
| `epic` | Epic | Epic to which this issue belongs |
......@@ -1173,7 +1212,10 @@ Relationship between an epic and an issue.
| `id` | ID | Global ID of the epic-issue relation |
| `iid` | ID! | Internal ID of the issue |
| `iteration` | Iteration | Iteration of the issue |
| `labels` | LabelConnection | Labels of the issue |
| `milestone` | Milestone | Milestone of the issue |
| `notes` | NoteConnection! | All notes on this noteable |
| `participants` | UserConnection | List of participants in the issue |
| `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
| `relationPath` | String | URI path of the epic-issue relation |
| `relativePosition` | Int | Relative position of the issue (used for positioning in epic tree and issue boards) |
......@@ -1241,13 +1283,17 @@ Autogenerated return type of EpicTreeReorder.
| `filesMaxCapacity` | Int | The maximum concurrency of LFS/attachment backfill for this secondary node |
| `id` | ID! | ID of this GeoNode |
| `internalUrl` | String | The URL defined on the primary node that secondary nodes should use to contact it |
| `mergeRequestDiffRegistries` | MergeRequestDiffRegistryConnection | Find merge request diff registries on this Geo node |
| `minimumReverificationInterval` | Int | The interval (in days) in which the repository verification is valid. Once expired, it will be reverified |
| `name` | String | The unique identifier for this Geo node |
| `packageFileRegistries` | PackageFileRegistryConnection | Package file registries of the GeoNode |
| `primary` | Boolean | Indicates whether this Geo node is the primary |
| `reposMaxCapacity` | Int | The maximum concurrency of repository backfill for this secondary node |
| `selectiveSyncNamespaces` | NamespaceConnection | The namespaces that should be synced, if `selective_sync_type` == `namespaces` |
| `selectiveSyncShards` | String! => Array | The repository storages whose projects should be synced, if `selective_sync_type` == `shards` |
| `selectiveSyncType` | String | Indicates if syncing is limited to only specific groups, or shards |
| `syncObjectStorage` | Boolean | Indicates if this secondary node will replicate blobs in Object Storage |
| `terraformStateVersionRegistries` | TerraformStateVersionRegistryConnection | Find terraform state version registries on this Geo node |
| `url` | String | The user-facing URL for this Geo node |
| `verificationMaxCapacity` | Int | The maximum concurrency of repository verification for this secondary node |
......@@ -1271,24 +1317,35 @@ Autogenerated return type of EpicTreeReorder.
| `autoDevopsEnabled` | Boolean | Indicates whether Auto DevOps is enabled for all projects within this group |
| `avatarUrl` | String | Avatar URL of the group |
| `board` | Board | A single board of the group |
| `boards` | BoardConnection | Boards of the group |
| `codeCoverageActivities` | CodeCoverageActivityConnection | Represents the code coverage activity for this group. Available only when feature flag `group_coverage_data_report_graph` is enabled |
| `containerRepositories` | ContainerRepositoryConnection | Container repositories of the project |
| `containsLockedProjects` | Boolean! | Includes at least one project where the repository size exceeds the limit |
| `description` | String | Description of the namespace |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `emailsDisabled` | Boolean | Indicates if a group has email notifications disabled |
| `epic` | Epic | Find a single epic |
| `epics` | EpicConnection | Find epics |
| `epicsEnabled` | Boolean | Indicates if Epics are enabled for namespace |
| `fullName` | String! | Full name of the namespace |
| `fullPath` | ID! | Full path of the namespace |
| `groupMembers` | GroupMemberConnection | A membership of a user within this group |
| `groupTimelogsEnabled` | Boolean | Indicates if Group timelogs are enabled for namespace |
| `id` | ID! | ID of the namespace |
| `isTemporaryStorageIncreaseEnabled` | Boolean! | Status of the temporary storage increase |
| `issues` | IssueConnection | Issues for projects in this group |
| `iterations` | IterationConnection | Find iterations |
| `label` | Label | A label available on this group |
| `labels` | LabelConnection | Labels available on this group |
| `lfsEnabled` | Boolean | Indicates if Large File Storage (LFS) is enabled for namespace |
| `mentionsDisabled` | Boolean | Indicates if a group is disabled from getting mentioned |
| `mergeRequests` | MergeRequestConnection | Merge requests for projects in this group |
| `milestones` | MilestoneConnection | Milestones of the group |
| `name` | String! | Name of the namespace |
| `parent` | Group | Parent group |
| `path` | String! | Path of the namespace |
| `projectCreationLevel` | String | The permission level required to create projects in the group |
| `projects` | ProjectConnection! | Projects within this namespace |
| `repositorySizeExcessProjectCount` | Int! | Number of projects in the root namespace where the repository size exceeds the limit |
| `requestAccessEnabled` | Boolean | Indicates if users can request access to namespace |
| `requireTwoFactorAuthentication` | Boolean | Indicates if all users in this group are required to set up two-factor authentication |
......@@ -1297,12 +1354,17 @@ Autogenerated return type of EpicTreeReorder.
| `storageSizeLimit` | Float | Total storage limit of the root namespace in bytes |
| `subgroupCreationLevel` | String | The permission level required to create subgroups within the group |
| `temporaryStorageIncreaseEndsOn` | Time | Date until the temporary storage increase is active |
| `timelogs` | TimelogConnection! | Time logged in issues by group members |
| `totalRepositorySize` | Float | Total repository size of all projects in the root namespace in bytes |
| `totalRepositorySizeExcess` | Float | Total excess repository size of all projects in the root namespace in bytes |
| `twoFactorGracePeriod` | Int | Time before two-factor authentication is enforced |
| `userPermissions` | GroupPermissions! | Permissions for the current user on the resource |
| `visibility` | String | Visibility of the namespace |
| `vulnerabilities` | VulnerabilityConnection | Vulnerabilities reported on the projects in the group and its subgroups |
| `vulnerabilitiesCountByDay` | VulnerabilitiesCountByDayConnection | Number of vulnerabilities per day for the projects in the group and its subgroups |
| `vulnerabilitiesCountByDayAndSeverity` **{warning-solid}** | VulnerabilitiesCountByDayAndSeverityConnection | **Deprecated:** Use `vulnerabilitiesCountByDay`. Deprecated in 13.3 |
| `vulnerabilityGrades` | VulnerableProjectsByGrade! => Array | Represents vulnerable project counts for each grade |
| `vulnerabilityScanners` | VulnerabilityScannerConnection | Vulnerability scanners reported on the project vulnerabilties of the group and its subgroups |
| `vulnerabilitySeveritiesCount` | VulnerabilitySeveritiesCount | Counts for each vulnerability severity in the group and its subgroups |
| `webUrl` | String! | Web URL of the group |
......@@ -1372,7 +1434,9 @@ Autogenerated return type of HttpIntegrationUpdate.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `projects` | ProjectConnection! | Projects selected in Instance Security Dashboard |
| `vulnerabilityGrades` | VulnerableProjectsByGrade! => Array | Represents vulnerable project counts for each grade |
| `vulnerabilityScanners` | VulnerabilityScannerConnection | Vulnerability scanners reported on the vulnerabilties from projects selected in Instance Security Dashboard |
| `vulnerabilitySeveritiesCount` | VulnerabilitySeveritiesCount | Counts for each vulnerability severity from projects selected in Instance Security Dashboard |
### InstanceStatisticsMeasurement
......@@ -1390,17 +1454,20 @@ Represents a recorded measurement (object count) for the Admins.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `alertManagementAlert` | AlertManagementAlert | Alert associated to this issue |
| `assignees` | UserConnection | Assignees of the issue |
| `author` | User! | User that created the issue |
| `blocked` | Boolean! | Indicates the issue is blocked |
| `blockedByCount` | Int | Count of issues blocking this issue |
| `closedAt` | Time | Timestamp of when the issue was closed |
| `confidential` | Boolean! | Indicates the issue is confidential |
| `createdAt` | Time! | Timestamp of when the issue was created |
| `currentUserTodos` | TodoConnection! | Todos for the current user |
| `description` | String | Description of the issue |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `designCollection` | DesignCollection | Collection of design images associated with this issue |
| `designs` **{warning-solid}** | DesignCollection | **Deprecated:** Use `designCollection`. Deprecated in 12.2 |
| `discussionLocked` | Boolean! | Indicates discussion is locked on the issue |
| `discussions` | DiscussionConnection! | All discussions on this noteable |
| `downvotes` | Int! | Number of downvotes the issue has received |
| `dueDate` | Time | Due date of the issue |
| `epic` | Epic | Epic to which this issue belongs |
......@@ -1410,7 +1477,10 @@ Represents a recorded measurement (object count) for the Admins.
| `id` | ID! | ID of the issue |
| `iid` | ID! | Internal ID of the issue |
| `iteration` | Iteration | Iteration of the issue |
| `labels` | LabelConnection | Labels of the issue |
| `milestone` | Milestone | Milestone of the issue |
| `notes` | NoteConnection! | All notes on this noteable |
| `participants` | UserConnection | List of participants in the issue |
| `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
| `relativePosition` | Int | Relative position of the issue (used for positioning in epic tree and issue boards) |
| `severity` | IssuableSeverity | Severity level of the incident |
......@@ -1636,6 +1706,7 @@ Autogenerated return type of JiraImportUsers.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `active` | Boolean | Indicates if the service is active |
| `projects` | JiraProjectConnection | List of all Jira projects fetched through Jira REST API |
| `type` | String | Class name of the service |
### JiraUser
......@@ -1678,11 +1749,14 @@ Autogenerated return type of MarkAsSpamSnippet.
| `approvalsLeft` | Int | Number of approvals left |
| `approvalsRequired` | Int | Number of approvals required |
| `approved` | Boolean! | Indicates if the merge request has all the required approvals. Returns true if no required approvals are configured. |
| `approvedBy` | UserConnection | Users who approved the merge request |
| `assignees` | UserConnection | Assignees of the merge request |
| `author` | User | User who created this merge request |
| `autoMergeEnabled` | Boolean! | Indicates if auto merge is enabled for the merge request |
| `commitCount` | Int | Number of commits in the merge request |
| `conflicts` | Boolean! | Indicates if the merge request has conflicts |
| `createdAt` | Time! | Timestamp of when the merge request was created |
| `currentUserTodos` | TodoConnection! | Todos for the current user |
| `defaultMergeCommitMessage` | String | Default merge commit message of the merge request |
| `description` | String | Description of the merge request (Markdown rendered as HTML for caching) |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
......@@ -1691,12 +1765,14 @@ Autogenerated return type of MarkAsSpamSnippet.
| `diffStats` | DiffStats! => Array | Details about which files were changed in this merge request |
| `diffStatsSummary` | DiffStatsSummary | Summary of which files were changed in this merge request |
| `discussionLocked` | Boolean! | Indicates if comments on the merge request are locked to members only |
| `discussions` | DiscussionConnection! | All discussions on this noteable |
| `downvotes` | Int! | Number of downvotes for the merge request |
| `forceRemoveSourceBranch` | Boolean | Indicates if the project settings will lead to source branch deletion after merge |
| `headPipeline` | Pipeline | The pipeline running on the branch HEAD of the merge request |
| `id` | ID! | ID of the merge request |
| `iid` | String! | Internal ID of the merge request |
| `inProgressMergeCommitSha` | String | Commit SHA of the merge request if merge is in progress |
| `labels` | LabelConnection | Labels of the merge request |
| `mergeCommitMessage` **{warning-solid}** | String | **Deprecated:** Use `defaultMergeCommitMessage`. Deprecated in 11.8 |
| `mergeCommitSha` | String | SHA of the merge request commit (set once merged) |
| `mergeError` | String | Error message due to a merge error |
......@@ -1706,6 +1782,9 @@ Autogenerated return type of MarkAsSpamSnippet.
| `mergeableDiscussionsState` | Boolean | Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged |
| `mergedAt` | Time | Timestamp of when the merge request was merged, null if not merged |
| `milestone` | Milestone | The milestone of the merge request |
| `notes` | NoteConnection! | All notes on this noteable |
| `participants` | UserConnection | Participants in the merge request |
| `pipelines` | PipelineConnection | Pipelines for the merge request |
| `project` | Project! | Alias for target_project |
| `projectId` | Int! | ID of the merge request project |
| `rebaseCommitSha` | String | Rebase commit SHA of the merge request |
......@@ -1858,6 +1937,7 @@ Autogenerated return type of MergeRequestUpdate.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `annotations` | MetricsDashboardAnnotationConnection | Annotations added to the dashboard |
| `path` | String | Path to a file with the dashboard definition |
| `schemaValidationWarnings` | String! => Array | Dashboard schema validation warnings |
......@@ -1917,6 +1997,7 @@ Contains statistics about a milestone.
| `lfsEnabled` | Boolean | Indicates if Large File Storage (LFS) is enabled for namespace |
| `name` | String! | Name of the namespace |
| `path` | String! | Path of the namespace |
| `projects` | ProjectConnection! | Projects within this namespace |
| `repositorySizeExcessProjectCount` | Int! | Number of projects in the root namespace where the repository size exceeds the limit |
| `requestAccessEnabled` | Boolean | Indicates if users can request access to namespace |
| `rootStorageStatistics` | RootStorageStatistics | Aggregated storage statistics of the namespace. Only available for root namespaces |
......@@ -2018,16 +2099,19 @@ Information about pagination in a connection..
| `coverage` | Float | Coverage percentage |
| `createdAt` | Time! | Timestamp of the pipeline's creation |
| `detailedStatus` | DetailedStatus! | Detailed status of the pipeline |
| `downstream` | PipelineConnection | Pipelines this pipeline will trigger |
| `duration` | Int | Duration of the pipeline in seconds |
| `finishedAt` | Time | Timestamp of the pipeline's completion |
| `id` | ID! | ID of the pipeline |
| `iid` | String! | Internal ID of the pipeline |
| `jobs` | CiJobConnection | Jobs belonging to the pipeline |
| `path` | String | Relative path to the pipeline's page |
| `project` | Project | Project the pipeline belongs to |
| `retryable` | Boolean! | Specifies if a pipeline can be retried |
| `securityReportSummary` | SecurityReportSummary | Vulnerability and scanned resource counts for each security scanner of the pipeline |
| `sha` | String! | SHA of the pipeline's commit |
| `sourceJob` | CiJob | Job where pipeline was triggered from |
| `stages` | CiStageConnection | Stages of the pipeline |
| `startedAt` | Time | Timestamp when the pipeline was started |
| `status` | PipelineStatusEnum! | Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED) |
| `updatedAt` | Time! | Timestamp of the pipeline's last activity |
......@@ -2078,21 +2162,30 @@ Autogenerated return type of PipelineRetry.
| `actualRepositorySizeLimit` | Float | Size limit for the repository in bytes |
| `alertManagementAlert` | AlertManagementAlert | A single Alert Management alert of the project |
| `alertManagementAlertStatusCounts` | AlertManagementAlertStatusCountsType | Counts of alerts by status for the project |
| `alertManagementAlerts` | AlertManagementAlertConnection | Alert Management alerts of the project |
| `alertManagementIntegrations` | AlertManagementIntegrationConnection | Integrations which can receive alerts for the project |
| `allowMergeOnSkippedPipeline` | Boolean | If `only_allow_merge_if_pipeline_succeeds` is true, indicates if merge requests of the project can also be merged with skipped jobs |
| `archived` | Boolean | Indicates the archived status of the project |
| `autocloseReferencedIssues` | Boolean | Indicates if issues referenced by merge requests and commits within the default branch are closed automatically |
| `avatarUrl` | String | URL to avatar image file of the project |
| `board` | Board | A single board of the project |
| `boards` | BoardConnection | Boards of the project |
| `clusterAgent` | ClusterAgent | Find a single cluster agent by name |
| `clusterAgents` | ClusterAgentConnection | Cluster agents associated with the project |
| `codeCoverageSummary` | CodeCoverageSummary | Code coverages summary associated with the project. Available only when feature flag `group_coverage_data_report` is enabled |
| `complianceFrameworks` | ComplianceFrameworkConnection | Compliance frameworks associated with the project |
| `containerExpirationPolicy` | ContainerExpirationPolicy | The container expiration policy of the project |
| `containerRegistryEnabled` | Boolean | Indicates if the project stores Docker container images in a container registry |
| `containerRepositories` | ContainerRepositoryConnection | Container repositories of the project |
| `createdAt` | Time | Timestamp of the project creation |
| `dastScannerProfiles` | DastScannerProfileConnection | The DAST scanner profiles associated with the project |
| `dastSiteProfile` | DastSiteProfile | DAST Site Profile associated with the project |
| `dastSiteProfiles` | DastSiteProfileConnection | DAST Site Profiles associated with the project |
| `dastSiteValidation` | DastSiteValidation | DAST Site Validation associated with the project |
| `description` | String | Short description of the project |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `environment` | Environment | A single environment of the project |
| `environments` | EnvironmentConnection | Environments of the project |
| `forksCount` | Int! | Number of times the project has been forked |
| `fullPath` | ID! | Full path of the project |
| `grafanaIntegration` | GrafanaIntegration | Grafana integration details for the project |
......@@ -2102,32 +2195,43 @@ Autogenerated return type of PipelineRetry.
| `importStatus` | String | Status of import background job of the project |
| `issue` | Issue | A single issue of the project |
| `issueStatusCounts` | IssueStatusCountsType | Counts of issues by status for the project |
| `issues` | IssueConnection | Issues of the project |
| `issuesEnabled` | Boolean | Indicates if Issues are enabled for the current user |
| `iterations` | IterationConnection | Find iterations |
| `jiraImportStatus` | String | Status of Jira import background job of the project |
| `jiraImports` | JiraImportConnection | Jira imports into the project |
| `jobsEnabled` | Boolean | Indicates if CI/CD pipeline jobs are enabled for the current user |
| `label` | Label | A label available on this project |
| `labels` | LabelConnection | Labels available on this project |
| `lastActivityAt` | Time | Timestamp of the project last activity |
| `lfsEnabled` | Boolean | Indicates if the project has Large File Storage (LFS) enabled |
| `mergeRequest` | MergeRequest | A single merge request of the project |
| `mergeRequests` | MergeRequestConnection | Merge requests of the project |
| `mergeRequestsEnabled` | Boolean | Indicates if Merge Requests are enabled for the current user |
| `mergeRequestsFfOnlyEnabled` | Boolean | Indicates if no merge commits should be created and all merges should instead be fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. |
| `milestones` | MilestoneConnection | Milestones of the project |
| `name` | String! | Name of the project (without namespace) |
| `nameWithNamespace` | String! | Full name of the project with its namespace |
| `namespace` | Namespace | Namespace of the project |
| `onlyAllowMergeIfAllDiscussionsAreResolved` | Boolean | Indicates if merge requests of the project can only be merged when all the discussions are resolved |
| `onlyAllowMergeIfPipelineSucceeds` | Boolean | Indicates if merge requests of the project can only be merged with successful jobs |
| `openIssuesCount` | Int | Number of open issues for the project |
| `packages` | PackageConnection | Packages of the project |
| `path` | String! | Path of the project |
| `pipeline` | Pipeline | Build pipeline of the project |
| `pipelines` | PipelineConnection | Build pipelines of the project |
| `printingMergeRequestLinkEnabled` | Boolean | Indicates if a link to create or view a merge request should display after a push to Git repositories of the project from the command line |
| `projectMembers` | MemberInterfaceConnection | Members of the project |
| `publicJobs` | Boolean | Indicates if there is public access to pipelines and job details of the project, including output logs and artifacts |
| `release` | Release | A single release of the project |
| `releases` | ReleaseConnection | Releases of the project |
| `removeSourceBranchAfterMerge` | Boolean | Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project |
| `repository` | Repository | Git repository of the project |
| `repositorySizeExcess` | Float | Size of repository that exceeds the limit in bytes |
| `requestAccessEnabled` | Boolean | Indicates if users can request member access to the project |
| `requirement` | Requirement | Find a single requirement |
| `requirementStatesCount` | RequirementStatesCount | Number of requirements for the project by their state |
| `requirements` | RequirementConnection | Find requirements |
| `sastCiConfiguration` | SastCiConfiguration | SAST CI configuration for the project |
| `securityDashboardPath` | String | Path to project's security dashboard |
| `securityScanners` | SecurityScanners | Information about security analyzers used in the project |
......@@ -2135,15 +2239,21 @@ Autogenerated return type of PipelineRetry.
| `sentryErrors` | SentryErrorCollection | Paginated collection of Sentry errors on the project |
| `serviceDeskAddress` | String | E-mail address of the service desk. |
| `serviceDeskEnabled` | Boolean | Indicates if the project has service desk enabled. |
| `services` | ServiceConnection | Project services |
| `sharedRunnersEnabled` | Boolean | Indicates if shared runners are enabled for the project |
| `snippets` | SnippetConnection | Snippets of the project |
| `snippetsEnabled` | Boolean | Indicates if Snippets are enabled for the current user |
| `sshUrlToRepo` | String | URL to connect to the project via SSH |
| `starCount` | Int! | Number of times the project has been starred |
| `statistics` | ProjectStatistics | Statistics of the project |
| `suggestionCommitMessage` | String | The commit message used to apply merge request suggestions |
| `tagList` | String | List of project topics (not Git tags) |
| `terraformStates` | TerraformStateConnection | Terraform states associated with the project |
| `userPermissions` | ProjectPermissions! | Permissions for the current user on the resource |
| `visibility` | String | Visibility of the project |
| `vulnerabilities` | VulnerabilityConnection | Vulnerabilities reported on the project |
| `vulnerabilitiesCountByDay` | VulnerabilitiesCountByDayConnection | Number of vulnerabilities per day for the project |
| `vulnerabilityScanners` | VulnerabilityScannerConnection | Vulnerability scanners reported on the project vulnerabilties |
| `vulnerabilitySeveritiesCount` | VulnerabilitySeveritiesCount | Counts for each vulnerability severity in the project |
| `webUrl` | String | Web URL of the project |
| `wikiEnabled` | Boolean | Indicates if Wikis are enabled for the current user |
......@@ -2275,7 +2385,9 @@ Represents a release.
| `createdAt` | Time | Timestamp of when the release was created |
| `description` | String | Description (also known as "release notes") of the release |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `evidences` | ReleaseEvidenceConnection | Evidence for the release |
| `links` | ReleaseLinks | Links of the release |
| `milestones` | MilestoneConnection | Milestones associated to the release |
| `name` | String | Name of the release |
| `releasedAt` | Time | Timestamp of when the release was released |
| `tagName` | String | Name of the tag associated with the release |
......@@ -2302,6 +2414,8 @@ A container for all assets associated with a release.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `count` | Int | Number of assets of the release |
| `links` | ReleaseAssetLinkConnection | Asset links of the release |
| `sources` | ReleaseSourceConnection | Sources of the release |
### ReleaseEvidence
......@@ -2381,6 +2495,7 @@ Represents a requirement.
| `lastTestReportState` | TestReportState | Latest requirement test report state |
| `project` | Project! | Project to which the requirement belongs |
| `state` | RequirementState! | State of the requirement |
| `testReports` | TestReportConnection | Test reports of the requirement |
| `title` | String | Title of the requirement |
| `titleHtml` | String | The GitLab Flavored Markdown rendering of `title` |
| `updatedAt` | Time! | Timestamp of when the requirement was last updated |
......@@ -2451,6 +2566,7 @@ Autogenerated return type of RunDASTScan.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `architectures` | RunnerArchitectureConnection | Runner architectures supported for the platform |
| `humanReadableName` | String! | Human readable name of the runner platform |
| `name` | String! | Name slug of the runner platform |
......@@ -2461,6 +2577,16 @@ Autogenerated return type of RunDASTScan.
| `installInstructions` | String! | Instructions for installing the runner on the specified architecture |
| `registerInstructions` | String! | Instructions for registering the runner |
### SastCiConfiguration
Represents a CI configuration of SAST.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `analyzers` | SastCiConfigurationAnalyzersEntityConnection | List of analyzers entities attached to SAST configuration. |
| `global` | SastCiConfigurationEntityConnection | List of global entities related to SAST configuration. |
| `pipeline` | SastCiConfigurationEntityConnection | List of pipeline entities related to SAST configuration. |
### SastCiConfigurationAnalyzersEntity
Represents an analyzer entity in SAST CI configuration.
......@@ -2471,6 +2597,7 @@ Represents an analyzer entity in SAST CI configuration.
| `enabled` | Boolean | Indicates whether an analyzer is enabled |
| `label` | String | Analyzer label used in the config UI |
| `name` | String | Name of the analyzer |
| `variables` | SastCiConfigurationEntityConnection | List of supported variables |
### SastCiConfigurationEntity
......@@ -2482,6 +2609,7 @@ Represents an entity in SAST CI configuration.
| `description` | String | Entity description that is displayed on the form. |
| `field` | String | CI keyword of entity. |
| `label` | String | Label for entity used in the form. |
| `options` | SastCiConfigurationOptionsEntityConnection | Different possible values of the field. |
| `size` | SastUiComponentSize | Size of the UI component. |
| `type` | String | Type of the field value. |
| `value` | String | Current value of the entity. |
......@@ -2524,6 +2652,7 @@ Represents a section of a summary of a security report.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `scannedResources` | ScannedResourceConnection | A list of the first 20 scanned resources |
| `scannedResourcesCount` | Int | Total number of scanned resources |
| `scannedResourcesCsvPath` | String | Path to download all the scanned resources in CSV format |
| `vulnerabilitiesCount` | Int | Total number of vulnerabilities |
......@@ -2663,12 +2792,15 @@ Represents a snippet entry.
| ----- | ---- | ----------- |
| `author` | User | The owner of the snippet |
| `blob` **{warning-solid}** | SnippetBlob! | **Deprecated:** Use `blobs`. Deprecated in 13.3 |
| `blobs` | SnippetBlobConnection | Snippet blobs |
| `createdAt` | Time! | Timestamp this snippet was created |
| `description` | String | Description of the snippet |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `discussions` | DiscussionConnection! | All discussions on this noteable |
| `fileName` | String | File Name of the snippet |
| `httpUrlToRepo` | String | HTTP URL to the snippet repository |
| `id` | ID! | ID of the snippet |
| `notes` | NoteConnection! | All notes on this noteable |
| `project` | Project | The project the snippet is associated with |
| `rawUrl` | String! | Raw URL of the snippet |
| `sshUrlToRepo` | String | SSH URL to the snippet repository |
......@@ -2922,7 +3054,10 @@ Autogenerated return type of ToggleAwardEmoji.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `blobs` | BlobConnection! | Blobs of the tree |
| `lastCommit` | Commit | Last commit for the tree |
| `submodules` | SubmoduleConnection! | Sub-modules of the tree |
| `trees` | TreeEntryConnection! | Trees of the tree |
### TreeEntry
......@@ -3066,13 +3201,20 @@ Autogenerated return type of UpdateSnippet.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `assignedMergeRequests` | MergeRequestConnection | Merge Requests assigned to the user |
| `authoredMergeRequests` | MergeRequestConnection | Merge Requests authored by the user |
| `avatarUrl` | String | URL of the user's avatar |
| `email` | String | User email |
| `groupCount` | Int | Group count for the user. Available only when feature flag `user_group_counts` is enabled |
| `groupMemberships` | GroupMemberConnection | Group memberships of the user |
| `id` | ID! | ID of the user |
| `name` | String! | Human-readable name of the user |
| `projectMemberships` | ProjectMemberConnection | Project memberships of the user |
| `snippets` | SnippetConnection | Snippets authored by the user |
| `starredProjects` | ProjectConnection | Projects starred by the user |
| `state` | UserState! | State of the user |
| `status` | UserStatus | User status |
| `todos` | TodoConnection! | Todos of the user |
| `userPermissions` | UserPermissions! | Permissions for the current user on the resource |
| `username` | String! | Username of the user. Unique within this instance of GitLab |
| `webPath` | String! | Web path of the user |
......@@ -3126,9 +3268,12 @@ Represents a vulnerability.
| ----- | ---- | ----------- |
| `description` | String | Description of the vulnerability |
| `detectedAt` | Time! | Timestamp of when the vulnerability was first detected |
| `discussions` | DiscussionConnection! | All discussions on this noteable |
| `id` | ID! | GraphQL ID of the vulnerability |
| `identifiers` | VulnerabilityIdentifier! => Array | Identifiers of the vulnerability. |
| `issueLinks` | VulnerabilityIssueLinkConnection! | List of issue links related to the vulnerability |
| `location` | VulnerabilityLocation | Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability |
| `notes` | NoteConnection! | All notes on this noteable |
| `primaryIdentifier` | VulnerabilityIdentifier | Primary identifier of the vulnerability. |
| `project` | Project | The project on which the vulnerability was found |
| `reportType` | VulnerabilityReportType | Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION, COVERAGE_FUZZING, API_FUZZING) |
......@@ -3332,6 +3477,7 @@ Represents vulnerability letter grades with associated projects.
| ----- | ---- | ----------- |
| `count` | Int! | Number of projects within this grade |
| `grade` | VulnerabilityGrade! | Grade based on the highest severity vulnerability present |
| `projects` | ProjectConnection! | Projects within this grade |
## Enumeration types
......
---
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"
}
}
```
......@@ -216,7 +216,7 @@ Parameters:
Example request:
```shell
curl --request POST --header "PRIVATE_TOKEN: <your_access_token>" --header "Content-Type: application/json" \
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" \
--data '{"destination_storage_name":"storage2"}' "https://gitlab.example.com/api/v4/projects/1/repository_storage_moves"
```
......
......@@ -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)
......
......@@ -17,9 +17,8 @@ we suggest investigating to see if a plugin exists. For instance here is the
## Pre-push static analysis
We strongly recommend installing
[Lefthook](https://github.com/Arkweid/lefthook) to automatically check for
static analysis offenses before pushing your changes.
We strongly recommend installing [Lefthook](https://github.com/Arkweid/lefthook) to automatically check
for static analysis offenses before pushing your changes.
To install `lefthook`, run the following in your GitLab source directory:
......@@ -33,10 +32,9 @@ overcommit --uninstall
gem install lefthook && lefthook install -f
```
Before you push your changes, Lefthook will then automatically run Danger checks, as well
as RuboCop, ES Lint, HAML Lint, and SCSS Lint for the changed files.
This saves you time as you don't have to wait for the same errors to be detected by CI/CD.
Before you push your changes, Lefthook then automatically run Danger checks, and other checks
for changed files. This saves you time as you don't have to wait for the same errors to be detected
by CI/CD.
Lefthook relies on a pre-push hook to prevent commits that violate its ruleset.
If you wish to override this behavior, pass the environment variable `LEFTHOOK=0`.
......@@ -45,7 +43,8 @@ That is, `LEFTHOOK=0 git push`.
You can also:
- Define [local configuration](https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md#local-config).
- Skip [checks per tag on the fly](https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md#skip-some-tags-on-the-fly).
- Skip [checks per tag on the fly](https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md#skip-some-tags-on-the-fly), e.g. `LEFTHOOK_EXCLUDE=frontend git push origin`.
- Run [hooks manually](https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md#run-githook-group-directly), e.g. `lefthook run pre-push`.
## Ruby, Rails, RSpec
......
......@@ -708,15 +708,14 @@ Git [pre-commit hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks)
run tests or other processes before committing to a branch, with the ability to not commit to the branch if
failures occur with these tests.
[`overcommit`](https://github.com/sds/overcommit) is a Git hooks manager, making configuring,
[`lefthook`](https://github.com/Arkweid/lefthook) is a Git hooks manager, making configuring,
installing, and removing Git hooks easy.
Sample configuration for `overcommit` is available in the
[`.overcommit.yml.example`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.overcommit.yml.example)
Configuration for `left` is available in the [`lefthook.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lefthook.yml)
file for the [`gitlab`](https://gitlab.com/gitlab-org/gitlab) project.
To set up `overcommit` for documentation linting, see
[Pre-commit static analysis](../contributing/style_guides.md#pre-push-static-analysis).
To set up `lefthook` for documentation linting, see
[Pre-push static analysis](../contributing/style_guides.md#pre-push-static-analysis).
#### Disable Vale tests
......
......@@ -161,6 +161,12 @@ headers whose values you want masked. For details on how to mask headers, see
It's also possible to authenticate the user before performing the DAST checks.
**Important:** It is highly recommended that you configure the scanner to authenticate to the application,
or it will not be able to check most of the application for security risks, as most
of your application is likely not accessible without authentication. It is also recommended
that you periodically confirm the scanner's authentication is still working as this tends to break over
time due to authentication changes to the application.
Create masked variables to pass the credentials that DAST uses.
To create masked variables for the username and password, see [Create a custom variable in the UI](../../../ci/variables/README.md#create-a-custom-variable-in-the-ui).
Note that the key of the username variable must be `DAST_USERNAME`
......
......@@ -82,6 +82,7 @@ Currently the following names are reserved as top level groups:
- `s`
- `search`
- `sent_notifications`
- `sitemap`
- `sitemap.xml`
- `sitemap.xml.gz`
- `slash-command-logo.png`
......
......@@ -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] }
)
......
......@@ -3,7 +3,7 @@
module Issues
class CreateFromVulnerabilityDataService < ::BaseService
def execute
return error("Can't create issue") unless can?(@current_user, :create_issue, @project)
return error("User is not permitted to create issue") unless can?(@current_user, :create_issue, @project)
begin
vulnerability = Gitlab::Vulnerabilities::Parser.fabricate(@params)
......
......@@ -2,7 +2,7 @@
module Issues
class CreateFromVulnerabilityService < ::BaseContainerService
def execute
return error("Can't create issue") unless can?(@current_user, :create_issue, @container)
return error("User is not permitted to create issue") unless can?(@current_user, :create_issue, @container)
vulnerability = params[:vulnerability].present
link_type = params[:link_type]
......
......@@ -16,8 +16,8 @@ module MergeRequests
]
target_branch = vulnerability.target_branch || @project.default_branch
return error("Can't create merge_request") unless can_create_merge_request?(source_branch)
return error("Can't create merge request") if vulnerability.remediations.empty?
return error("User is not permitted to create merge request") unless can_create_merge_request?(source_branch)
return error("No remediations available for merge request") if vulnerability.remediations.empty?
patch_result = create_patch(vulnerability, source_branch, target_branch)
......
......@@ -47,7 +47,8 @@ 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_params[:uuid] = calculate_uuid_v5(finding)
vulnerability_finding = create_or_find_vulnerability_finding(finding, vulnerability_params)
update_vulnerability_scanner(finding)
......@@ -60,6 +61,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)
......@@ -79,8 +82,6 @@ module Security
.create_with(create_params)
.find_or_initialize_by(find_params)
vulnerability_finding.uuid = calculcate_uuid_v5(vulnerability_finding, find_params)
vulnerability_finding.save!
vulnerability_finding
rescue ActiveRecord::RecordNotUnique
......@@ -90,11 +91,11 @@ module Security
end
end
def calculcate_uuid_v5(vulnerability_finding, finding_params)
def calculate_uuid_v5(vulnerability_finding)
uuid_v5_name_components = {
report_type: vulnerability_finding.report_type,
primary_identifier_fingerprint: vulnerability_finding.primary_identifier&.fingerprint || finding_params.dig(:primary_identifier, :fingerprint),
location_fingerprint: vulnerability_finding.location_fingerprint,
primary_identifier_fingerprint: vulnerability_finding.primary_fingerprint,
location_fingerprint: vulnerability_finding.location.fingerprint,
project_id: project.id
}
......@@ -104,8 +105,6 @@ module Security
name = uuid_v5_name_components.values.join('-')
Gitlab::AppLogger.debug(message: "Generating UUIDv5 with name: #{name}") if Gitlab.dev_env_or_com?
Gitlab::Vulnerabilities::CalculateFindingUUID.call(name)
end
......@@ -125,6 +124,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
......
......@@ -35,7 +35,7 @@
.card-footer.p-3
.float-right{ class: ("invisible" unless purchase_link.action == 'upgrade' || is_current_plan) }
- if show_contact_sales_button?(purchase_link.action)
= link_to s_('BillingPlan|Contact sales'), "#{contact_sales_url}?test=inappcontactsales#{plan.code}", class: "btn btn-success btn-inverted", data: { **experiment_tracking_data_for_button_click('contact_sales') }
= link_to s_('BillingPlan|Contact sales'), "#{contact_sales_url}?test=inappcontactsales#{plan.code}", class: "btn btn-success-secondary gl-button", data: { **experiment_tracking_data_for_button_click('contact_sales') }
- upgrade_button_class = "disabled" if is_current_plan && !namespace.trial_active?
- cta_class = '-new' if use_new_purchase_flow?(namespace)
= link_to s_('BillingPlan|Upgrade'), plan_purchase_or_upgrade_url(namespace, plan, current_plan), class: "btn btn-success #{upgrade_button_class} billing-cta-purchase#{cta_class}", data: { **experiment_tracking_data_for_button_click('upgrade') }
= link_to s_('BillingPlan|Upgrade'), plan_purchase_or_upgrade_url(namespace, plan, current_plan), class: "btn btn-success gl-button #{upgrade_button_class} billing-cta-purchase#{cta_class}", data: { **experiment_tracking_data_for_button_click('upgrade') }
......@@ -24,10 +24,10 @@
%p= s_("BillingPlans|This group uses the plan associated with its parent group.")
- parent_billing_page_link = link_to parent_group.full_name, group_billings_path(parent_group)
%p= s_("BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}.").html_safe % { parent_billing_page_link: parent_billing_page_link }
= link_to s_("BillingPlans|Manage plan"), group_billings_path(parent_group), class: 'btn btn-success'
= link_to s_("BillingPlans|Manage plan"), group_billings_path(parent_group), class: 'btn btn-success gl-button'
- else
= render 'shared/billings/trial_status', namespace: namespace
- if namespace.eligible_for_trial?
- glm_content = namespace_for_user ? 'user-billing' : 'group-billing'
%p= link_to 'Start your free trial', new_trial_registration_path(glm_source: 'gitlab.com', glm_content: glm_content), class: 'btn btn-primary'
%p= link_to 'Start your free trial', new_trial_registration_path(glm_source: 'gitlab.com', glm_content: glm_content), class: 'btn btn-primary gl-button'
---
title: Add Vulnerabilities::FindingLink model
merge_request: 46555
author:
type: added
---
title: Improve error messages for Vulnerability Issue/MR creation
merge_request: 46589
author:
type: changed
......@@ -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
......@@ -94,8 +97,6 @@ module Gitlab
end
end
protected
def primary_fingerprint
primary_identifier&.fingerprint
end
......
# 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
......
......@@ -37,7 +37,7 @@ RSpec.describe Issues::CreateFromVulnerabilityDataService, '#execute' do
it 'returns expected error' do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq("Can't create issue")
expect(result[:message]).to eq("User is not permitted to create issue")
end
end
......@@ -47,7 +47,7 @@ RSpec.describe Issues::CreateFromVulnerabilityDataService, '#execute' do
it 'returns expected error' do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq("Can't create issue")
expect(result[:message]).to eq("User is not permitted to create issue")
end
end
......
......@@ -80,7 +80,7 @@ RSpec.describe Issues::CreateFromVulnerabilityService, '#execute' do
it 'returns expected error' do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq("Can't create issue")
expect(result[:message]).to eq("User is not permitted to create issue")
end
end
......@@ -89,7 +89,7 @@ RSpec.describe Issues::CreateFromVulnerabilityService, '#execute' do
it 'returns expected error' do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq("Can't create issue")
expect(result[:message]).to eq("User is not permitted to create issue")
end
end
......
......@@ -43,7 +43,7 @@ RSpec.describe MergeRequests::CreateFromVulnerabilityDataService, '#execute' do
it 'returns expected error' do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq("Can't create merge_request")
expect(result[:message]).to eq("User is not permitted to create merge request")
end
end
......@@ -53,7 +53,7 @@ RSpec.describe MergeRequests::CreateFromVulnerabilityDataService, '#execute' do
it 'returns expected error' do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq("Can't create merge_request")
expect(result[:message]).to eq("User is not permitted to create merge request")
end
end
......@@ -213,7 +213,7 @@ RSpec.describe MergeRequests::CreateFromVulnerabilityDataService, '#execute' do
it 'return expected error' do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq("Can't create merge request")
expect(result[:message]).to eq("No remediations available for merge request")
end
end
end
......
......@@ -2,6 +2,19 @@
require 'spec_helper'
UUID_REGEXP = Regexp.new("^([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-" \
"([0-9a-f]{2})([0-9a-f]{2})-([0-9a-f]{12})$").freeze
RSpec::Matchers.define :be_uuid_v5 do
match do |string|
expect(string).to be_a(String)
uuid_components = string.downcase.scan(UUID_REGEXP).first
time_hi_and_version = uuid_components[2].to_i(16)
(time_hi_and_version >> 12) == 5
end
end
RSpec.describe Security::StoreReportService, '#execute' do
let_it_be(:user) { create(:user) }
let(:artifact) { create(:ee_ci_job_artifact, trait) }
......@@ -24,11 +37,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
......@@ -57,7 +70,9 @@ RSpec.describe Security::StoreReportService, '#execute' do
end
it 'calculates UUIDv5 for all findings' do
expect(Vulnerabilities::Finding.pluck(:uuid)).to all(be_a(String))
subject
uuids = Vulnerabilities::Finding.pluck(:uuid)
expect(uuids).to all(be_uuid_v5)
end
end
......
# 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
......
......@@ -49,6 +49,7 @@ module Gitlab
s
search
sent_notifications
sitemap
sitemap.xml
sitemap.xml.gz
slash-command-logo.png
......
......@@ -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')
......
......@@ -160,6 +160,7 @@ module Gitlab
projects_with_tracing_enabled: count(ProjectTracingSetting),
projects_with_error_tracking_enabled: count(::ErrorTracking::ProjectErrorTrackingSetting.where(enabled: true)),
projects_with_alerts_service_enabled: count(AlertsService.active),
projects_with_alerts_created: distinct_count(::AlertManagement::Alert, :project_id),
projects_with_prometheus_alerts: distinct_count(PrometheusAlert, :project_id),
projects_with_terraform_reports: distinct_count(::Ci::JobArtifact.terraform_reports, :project_id),
projects_with_terraform_states: distinct_count(::Terraform::State, :project_id),
......@@ -215,7 +216,8 @@ module Gitlab
# rubocop: enable UsageData/LargeTable:
packages: count(::Packages::Package.where(last_28_days_time_period)),
personal_snippets: count(PersonalSnippet.where(last_28_days_time_period)),
project_snippets: count(ProjectSnippet.where(last_28_days_time_period))
project_snippets: count(ProjectSnippet.where(last_28_days_time_period)),
projects_with_alerts_created: distinct_count(::AlertManagement::Alert.where(last_28_days_time_period), :project_id)
}.merge(
snowplow_event_counts(last_28_days_time_period(column: :collector_tstamp)),
aggregated_metrics_monthly
......
......@@ -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 ""
......
......@@ -120,7 +120,9 @@ describe('MilestoneToken', () => {
wrapper.vm.fetchMilestoneBySearchTerm('foo');
return waitForPromises().then(() => {
expect(createFlash).toHaveBeenCalledWith('There was a problem fetching milestones.');
expect(createFlash).toHaveBeenCalledWith({
message: 'There was a problem fetching milestones.',
});
});
});
......
# 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
......@@ -107,7 +107,7 @@ RSpec.describe Gitlab::PathRegex do
end
let(:sitemap_words) do
%w(sitemap.xml sitemap.xml.gz)
%w(sitemap sitemap.xml sitemap.xml.gz)
end
let(:ee_top_level_words) do
......@@ -177,7 +177,7 @@ RSpec.describe Gitlab::PathRegex do
# We ban new items in this list, see https://gitlab.com/gitlab-org/gitlab/-/issues/215362
it 'does not allow expansion' do
expect(described_class::TOP_LEVEL_ROUTES.size).to eq(43)
expect(described_class::TOP_LEVEL_ROUTES.size).to eq(44)
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
......@@ -308,6 +308,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
projects_with_tracing_enabled: 2,
projects_with_error_tracking_enabled: 2
)
expect(described_class.usage_activity_by_stage_monitor(described_class.last_28_days_time_period)).to include(
clusters: 1,
clusters_applications_prometheus: 1,
......@@ -470,6 +471,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:projects_with_prometheus_alerts]).to eq(2)
expect(count_data[:projects_with_terraform_reports]).to eq(2)
expect(count_data[:projects_with_terraform_states]).to eq(2)
expect(count_data[:projects_with_alerts_created]).to eq(1)
expect(count_data[:protected_branches]).to eq(2)
expect(count_data[:protected_branches_except_default]).to eq(1)
expect(count_data[:terraform_reports]).to eq(6)
......@@ -611,6 +613,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
create(:deployment, :success, deployment_options)
create(:project_snippet, project: project, created_at: n.days.ago)
create(:personal_snippet, created_at: n.days.ago)
create(:alert_management_alert, project: project, created_at: n.days.ago)
end
stub_application_setting(self_monitoring_project: project)
......@@ -631,6 +634,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(counts_monthly[:snippets]).to eq(2)
expect(counts_monthly[:personal_snippets]).to eq(1)
expect(counts_monthly[:project_snippets]).to eq(1)
expect(counts_monthly[:projects_with_alerts_created]).to eq(1)
expect(counts_monthly[:packages]).to eq(1)
expect(counts_monthly[:promoted_issues]).to eq(1)
end
......
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20201102112206_rename_sitemap_namespace.rb')
RSpec.describe RenameSitemapNamespace do
let(:namespaces) { table(:namespaces) }
let(:routes) { table(:routes) }
let(:sitemap_path) { 'sitemap' }
it 'correctly run #up and #down' do
create_namespace(sitemap_path)
reversible_migration do |migration|
migration.before -> {
expect(namespaces.pluck(:path)).to contain_exactly(sitemap_path)
}
migration.after -> {
expect(namespaces.pluck(:path)).to contain_exactly(sitemap_path + '0')
}
end
end
def create_namespace(path)
namespaces.create!(name: path, path: path).tap do |namespace|
routes.create!(path: namespace.path, name: namespace.name, source_id: namespace.id, source_type: 'Namespace')
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
......@@ -114,14 +114,6 @@ RSpec.describe Deployment do
deployment.run!
end
it 'does not execute Deployments::ExecuteHooksWorker when feature is disabled' do
stub_feature_flags(ci_send_deployment_hook_when_start: false)
expect(Deployments::ExecuteHooksWorker)
.not_to receive(:perform_async).with(deployment.id)
deployment.run!
end
it 'executes Deployments::DropOlderDeploymentsWorker asynchronously' do
expect(Deployments::DropOlderDeploymentsWorker)
.to receive(:perform_async).once.with(deployment.id)
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe NotePolicy do
describe '#rules' do
describe '#rules', :aggregate_failures do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
......@@ -11,14 +11,15 @@ RSpec.describe NotePolicy do
let(:policy) { described_class.new(user, note) }
let(:note) { create(:note, noteable: noteable, author: user, project: project) }
shared_examples_for 'user cannot read or act on the note' do
specify do
expect(policy).to be_disallowed(:admin_note, :reposition_note, :resolve_note, :read_note, :award_emoji)
end
end
shared_examples_for 'a discussion with a private noteable' do
context 'when the note author can no longer see the noteable' do
it 'can not edit nor read the note' do
expect(policy).to be_disallowed(:admin_note)
expect(policy).to be_disallowed(:resolve_note)
expect(policy).to be_disallowed(:read_note)
expect(policy).to be_disallowed(:award_emoji)
end
it_behaves_like 'user cannot read or act on the note'
end
context 'when the note author can still see the noteable' do
......@@ -28,6 +29,7 @@ RSpec.describe NotePolicy do
it 'can edit the note' do
expect(policy).to be_allowed(:admin_note)
expect(policy).to be_allowed(:reposition_note)
expect(policy).to be_allowed(:resolve_note)
expect(policy).to be_allowed(:read_note)
expect(policy).to be_allowed(:award_emoji)
......@@ -35,6 +37,13 @@ RSpec.describe NotePolicy do
end
end
shared_examples_for 'a note on a public noteable' do
it 'can only read and award emoji on the note' do
expect(policy).to be_allowed(:read_note, :award_emoji)
expect(policy).to be_disallowed(:reposition_note, :admin_note, :resolve_note)
end
end
context 'when the noteable is a deleted commit' do
let(:commit) { nil }
let(:note) { create(:note_on_commit, commit_id: '12345678', author: user, project: project) }
......@@ -42,6 +51,7 @@ RSpec.describe NotePolicy do
it 'allows to read' do
expect(policy).to be_allowed(:read_note)
expect(policy).to be_disallowed(:admin_note)
expect(policy).to be_disallowed(:reposition_note)
expect(policy).to be_disallowed(:resolve_note)
expect(policy).to be_disallowed(:award_emoji)
end
......@@ -66,31 +76,60 @@ RSpec.describe NotePolicy do
end
end
context 'when the noteable is a Design' do
include DesignManagementTestHelpers
let(:note) { create(:note, noteable: noteable, project: project) }
let(:noteable) { create(:design, issue: issue) }
before do
enable_design_management
end
it 'can read, award emoji and reposition the note' do
expect(policy).to be_allowed(:reposition_note, :read_note, :award_emoji)
expect(policy).to be_disallowed(:admin_note, :resolve_note)
end
context 'when project is private' do
let(:project) { create(:project, :private) }
it_behaves_like 'user cannot read or act on the note'
end
end
context 'when the noteable is a personal snippet' do
let(:noteable) { create(:personal_snippet, :public) }
let(:note) { create(:note, noteable: noteable, author: user) }
let(:note) { create(:note, noteable: noteable) }
it 'can edit note' do
expect(policy).to be_allowed(:admin_note)
expect(policy).to be_allowed(:resolve_note)
expect(policy).to be_allowed(:read_note)
end
it_behaves_like 'a note on a public noteable'
context 'when user is the author of the personal snippet' do
let(:note) { create(:note, noteable: noteable, author: user) }
context 'when it is private' do
let(:noteable) { create(:personal_snippet, :private) }
it 'can edit note' do
expect(policy).to be_allowed(:read_note, :award_emoji, :admin_note, :reposition_note, :resolve_note)
end
context 'when it is private' do
let(:noteable) { create(:personal_snippet, :private) }
it 'can not edit nor read the note' do
expect(policy).to be_disallowed(:admin_note)
expect(policy).to be_disallowed(:resolve_note)
expect(policy).to be_disallowed(:read_note)
it_behaves_like 'user cannot read or act on the note'
end
end
end
context 'when the project is public' do
context 'when user is not the author of the note' do
let(:note) { create(:note, noteable: noteable, project: project) }
it_behaves_like 'a note on a public noteable'
end
context 'when the note author is not a project member' do
it 'can edit a note' do
expect(policy).to be_allowed(:admin_note)
expect(policy).to be_allowed(:reposition_note)
expect(policy).to be_allowed(:resolve_note)
expect(policy).to be_allowed(:read_note)
end
......@@ -101,6 +140,7 @@ RSpec.describe NotePolicy do
it 'can edit note' do
expect(policy).to be_allowed(:admin_note)
expect(policy).to be_allowed(:reposition_note)
expect(policy).to be_allowed(:resolve_note)
expect(policy).to be_allowed(:read_note)
end
......@@ -132,6 +172,7 @@ RSpec.describe NotePolicy do
it 'can edit a note' do
expect(policy).to be_allowed(:admin_note)
expect(policy).to be_allowed(:reposition_note)
expect(policy).to be_allowed(:resolve_note)
expect(policy).to be_allowed(:read_note)
end
......@@ -140,6 +181,7 @@ RSpec.describe NotePolicy do
context 'when the note author is not a project member' do
it 'can not edit a note' do
expect(policy).to be_disallowed(:admin_note)
expect(policy).to be_disallowed(:reposition_note)
expect(policy).to be_disallowed(:resolve_note)
end
......@@ -154,6 +196,7 @@ RSpec.describe NotePolicy do
it 'allows the author to manage the discussion' do
expect(policy).to be_allowed(:admin_note)
expect(policy).to be_allowed(:reposition_note)
expect(policy).to be_allowed(:resolve_note)
expect(policy).to be_allowed(:read_note)
expect(policy).to be_allowed(:award_emoji)
......@@ -180,21 +223,13 @@ RSpec.describe NotePolicy do
shared_examples_for 'user can act on the note' do
it 'allows the user to read the note' do
expect(policy).not_to be_allowed(:admin_note)
expect(policy).to be_disallowed(:admin_note)
expect(policy).to be_disallowed(:reposition_note)
expect(policy).to be_allowed(:resolve_note)
expect(policy).to be_allowed(:award_emoji)
end
end
shared_examples_for 'user cannot read or act on the note' do
it 'allows user to read the note' do
expect(policy).not_to be_allowed(:admin_note)
expect(policy).not_to be_allowed(:resolve_note)
expect(policy).not_to be_allowed(:read_note)
expect(policy).not_to be_allowed(:award_emoji)
end
end
context 'when noteable is a public issue' do
let(:note) { create(:note, system: true, noteable: noteable, author: user, project: project) }
......@@ -274,42 +309,42 @@ RSpec.describe NotePolicy do
shared_examples_for 'confidential notes permissions' do
it 'does not allow non members to read confidential notes and replies' do
expect(permissions(non_member, confidential_note)).to be_disallowed(:read_note, :admin_note, :resolve_note, :award_emoji)
expect(permissions(non_member, confidential_note)).to be_disallowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji)
end
it 'does not allow guests to read confidential notes and replies' do
expect(permissions(guest, confidential_note)).to be_disallowed(:read_note, :admin_note, :resolve_note, :award_emoji)
expect(permissions(guest, confidential_note)).to be_disallowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji)
end
it 'allows reporter to read all notes but not resolve and admin them' do
expect(permissions(reporter, confidential_note)).to be_allowed(:read_note, :award_emoji)
expect(permissions(reporter, confidential_note)).to be_disallowed(:admin_note, :resolve_note)
expect(permissions(reporter, confidential_note)).to be_disallowed(:admin_note, :reposition_note, :resolve_note)
end
it 'allows developer to read and resolve all notes' do
expect(permissions(developer, confidential_note)).to be_allowed(:read_note, :award_emoji, :resolve_note)
expect(permissions(developer, confidential_note)).to be_disallowed(:admin_note)
expect(permissions(developer, confidential_note)).to be_disallowed(:admin_note, :reposition_note)
end
it 'allows maintainers to read all notes and admin them' do
expect(permissions(maintainer, confidential_note)).to be_allowed(:read_note, :admin_note, :resolve_note, :award_emoji)
expect(permissions(maintainer, confidential_note)).to be_allowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji)
end
context 'when admin mode is enabled', :enable_admin_mode do
it 'allows admins to read all notes and admin them' do
expect(permissions(admin, confidential_note)).to be_allowed(:read_note, :admin_note, :resolve_note, :award_emoji)
expect(permissions(admin, confidential_note)).to be_allowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji)
end
end
context 'when admin mode is disabled' do
it 'does not allow non members to read confidential notes and replies' do
expect(permissions(admin, confidential_note)).to be_disallowed(:read_note, :admin_note, :resolve_note, :award_emoji)
expect(permissions(admin, confidential_note)).to be_disallowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji)
end
end
it 'allows noteable author to read and resolve all notes' do
expect(permissions(author, confidential_note)).to be_allowed(:read_note, :resolve_note, :award_emoji)
expect(permissions(author, confidential_note)).to be_disallowed(:admin_note)
expect(permissions(author, confidential_note)).to be_disallowed(:admin_note, :reposition_note)
end
end
......@@ -321,7 +356,7 @@ RSpec.describe NotePolicy do
it 'allows noteable assignees to read all notes' do
expect(permissions(assignee, confidential_note)).to be_allowed(:read_note, :award_emoji)
expect(permissions(assignee, confidential_note)).to be_disallowed(:admin_note, :resolve_note)
expect(permissions(assignee, confidential_note)).to be_disallowed(:admin_note, :reposition_note, :resolve_note)
end
end
......@@ -333,7 +368,7 @@ RSpec.describe NotePolicy do
it 'allows noteable assignees to read all notes' do
expect(permissions(assignee, confidential_note)).to be_allowed(:read_note, :award_emoji)
expect(permissions(assignee, confidential_note)).to be_disallowed(:admin_note, :resolve_note)
expect(permissions(assignee, confidential_note)).to be_disallowed(:admin_note, :reposition_note, :resolve_note)
end
end
......@@ -350,11 +385,11 @@ RSpec.describe NotePolicy do
it 'allows snippet author to read and resolve all notes' do
expect(permissions(author, confidential_note)).to be_allowed(:read_note, :resolve_note, :award_emoji)
expect(permissions(author, confidential_note)).to be_disallowed(:admin_note)
expect(permissions(author, confidential_note)).to be_disallowed(:admin_note, :reposition_note)
end
it 'does not allow maintainers to read confidential notes and replies' do
expect(permissions(maintainer, confidential_note)).to be_disallowed(:read_note, :admin_note, :resolve_note, :award_emoji)
expect(permissions(maintainer, confidential_note)).to be_disallowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji)
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
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')
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Members::InviteService do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:project_user) { create(:user) }
before do
project.add_maintainer(user)
end
it 'adds an existing user to members' do
params = { email: project_user.email.to_s, access_level: Gitlab::Access::GUEST }
result = described_class.new(user, params).execute(project)
expect(result[:status]).to eq(:success)
expect(project.users).to include project_user
end
it 'creates a new user for an unknown email address' do
params = { email: 'email@example.org', access_level: Gitlab::Access::GUEST }
result = described_class.new(user, params).execute(project)
expect(result[:status]).to eq(:success)
end
it 'limits the number of emails to 100' do
emails = Array.new(101).map { |n| "email#{n}@example.com" }
params = { email: emails, access_level: Gitlab::Access::GUEST }
result = described_class.new(user, params).execute(project)
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Too many users specified (limit is 100)')
end
it 'does not invite an invalid email' do
params = { email: project_user.id.to_s, access_level: Gitlab::Access::GUEST }
result = described_class.new(user, params).execute(project)
expect(result[:status]).to eq(:error)
expect(result[:message][project_user.id.to_s]).to eq("Invite email is invalid")
expect(project.users).not_to include project_user
end
it 'does not invite to an invalid access level' do
params = { email: project_user.email, access_level: -1 }
result = described_class.new(user, params).execute(project)
expect(result[:status]).to eq(:error)
expect(result[:message][project_user.email]).to eq("Access level is not included in the list")
end
it 'does not add a member with an existing invite' do
invited_member = create(:project_member, :invited, project: project)
params = { email: invited_member.invite_email,
access_level: Gitlab::Access::GUEST }
result = described_class.new(user, params).execute(project)
expect(result[:status]).to eq(:error)
expect(result[:message][invited_member.invite_email]).to eq("Member already invited to #{project.name}")
end
end
......@@ -19,9 +19,11 @@ RSpec.describe Packages::Composer::VersionParserService do
nil | '1.7.x' | '1.7.x-dev'
'v1.0.0' | nil | '1.0.0'
'v1.0' | nil | '1.0'
'v1.0.1+meta' | nil | '1.0.1+meta'
'1.0' | nil | '1.0'
'1.0.2' | nil | '1.0.2'
'1.0.2-beta2' | nil | '1.0.2-beta2'
'1.0.1+meta' | nil | '1.0.1+meta'
end
with_them do
......
......@@ -23,6 +23,10 @@ module Spec
all_rows[1]
end
def third_row
all_rows[2]
end
def invite_users_form
page.find('[data-testid="invite-users-form"]')
end
......
......@@ -45,7 +45,7 @@ RSpec.describe ApplicationWorker do
instance.jid = 'a jid'
expect(result).to include(
'class' => worker.class,
'class' => instance.class.name,
'job_status' => 'running',
'queue' => worker.queue,
'jid' => instance.jid
......@@ -69,7 +69,7 @@ RSpec.describe ApplicationWorker do
it 'does not override predefined context keys with custom payload' do
payload['class'] = 'custom value'
expect(result).to include('class' => worker.class)
expect(result).to include('class' => instance.class.name)
end
end
......
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