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

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

Add DAST recommendation about the importance of authentication

See merge request gitlab-org/gitlab!46913
parents f16f05ff e1e7da41
......@@ -52,7 +52,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
def toggle_shared_runners
if Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true) && !project.shared_runners_enabled && project.group && project.group.shared_runners_setting == 'disabled_and_unoverridable'
if !project.shared_runners_enabled && project.group && project.group.shared_runners_setting == 'disabled_and_unoverridable'
return redirect_to project_runners_path(@project), alert: _("Cannot enable shared runners because parent group does not allow it")
end
......
# frozen_string_literal: true
module Types
class GroupInvitationType < BaseObject
expose_permissions Types::PermissionTypes::Group
authorize :read_group
implements InvitationInterface
graphql_name 'GroupInvitation'
description 'Represents a Group Invitation'
field :group, Types::GroupType, null: true,
description: 'Group that a User is invited to',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.source_id).find }
end
end
# frozen_string_literal: true
module Types
module InvitationInterface
include BaseInterface
field :email, GraphQL::STRING_TYPE, null: false,
description: 'Email of the member to invite'
field :access_level, Types::AccessLevelType, null: true,
description: 'GitLab::Access level'
field :created_by, Types::UserType, null: true,
description: 'User that authorized membership'
field :created_at, Types::TimeType, null: true,
description: 'Date and time the membership was created'
field :updated_at, Types::TimeType, null: true,
description: 'Date and time the membership was last updated'
field :expires_at, Types::TimeType, null: true,
description: 'Date and time the membership expires'
field :user, Types::UserType, null: true,
description: 'User that is associated with the member object'
definition_methods do
def resolve_type(object, context)
case object
when GroupMember
Types::GroupInvitationType
when ProjectMember
Types::ProjectInvitationType
else
raise ::Gitlab::Graphql::Errors::BaseError, "Unknown member type #{object.class.name}"
end
end
end
end
end
# frozen_string_literal: true
module Types
class ProjectInvitationType < BaseObject
graphql_name 'ProjectInvitation'
description 'Represents a Project Membership Invitation'
expose_permissions Types::PermissionTypes::Project
implements InvitationInterface
authorize :read_project
field :project, Types::ProjectType, null: true,
description: 'Project ID for the project of the invitation'
def project
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.source_id).find
end
end
end
......@@ -37,27 +37,6 @@ module FromUnion
# rubocop: disable Gitlab/Union
extend FromSetOperator
define_set_operator Gitlab::SQL::Union
alias_method :from_union_set_operator, :from_union
def from_union(members, remove_duplicates: true, alias_as: table_name)
if Feature.enabled?(:sql_set_operators)
from_union_set_operator(members, remove_duplicates: remove_duplicates, alias_as: alias_as)
else
# The original from_union method.
standard_from_union(members, remove_duplicates: remove_duplicates, alias_as: alias_as)
end
end
private
def standard_from_union(members, remove_duplicates: true, alias_as: table_name)
union = Gitlab::SQL::Union
.new(members, remove_duplicates: remove_duplicates)
.to_sql
from(Arel.sql("(#{union}) #{alias_as}"))
end
# rubocop: enable Gitlab/Union
end
end
......@@ -96,6 +96,8 @@ class Member < ApplicationRecord
scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
scope :with_user, -> (user) { where(user: user) }
scope :with_user_by_email, -> (email) { left_join_users.where(users: { email: email } ) }
scope :preload_user_and_notification_settings, -> { preload(user: :notification_settings) }
scope :with_source_id, ->(source_id) { where(source_id: source_id) }
......
......@@ -393,7 +393,6 @@ class Namespace < ApplicationRecord
end
def changing_shared_runners_enabled_is_allowed
return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
return unless new_record? || changes.has_key?(:shared_runners_enabled)
if shared_runners_enabled && has_parent? && parent.shared_runners_setting == 'disabled_and_unoverridable'
......@@ -402,7 +401,6 @@ class Namespace < ApplicationRecord
end
def changing_allow_descendants_override_disabled_shared_runners_is_allowed
return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
return unless new_record? || changes.has_key?(:allow_descendants_override_disabled_shared_runners)
if shared_runners_enabled && !new_record?
......
......@@ -1195,7 +1195,6 @@ class Project < ApplicationRecord
end
def changing_shared_runners_enabled_is_allowed
return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
return unless new_record? || changes.has_key?(:shared_runners_enabled)
if shared_runners_enabled && group && group.shared_runners_setting == 'disabled_and_unoverridable'
......
# frozen_string_literal: true
class InvitationPresenter < Gitlab::View::Presenter::Delegated
presents :invitation
end
# frozen_string_literal: true
module Members
class InviteService < Members::BaseService
DEFAULT_LIMIT = 100
attr_reader :errors
def initialize(current_user, params)
@current_user, @params = current_user, params.dup
@errors = {}
end
def execute(source)
return error(s_('Email cannot be blank')) if params[:email].blank?
emails = params[:email].split(',').uniq.flatten
return error(s_("Too many users specified (limit is %{user_limit})") % { user_limit: user_limit }) if
user_limit && emails.size > user_limit
emails.each do |email|
next if existing_member?(source, email)
next if existing_invite?(source, email)
if existing_user?(email)
add_existing_user_as_member(current_user, source, params, email)
next
end
invite_new_member_and_user(current_user, source, params, email)
end
return success unless errors.any?
error(errors)
end
private
def invite_new_member_and_user(current_user, source, params, email)
new_member = (source.class.name + 'Member').constantize.create(source_id: source.id,
user_id: nil,
access_level: params[:access_level],
invite_email: email,
created_by_id: current_user.id,
expires_at: params[:expires_at],
requested_at: Time.current.utc)
unless new_member.valid? && new_member.persisted?
errors[params[:email]] = new_member.errors.full_messages.to_sentence
end
end
def add_existing_user_as_member(current_user, source, params, email)
new_member = create_member(current_user, existing_user(email), source, params.merge({ invite_email: email }))
unless new_member.valid? && new_member.persisted?
errors[email] = new_member.errors.full_messages.to_sentence
end
end
def create_member(current_user, user, source, params)
source.add_user(user, params[:access_level], current_user: current_user, expires_at: params[:expires_at])
end
def user_limit
limit = params.fetch(:limit, DEFAULT_LIMIT)
limit && limit < 0 ? nil : limit
end
def existing_member?(source, email)
existing_member = source.members.with_user_by_email(email).exists?
if existing_member
errors[email] = "Already a member of #{source.name}"
return true
end
false
end
def existing_invite?(source, email)
existing_invite = source.members.search_invite_email(email).exists?
if existing_invite
errors[email] = "Member already invited to #{source.name}"
return true
end
false
end
def existing_user(email)
User.find_by_email(email)
end
def existing_user?(email)
existing_user(email).present?
end
end
end
......@@ -9,7 +9,7 @@ module Packages
def execute
if @tag_name.present?
@tag_name.match(Gitlab::Regex.composer_package_version_regex).captures[0]
@tag_name.delete_prefix('v')
elsif @branch_name.present?
branch_sufix_or_prefix(@branch_name.match(Gitlab::Regex.composer_package_version_regex))
end
......
......@@ -16,6 +16,7 @@ module Search
Gitlab::SearchResults.new(current_user,
params[:search],
projects,
order_by: params[:order_by],
sort: params[:sort],
filters: { state: params[:state], confidential: params[:confidential] })
end
......
......@@ -16,6 +16,7 @@ module Search
params[:search],
projects,
group: group,
order_by: params[:order_by],
sort: params[:sort],
filters: { state: params[:state], confidential: params[:confidential] }
)
......
......@@ -17,6 +17,7 @@ module Search
params[:search],
project: project,
repository_ref: params[:repository_ref],
order_by: params[:order_by],
sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] }
)
......
---
title: Allow semver versions in composer packages
merge_request: 46301
author:
type: fixed
---
title: Add ability to sort to search API
merge_request: 46646
author:
type: added
---
title: Enable refactored union set operator
merge_request: 46295
author:
type: added
---
title: Add API post /invitations by email
merge_request: 45950
author:
type: added
---
name: disable_shared_runners_on_group
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36080
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258991
type: development
group: group::runner
default_enabled: true
---
name: sql_set_operators
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39786
rollout_issue_url:
group: group::access
type: development
default_enabled: false
......@@ -8,3 +8,4 @@ Grape::Validations.register_validator(:integer_none_any, ::API::Validations::Val
Grape::Validations.register_validator(:array_none_any, ::API::Validations::Validators::ArrayNoneAny)
Grape::Validations.register_validator(:check_assignees_count, ::API::Validations::Validators::CheckAssigneesCount)
Grape::Validations.register_validator(:untrusted_regexp, ::API::Validations::Validators::UntrustedRegexp)
Grape::Validations.register_validator(:email_or_email_list, ::API::Validations::Validators::EmailOrEmailList)
# frozen_string_literal: true
class CreateVulnerabilityFindingLinks < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
create_table :vulnerability_finding_links, if_not_exists: true do |t|
t.timestamps_with_timezone null: false
t.references :vulnerability_occurrence, index: { name: 'finding_links_on_vulnerability_occurrence_id' }, null: false, foreign_key: { on_delete: :cascade }
t.text :name, limit: 255
t.text :url, limit: 2048, null: false
end
add_text_limit :vulnerability_finding_links, :name, 255
add_text_limit :vulnerability_finding_links, :url, 2048
end
def down
drop_table :vulnerability_finding_links
end
end
50e4e42c804d3abdcfe9ab2bbb890262d4b2ddd93bff1b2af1da1e55a0300cf5
\ No newline at end of file
......@@ -17104,6 +17104,26 @@ CREATE SEQUENCE vulnerability_feedback_id_seq
ALTER SEQUENCE vulnerability_feedback_id_seq OWNED BY vulnerability_feedback.id;
CREATE TABLE vulnerability_finding_links (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
vulnerability_occurrence_id bigint NOT NULL,
name text,
url text NOT NULL,
CONSTRAINT check_55f0a95439 CHECK ((char_length(name) <= 255)),
CONSTRAINT check_b7fe886df6 CHECK ((char_length(url) <= 2048))
);
CREATE SEQUENCE vulnerability_finding_links_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE vulnerability_finding_links_id_seq OWNED BY vulnerability_finding_links.id;
CREATE TABLE vulnerability_historical_statistics (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
......@@ -18203,6 +18223,8 @@ ALTER TABLE ONLY vulnerability_exports ALTER COLUMN id SET DEFAULT nextval('vuln
ALTER TABLE ONLY vulnerability_feedback ALTER COLUMN id SET DEFAULT nextval('vulnerability_feedback_id_seq'::regclass);
ALTER TABLE ONLY vulnerability_finding_links ALTER COLUMN id SET DEFAULT nextval('vulnerability_finding_links_id_seq'::regclass);
ALTER TABLE ONLY vulnerability_historical_statistics ALTER COLUMN id SET DEFAULT nextval('vulnerability_historical_statistics_id_seq'::regclass);
ALTER TABLE ONLY vulnerability_identifiers ALTER COLUMN id SET DEFAULT nextval('vulnerability_identifiers_id_seq'::regclass);
......@@ -19646,6 +19668,9 @@ ALTER TABLE ONLY vulnerability_exports
ALTER TABLE ONLY vulnerability_feedback
ADD CONSTRAINT vulnerability_feedback_pkey PRIMARY KEY (id);
ALTER TABLE ONLY vulnerability_finding_links
ADD CONSTRAINT vulnerability_finding_links_pkey PRIMARY KEY (id);
ALTER TABLE ONLY vulnerability_historical_statistics
ADD CONSTRAINT vulnerability_historical_statistics_pkey PRIMARY KEY (id);
......@@ -19870,6 +19895,8 @@ CREATE UNIQUE INDEX epic_user_mentions_on_epic_id_and_note_id_index ON epic_user
CREATE UNIQUE INDEX epic_user_mentions_on_epic_id_index ON epic_user_mentions USING btree (epic_id) WHERE (note_id IS NULL);
CREATE INDEX finding_links_on_vulnerability_occurrence_id ON vulnerability_finding_links USING btree (vulnerability_occurrence_id);
CREATE INDEX idx_audit_events_on_entity_id_desc_author_id_created_at ON audit_events USING btree (entity_id, entity_type, id DESC, author_id, created_at);
CREATE INDEX idx_ci_pipelines_artifacts_locked ON ci_pipelines USING btree (ci_ref_id, id) WHERE (locked = 1);
......@@ -24195,6 +24222,9 @@ ALTER TABLE ONLY gpg_signatures
ALTER TABLE ONLY board_group_recent_visits
ADD CONSTRAINT fk_rails_ca04c38720 FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE;
ALTER TABLE ONLY vulnerability_finding_links
ADD CONSTRAINT fk_rails_cbdfde27ce FOREIGN KEY (vulnerability_occurrence_id) REFERENCES vulnerability_occurrences(id) ON DELETE CASCADE;
ALTER TABLE ONLY issues_self_managed_prometheus_alert_events
ADD CONSTRAINT fk_rails_cc5d88bbb0 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
......
......@@ -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,15 +177,17 @@ 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#authenticating-with-a-ci-job-token)
- [NPM Repository](../user/packages/npm_registry/index.md#authenticate-with-a-ci-job-token)
- [Nuget Repository](../user/packages/nuget_repository/index.md)
- [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"
}
}
```
......@@ -26,6 +26,8 @@ GET /search
| `search` | string | yes | The search query |
| `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. |
| `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. |
| `order_by` | string | no | Allowed values are `created_at` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
| `sort` | string | no | Allowed values are `asc` or `desc` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, users.
......@@ -436,6 +438,8 @@ GET /groups/:id/search
| `search` | string | yes | The search query |
| `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. |
| `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. |
| `order_by` | string | no | Allowed values are `created_at` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
| `sort` | string | no | Allowed values are `asc` or `desc` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, users.
......@@ -816,6 +820,8 @@ GET /projects/:id/search
| `ref` | string | no | The name of a repository branch or tag to search on. The project's default branch is used by default. This is only applicable for scopes: commits, blobs, and wiki_blobs. |
| `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. |
| `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. |
| `order_by` | string | no | Allowed values are `created_at` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
| `sort` | string | no | Allowed values are `asc` or `desc` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
Search the expression within the specified scope. Currently these scopes are supported: issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs, users.
......
......@@ -207,6 +207,12 @@ guide on how you can add a new custom validator.
checks if the value of the given parameter is either an `Array`, `None`, or `Any`.
It allows only either of these mentioned values to move forward in the request.
- `EmailOrEmailList`:
The [`EmailOrEmailList` validator](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/api/validations/validators/email_or_email_list.rb)
checks if the value of a string or a list of strings contains only valid
email addresses. It allows only lists with all valid email addresses to move forward in the request.
### Adding a new custom validator
Custom validators are a great way to validate parameters before sending
......
......@@ -258,6 +258,7 @@ Table description links:
| [NGINX](#nginx) | Routes requests to appropriate components, terminates SSL | ✅ | ✅ | ⚙ | ✅ | ⤓ | ❌ | CE & EE |
| [Node Exporter](#node-exporter) | Prometheus endpoint with system metrics | ✅ | N/A | N/A | ✅ | ❌ | ❌ | CE & EE |
| [Outbound email (SMTP)](#outbound-email) | Send email messages to users | ⤓ | ⚙ | ⤓ | ✅ | ⤓ | ⤓ | CE & EE |
| [Patroni](#patroni) | Manage PostgreSQL HA cluster leader selection and replication | ⚙ | ❌ | ❌ | ✅ | ❌ | ❌ | EE Only |
| [PgBouncer Exporter](#pgbouncer-exporter) | Prometheus endpoint with PgBouncer metrics | ⚙ | ❌ | ❌ | ✅ | ❌ | ❌ | CE & EE |
| [PgBouncer](#pgbouncer) | Database connection pooling, failover | ⚙ | ❌ | ❌ | ✅ | ❌ | ❌ | EE Only |
| [PostgreSQL Exporter](#postgresql-exporter) | Prometheus endpoint with PostgreSQL metrics | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | CE & EE |
......@@ -545,6 +546,15 @@ NGINX has an Ingress port for all HTTP requests and routes them to the appropria
[Node Exporter](https://github.com/prometheus/node_exporter) is a Prometheus tool that gives us metrics on the underlying machine (think CPU/Disk/Load). It's just a packaged version of the common open source offering from the Prometheus project.
#### Patroni
- [Project Page](https://github.com/zalando/patroni)
- Configuration:
- [Omnibus](../administration/postgresql/replication_and_failover.md#patroni)
- Layer: Core Service (Data)
- Process: `patroni`
- GitLab.com: [Database Architecture](https://about.gitlab.com/handbook/engineering/infrastructure/production/architecture/#database-architecture)
#### PgBouncer
- [Project page](https://github.com/pgbouncer/pgbouncer/blob/master/README.md)
......
......@@ -633,6 +633,8 @@ To enable this feature:
1. Expand the **Permissions, LFS, 2FA** section, and enter IP address ranges into **Allow access to the following IP addresses** field.
1. Click **Save changes**.
![Domain restriction by IP address](img/restrict-by-ip.gif)
#### Allowed domain restriction **(PREMIUM)**
>- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7297) in [GitLab Premium and Silver](https://about.gitlab.com/pricing/) 12.2.
......@@ -661,6 +663,8 @@ To enable this feature:
1. Expand the **Permissions, LFS, 2FA** section, and enter the domain names into **Restrict membership by email** field.
1. Click **Save changes**.
![Domain restriction by email](img/restrict-by-email.gif)
This will enable the domain-checking for all new users added to the group from this moment on.
#### Group file templates **(PREMIUM)**
......
......@@ -4,156 +4,148 @@ group: Package
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
---
# GitLab NPM Registry
# NPM packages in the Package Registry
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.7.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Core in 13.3.
With the GitLab NPM Registry, every
project can have its own space to store NPM packages.
Publish NPM packages in your project's Package Registry. Then install the
packages whenever you need to use them as a dependency.
![GitLab NPM Registry](img/npm_package_view_v12_5.png)
NOTE: **Note:**
Only [scoped](https://docs.npmjs.com/misc/scope) packages are supported.
## Enabling the NPM Registry
NOTE: **Note:**
This option is available only if your GitLab administrator has
[enabled support for the NPM registry](../../../administration/packages/index.md).
Enabling the NPM registry makes it available for all new projects
by default. To enable it for existing projects, or if you want to disable it:
1. Navigate to your project's **Settings > General > Visibility, project features, permissions**.
1. Find the Packages feature and enable or disable it.
1. Click on **Save changes** for the changes to take effect.
## Build an NPM package
You should then be able to see the **Packages & Registries** section on the left sidebar.
This section covers how to install NPM or Yarn and build a package for your
JavaScript project.
Before proceeding to authenticating with the GitLab NPM Registry, you should
get familiar with the package naming convention.
If you already use NPM and know how to build your own packages, go to
the [next section](#authenticate-to-the-package-registry).
## Getting started
### Install NPM
This section covers how to install NPM (or Yarn) and build a package for your
JavaScript project. This is a quickstart if you are new to NPM packages. If you
are already using NPM and understand how to build your own packages, move on to
the [next section](#authenticating-to-the-gitlab-npm-registry).
Install Node.js and NPM in your local development environment by following
the instructions at [npmjs.com](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
### Installing NPM
Follow the instructions at [npmjs.com](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) to download and install Node.js and
NPM to your local development environment.
Once installation is complete, verify you can use NPM in your terminal by
When installation is complete, verify you can use NPM in your terminal by
running:
```shell
npm --version
```
You should see the NPM version printed in the output:
The NPM version is shown in the output:
```plaintext
6.10.3
```
### Installing Yarn
### Install Yarn
You may want to install and use Yarn as an alternative to NPM. Follow the
instructions at [yarnpkg.com](https://classic.yarnpkg.com/en/docs/install) to install on
your development environment.
As an alternative to NPM, you can install Yarn in your local environment by following the
instructions at [yarnpkg.com](https://classic.yarnpkg.com/en/docs/install).
Once installed, you can verify that Yarn is available with the following command:
When installation is complete, verify you can use Yarn in your terminal by
running:
```shell
yarn --version
```
You should see the version printed like so:
The Yarn version is shown in the output:
```plaintext
1.19.1
```
### Creating a project
### Create a project
Understanding how to create a full JavaScript project is outside the scope of
this guide but you can initialize a new empty package by creating and navigating
to an empty directory and using the following command:
To create a project:
```shell
npm init
```
1. Create an empty directory.
1. Go to the directory and initialize an empty package by running:
Or if you're using Yarn:
```shell
npm init
```
```shell
yarn init
```
Or if you're using Yarn:
```shell
yarn init
```
This takes you through a series of questions to produce a `package.json`
file, which is required for all NPM packages. The most important question is the
package name. NPM packages must [follow the naming convention](#package-naming-convention)
and be scoped to the project or group where the registry exists.
1. Enter responses to the questions. Ensure the **package name** follows
the [naming convention](#package-naming-convention) and is scoped to the
project or group where the registry exists.
Once you have completed the setup, you are now ready to upload your package to
the GitLab registry. To get started, you need to set up authentication and then
configure GitLab as a remote registry.
A `package.json` file is created.
## Authenticating to the GitLab NPM Registry
## Authenticate to the Package Registry
If a project is private or you want to upload an NPM package to GitLab,
you need to provide credentials for authentication. [Personal access tokens](../../profile/personal_access_tokens.md)
and [deploy tokens](../../project/deploy_tokens/index.md)
are preferred, but support is available for [OAuth tokens](../../../api/oauth2.md#resource-owner-password-credentials-flow).
To authenticate to the Package Registry, you must use one of the following:
CAUTION: **Two-factor authentication (2FA) is only supported with personal access tokens:**
If you have 2FA enabled, you need to use a [personal access token](../../profile/personal_access_tokens.md) with OAuth headers with the scope set to `api` or a [deploy token](../../project/deploy_tokens/index.md) with `read_package_registry` or `write_package_registry` scopes. Standard OAuth tokens cannot authenticate to the GitLab NPM Registry.
- A [personal access token](../../../user/profile/personal_access_tokens.md)
(required for two-factor authentication (2FA)), with the scope set to `api`.
- A [deploy token](./../../project/deploy_tokens/index.md), with the scope set to `read_package_registry`, `write_package_registry`, or both.
- It's not recommended, but you can use [OAuth tokens](../../../api/oauth2.md#resource-owner-password-credentials-flow).
Standard OAuth tokens cannot authenticate to the GitLab NPM Registry. You must use a personal access token with OAuth headers.
- A [CI job token](#authenticate-with-a-ci-job-token).
### Authenticating with a personal access token or deploy token
### Authenticate with a personal access token or deploy token
To authenticate with a [personal access token](../../profile/personal_access_tokens.md) or [deploy token](../../project/deploy_tokens/index.md),
set your NPM configuration:
```shell
# Set URL for your scoped packages.
# For example package with name `@foo/bar` will use this URL for download
npm config set @foo:registry https://gitlab.com/api/v4/packages/npm/
# Set URL for your scoped packages
# For example, a package named `@foo/bar` uses this URL for download
npm config set @foo:registry https://gitlab.example.com/api/v4/packages/npm/
# Add the token for the scoped packages URL. This will allow you to download
# `@foo/` packages from private projects.
npm config set '//gitlab.com/api/v4/packages/npm/:_authToken' "<your_token>"
# Add the token for the scoped packages URL
# Use this to download `@foo/` packages from private projects
npm config set '//gitlab.example.com/api/v4/packages/npm/:_authToken' "<your_token>"
# Add token for uploading to the registry. Replace <your_project_id>
# with the project you want your package to be uploaded to.
npm config set '//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "<your_token>"
# Add token for to publish to the package registry
# Replace <your_project_id> with the project you want to publish your package to
npm config set '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "<your_token>"
```
Replace `<your_project_id>` with your project ID which can be found on the home page
of your project and `<your_token>` with your personal access token or deploy token.
- `<your_project_id>` is your project ID, found on the project's home page.
- `<your_token>` is your personal access token or deploy token.
- Replace `gitlab.example.com` with your domain name.
You should now be able to publish and install NPM packages in your project.
If you encounter an error with [Yarn](https://classic.yarnpkg.com/en/), view
[troubleshooting steps](#troubleshooting).
### Authenticate with a CI job token
If you have a self-managed GitLab installation, replace `gitlab.com` with your
domain name.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9104) in GitLab Premium 12.5.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Core in 13.3.
If you're using NPM with GitLab CI/CD, a CI job token can be used instead of a personal access token or deploy token.
The token inherits the permissions of the user that generates the pipeline.
You should now be able to download and upload NPM packages to your project.
Add a corresponding section to your `.npmrc` file:
NOTE: **Note:**
If you encounter an error message with [Yarn](https://classic.yarnpkg.com/en/), see the
[troubleshooting section](#troubleshooting).
```ini
@foo:registry=https://gitlab.example.com/api/v4/packages/npm/
//gitlab.example.com/api/v4/packages/npm/:_authToken=${CI_JOB_TOKEN}
//gitlab.example.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}
```
### Using variables to avoid hard-coding auth token values
#### Use variables to avoid hard-coding auth token values
To avoid hard-coding the `authToken` value, you may use a variables in its place:
To avoid hard-coding the `authToken` value, you may use a variable in its place:
```shell
npm config set '//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "${NPM_TOKEN}"
npm config set '//gitlab.com/api/v4/packages/npm/:_authToken' "${NPM_TOKEN}"
npm config set '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "${NPM_TOKEN}"
npm config set '//gitlab.example.com/api/v4/packages/npm/:_authToken' "${NPM_TOKEN}"
```
Then, you could run `npm publish` either locally or via GitLab CI/CD:
Then, you can run `npm publish` either locally or by using GitLab CI/CD.
- **Locally:** Export `NPM_TOKEN` before publishing:
......@@ -164,174 +156,194 @@ Then, you could run `npm publish` either locally or via GitLab CI/CD:
- **GitLab CI/CD:** Set an `NPM_TOKEN` [variable](../../../ci/variables/README.md)
under your project's **Settings > CI/CD > Variables**.
### Authenticating with a CI job token
## Package naming convention
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9104) in GitLab Premium 12.5.
Your NPM package name must be in the format of `@scope:package-name`.
If you’re using NPM with GitLab CI/CD, a CI job token can be used instead of a personal access token or deploy token.
The token inherits the permissions of the user that generates the pipeline.
- The `@scope` is the root namespace of the GitLab project. It must match exactly, including the case.
- The `package-name` can be whatever you want.
Add a corresponding section to your `.npmrc` file:
For example, if your project is `https://gitlab.example.com/my-org/engineering-group/team-amazing/analytics`,
the root namespace is `my-org`. When you publish a package, it must have `my-org` as the scope.
```ini
@foo:registry=https://gitlab.com/api/v4/packages/npm/
//gitlab.com/api/v4/packages/npm/:_authToken=${CI_JOB_TOKEN}
//gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}
| Project | Package | Supported |
| ---------------------- | ----------------------- | --------- |
| `my-org/bar` | `@my-org/bar` | Yes |
| `my-org/bar/baz` | `@my-org/baz` | Yes |
| `My-org/Bar/baz` | `@My-org/Baz` | Yes |
| `my-org/bar/buz` | `@my-org/anything` | Yes |
| `gitlab-org/gitlab` | `@gitlab-org/gitlab` | Yes |
| `gitlab-org/gitlab` | `@foo/bar` | No |
In GitLab, this regex validates all package names from all package managers:
```plaintext
/\A\@?(([\w\-\.\+]*)\/)*([\w\-\.]+)@?(([\w\-\.\+]*)\/)*([\w\-\.]*)\z/
```
## Uploading packages
This regex allows almost all of the characters that NPM allows, with a few exceptions (for example, `~` is not allowed).
DANGER: **Warning:**
Due to a [bug in NPM](https://github.com/npm/cli/issues/1994), version `7.x` and later do not respect the `publishConfig` entry in the `package.json` file. To publish, you must use an earlier version of NPM, or temporarily set your `.npmrc` scope to `@foo:registry=https://gitlab.example.com/api/v4/projects/<project_id>/packages/npm`.
The regex also allows for capital letters, while NPM does not. Capital letters are needed because the scope must be
identical to the root namespace of the project.
CAUTION: **Caution:**
When you update the path of a user or group, or transfer a subgroup or project,
you must remove any NPM packages first. You cannot update the root namespace
of a project with NPM packages. Make sure you update your `.npmrc` files to follow
the naming convention and run `npm publish` if necessary.
Before you can upload a package, you need to specify the registry
## Publish an NPM package
Before you can publish a package, you must specify the registry
for NPM. To do this, add the following section to the bottom of `package.json`:
```json
"publishConfig": {
"@foo:registry":"https://gitlab.com/api/v4/projects/<your_project_id>/packages/npm/"
"@foo:registry":"https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/"
}
```
Replace `<your_project_id>` with your project ID, which can be found on the home
page of your project, and replace `@foo` with your own scope.
- `<your_project_id>` is your project ID, found on the project's home page.
- `@foo` is your scope.
- Replace `gitlab.example.com` with your domain name.
If you have a self-managed GitLab installation, replace `gitlab.com` with your
domain name.
DANGER: **Warning:**
The `publishConfig` entry in the `package.json` file is not respected, because of a
[bug in NPM](https://github.com/npm/cli/issues/1994) version `7.x` and later. You must
use an earlier version of NPM, or temporarily set your `.npmrc` scope to
`@foo:registry=https://gitlab.example.com/api/v4/projects/<project_id>/packages/npm`.
Once you have enabled it and set up [authentication](#authenticating-to-the-gitlab-npm-registry),
After you have set up [authentication](#authenticate-to-the-package-registry),
you can upload an NPM package to your project:
```shell
npm publish
```
You can then navigate to your project's **Packages & Registries** page and see the uploaded
packages or even delete them.
To view the package, go to your project's **Packages & Registries**.
Attempting to publish a package with a name that already exists within
a given scope causes a `403 Forbidden!` error.
If you try to publish a package [with a name that already exists](#publishing-packages-with-the-same-name-or-version) within
a given scope, you get a `403 Forbidden!` error.
## Uploading a package with the same version twice
## Publish an NPM package by using CI/CD
You cannot upload a package with the same name and version twice, unless you
delete the existing package first. This aligns with npmjs.org's behavior, with
the exception that npmjs.org does not allow users to ever publish the same version
more than once, even if it has been deleted.
## Package naming convention
To work with NPM commands within [GitLab CI/CD](./../../../ci/README.md), you can use
`CI_JOB_TOKEN` in place of the personal access token or deploy token in your commands.
**Packages must be scoped in the root namespace of the project**. The package
name may be anything but it is preferred that the project name be used unless
it is not possible due to a naming collision. For example:
An example `.gitlab-ci.yml` file for publishing NPM packages:
| Project | Package | Supported |
| ---------------------- | ----------------------- | --------- |
| `foo/bar` | `@foo/bar` | Yes |
| `foo/bar/baz` | `@foo/baz` | Yes |
| `foo/bar/buz` | `@foo/anything` | Yes |
| `gitlab-org/gitlab` | `@gitlab-org/gitlab` | Yes |
| `gitlab-org/gitlab` | `@foo/bar` | No |
```yaml
image: node:latest
The regex that is used for naming is validating all package names from all package managers:
stages:
- deploy
```plaintext
/\A\@?(([\w\-\.\+]*)\/)*([\w\-\.]+)@?(([\w\-\.\+]*)\/)*([\w\-\.]*)\z/
deploy:
stage: deploy
script:
- echo "//gitlab.example.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}">.npmrc
- npm publish
```
It allows for capital letters, while NPM does not, and allows for almost all of the
characters NPM allows with a few exceptions (`~` is not allowed).
## Publishing packages with the same name or version
NOTE: **Note:**
Capital letters are needed because the scope is required to be
identical to the top level namespace of the project. So, for example, if your
project path is `My-Group/project-foo`, your package must be named `@My-Group/any-package-name`.
`@my-group/any-package-name` will not work.
You cannot publish a package if a package of the same name and version already exists.
You must delete the existing package first.
CAUTION: **When updating the path of a user/group or transferring a (sub)group/project:**
Make sure to remove any NPM packages first. You cannot update the root namespace of a project with NPM packages. Don't forget to update your `.npmrc` files to follow the above naming convention and run `npm publish` if necessary.
This aligns with npmjs.org's behavior. However, npmjs.org does not ever let you publish
the same version more than once, even if it has been deleted.
Now, you can configure your project to authenticate with the GitLab NPM
Registry.
## Install a package
## Installing a package
NPM packages are commonly-installed by using the `npm` or `yarn` commands
in a JavaScript project.
NPM packages are commonly installed using the `npm` or `yarn` commands
inside a JavaScript project. If you haven't already, set the
URL for scoped packages. You can do this with the following command:
1. Set the URL for scoped packages by running:
```shell
npm config set @foo:registry https://gitlab.com/api/v4/packages/npm/
```
```shell
npm config set @foo:registry https://gitlab.example.com/api/v4/packages/npm/
```
Replace `@foo` with your scope.
Replace `@foo` with your scope.
Next, you need to ensure [authentication](#authenticating-to-the-gitlab-npm-registry)
is setup so you can successfully install the package. Once this has been
completed, you can run the following command inside your project to install a
package:
1. Ensure [authentication](#authenticate-to-the-package-registry) is configured.
1. In your project, to install a package, run:
```shell
npm install @my-project-scope/my-package
```
```shell
npm install @my-project-scope/my-package
```
Or if you're using Yarn:
Or if you're using Yarn:
```shell
yarn add @my-project-scope/my-package
```
### Forwarding requests to npmjs.org
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/55344) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.9.
```shell
yarn add @my-project-scope/my-package
```
By default, when an NPM package is not found in the GitLab NPM Registry, the request is forwarded to [npmjs.com](https://www.npmjs.com/).
In [GitLab 12.9 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/55344),
when an NPM package is not found in the Package Registry, the request is forwarded to [npmjs.com](https://www.npmjs.com/).
Administrators can disable this behavior in the [Continuous Integration settings](../../admin_area/settings/continuous_integration.md).
### Installing packages from other organizations
### Install NPM packages from other organizations
You can route package requests to organizations and users outside of GitLab.
To do this, add lines to your `.npmrc` file, replacing `my-org` with the namespace or group that owns your project's repository. The name is case-sensitive and must match the name of your group or namespace exactly.
To do this, add lines to your `.npmrc` file. Replace `my-org` with the namespace or group that owns your project's repository,
and use your organization's URL. The name is case-sensitive and must match the name of your group or namespace exactly.
```shell
@foo:registry=https://gitlab.example.com/api/v4/packages/npm/
//gitlab.com/api/v4/packages/npm/:_authToken= "<your_token>"
//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken= "<your_token>"
//gitlab.example.com/api/v4/packages/npm/:_authToken= "<your_token>"
//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken= "<your_token>"
@my-other-org:registry=https://gitlab.example.com/api/v4/packages/npm/
//gitlab.com/api/v4/packages/npm/:_authToken= "<your_token>"
//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken= "<your_token>"
//gitlab.example.com/api/v4/packages/npm/:_authToken= "<your_token>"
//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken= "<your_token>"
```
## Removing a package
### NPM dependencies metadata
In the packages view of your project page, you can delete packages by clicking
the red trash icons or by clicking the **Delete** button on the package details
page.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11867) in GitLab Premium 12.6.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Core in 13.3.
## Publishing a package with CI/CD
In GitLab 12.6 and later, packages published to the Package Registry expose the following attributes to the NPM client:
To work with NPM commands within [GitLab CI/CD](./../../../ci/README.md), you can use
`CI_JOB_TOKEN` in place of the personal access token or deploy token in your commands.
- name
- version
- dist-tags
- dependencies
- dependencies
- devDependencies
- bundleDependencies
- peerDependencies
- deprecated
A simple example `.gitlab-ci.yml` file for publishing NPM packages:
## Add NPM distribution tags
```yaml
image: node:latest
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9425) in GitLab Premium 12.8.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Core in 13.3.
stages:
- deploy
You can add [distribution tags](https://docs.npmjs.com/cli/dist-tag) to newly-published packages.
Tags are optional and can be assigned to only one package at a time.
deploy:
stage: deploy
script:
- echo "//gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}">.npmrc
- npm publish
When you publish a package without a tag, the `latest` tag is added by default.
When you install a package without specifying the tag or version, the `latest` tag is used.
Examples of the supported `dist-tag` commands:
```shell
npm publish @scope/package --tag # Publish a package with new tag
npm dist-tag add @scope/package@version my-tag # Add a tag to an existing package
npm dist-tag ls @scope/package # List all tags under the package
npm dist-tag rm @scope/package@version my-tag # Delete a tag from the package
npm install @scope/package@my-tag # Install a specific tag
```
Learn more about [using `CI_JOB_TOKEN` to authenticate to the GitLab NPM registry](#authenticating-with-a-ci-job-token).
You cannot use your `CI_JOB_TOKEN` or deploy token with the `npm dist-tag` commands.
View [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/258835) for details.
Due to a bug in NPM 6.9.0, deleting distribution tags fails. Make sure your NPM version is 6.9.1 or later.
## Troubleshooting
......@@ -347,7 +359,7 @@ info No lockfile found.
warning XXX: No license field
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
error An unexpected error occurred: "https://gitlab.com/api/v4/projects/XXX/packages/npm/XXX/XXX/-/XXX/XXX-X.X.X.tgz: Request failed \"404 Not Found\"".
error An unexpected error occurred: "https://gitlab.example.com/api/v4/projects/XXX/packages/npm/XXX/XXX/-/XXX/XXX-X.X.X.tgz: Request failed \"404 Not Found\"".
info If you think this is a bug, please open a bug report with the information provided in "/Users/XXX/gitlab-migration/module-util/yarn-error.log".
info Visit https://classic.yarnpkg.com/en/docs/cli/install for documentation about this command
```
......@@ -356,14 +368,14 @@ In this case, try adding this to your `.npmrc` file (and replace `<your_token>`
with your personal access token or deploy token):
```plaintext
//gitlab.com/api/v4/projects/:_authToken=<your_token>
//gitlab.example.com/api/v4/projects/:_authToken=<your_token>
```
You can also use `yarn config` instead of `npm config` when setting your auth-token dynamically:
```shell
yarn config set '//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "<your_token>"
yarn config set '//gitlab.com/api/v4/packages/npm/:_authToken' "<your_token>"
yarn config set '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "<your_token>"
yarn config set '//gitlab.example.com/api/v4/packages/npm/:_authToken' "<your_token>"
```
### `npm publish` targets default NPM registry (`registry.npmjs.org`)
......@@ -379,7 +391,7 @@ should look like:
"version": "1.0.0",
"description": "Example package for GitLab NPM registry",
"publishConfig": {
"@foo:registry":"https://gitlab.com/api/v4/projects/<your_project_id>/packages/npm/"
"@foo:registry":"https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/"
}
}
```
......@@ -387,14 +399,14 @@ should look like:
And the `.npmrc` file should look like:
```ini
//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken=<your_token>
//gitlab.com/api/v4/packages/npm/:_authToken=<your_token>
@foo:registry=https://gitlab.com/api/v4/packages/npm/
//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken=<your_token>
//gitlab.example.com/api/v4/packages/npm/:_authToken=<your_token>
@foo:registry=https://gitlab.example.com/api/v4/packages/npm/
```
### `npm install` returns `Error: Failed to replace env in config: ${NPM_TOKEN}`
You do not need a token to run `npm install` unless your project is private (the token is only required to publish). If the `.npmrc` file was checked in with a reference to `$NPM_TOKEN`, you can remove it. If you prefer to leave the reference in, you need to set a value prior to running `npm install` or set the value using [GitLab environment variables](./../../../ci/variables/README.md):
You do not need a token to run `npm install` unless your project is private. The token is only required to publish. If the `.npmrc` file was checked in with a reference to `$NPM_TOKEN`, you can remove it. If you prefer to leave the reference in, you must set a value prior to running `npm install` or set the value by using [GitLab environment variables](./../../../ci/variables/README.md):
```shell
NPM_TOKEN=<your_token> npm install
......@@ -402,50 +414,11 @@ NPM_TOKEN=<your_token> npm install
### `npm install` returns `npm ERR! 403 Forbidden`
- Check that your token is not expired and has appropriate permissions.
- Check that [your token does not begin with `-`](https://gitlab.com/gitlab-org/gitlab/-/issues/235473).
- Check if you have attempted to publish a package with a name that already exists within a given scope.
- Ensure the scoped packages URL includes a trailing slash:
- Correct: `//gitlab.com/api/v4/packages/npm/`
- Incorrect: `//gitlab.com/api/v4/packages/npm`
## NPM dependencies metadata
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11867) in GitLab Premium 12.6.
Starting from GitLab 12.6, new packages published to the GitLab NPM Registry expose the following attributes to the NPM client:
- name
- version
- dist-tags
- dependencies
- dependencies
- devDependencies
- bundleDependencies
- peerDependencies
- deprecated
## NPM distribution tags
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9425) in GitLab Premium 12.8.
You can add [distribution tags](https://docs.npmjs.com/cli/dist-tag) for newly published packages.
They follow NPM's convention where they are optional, and each tag can only be assigned to one
package at a time. The `latest` tag is added by default when a package is published without a tag.
The same applies to installing a package without specifying the tag or version.
Examples of the supported `dist-tag` commands and using tags in general:
```shell
npm publish @scope/package --tag # Publish new package with new tag
npm dist-tag add @scope/package@version my-tag # Add a tag to an existing package
npm dist-tag ls @scope/package # List all tags under the package
npm dist-tag rm @scope/package@version my-tag # Delete a tag from the package
npm install @scope/package@my-tag # Install a specific tag
```
NOTE: **Note:**
You cannot use your `CI_JOB_TOKEN` or deploy token with the `npm dist-tag` commands. View [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/258835) for details.
If you get this error, ensure that:
CAUTION: **Warning:**
Due to a bug in NPM 6.9.0, deleting dist tags fails. Make sure your NPM version is greater than 6.9.1.
- Your token is not expired and has appropriate permissions.
- [Your token does not begin with `-`](https://gitlab.com/gitlab-org/gitlab/-/issues/235473).
- A package with the same name doesn't already exist within the given scope.
- The scoped packages URL includes a trailing slash:
- Correct: `//gitlab.example.com/api/v4/packages/npm/`
- Incorrect: `//gitlab.example.com/api/v4/packages/npm`
......@@ -31,7 +31,7 @@ authenticate with GitLab by using the `CI_JOB_TOKEN`.
CI/CD templates, which you can use to get started, are in [this repo](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates).
Learn more about [using CI/CD to build Maven packages](../maven_repository/index.md#create-maven-packages-with-gitlab-cicd), [NPM packages](../npm_registry/index.md#publishing-a-package-with-cicd), [Composer packages](../composer_repository/index.md#publish-a-composer-package-by-using-cicd), [NuGet Packages](../nuget_repository/index.md#publishing-a-nuget-package-with-cicd), [Conan Packages](../conan_repository/index.md#publish-a-conan-package-by-using-cicd), [PyPI packages](../pypi_repository/index.md#using-gitlab-ci-with-pypi-packages), and [generic packages](../generic_packages/index.md#publish-a-generic-package-by-using-cicd).
Learn more about [using CI/CD to build Maven packages](../maven_repository/index.md#create-maven-packages-with-gitlab-cicd), [NPM packages](../npm_registry/index.md#publish-an-npm-package-by-using-cicd), [Composer packages](../composer_repository/index.md#publish-a-composer-package-by-using-cicd), [NuGet Packages](../nuget_repository/index.md#publishing-a-nuget-package-with-cicd), [Conan Packages](../conan_repository/index.md#publish-a-conan-package-by-using-cicd), [PyPI packages](../pypi_repository/index.md#using-gitlab-ci-with-pypi-packages), and [generic packages](../generic_packages/index.md#publish-a-generic-package-by-using-cicd).
If you use CI/CD to build a package, extended activity
information is displayed when you view the package details:
......
......@@ -67,9 +67,9 @@ If you are using NPM, this involves creating an `.npmrc` file and adding the app
to your project using your project ID, then adding a section to your `package.json` file with a similar URL.
Follow
the instructions in the [GitLab NPM Registry documentation](../npm_registry/index.md#authenticating-to-the-gitlab-npm-registry). After
the instructions in the [GitLab NPM Registry documentation](../npm_registry/index.md#authenticate-to-the-package-registry). After
you do this, you can push your NPM package to your project using `npm publish`, as described in the
[uploading packages](../npm_registry/index.md#uploading-packages) section of the docs.
[publishing packages](../npm_registry/index.md#publish-an-npm-package) section of the docs.
#### Maven
......
......@@ -9,7 +9,7 @@ export default {
i18n: {
editPermissions: s__('Members|Edit permissions'),
modalBody: s__(
'Members|%{userName} is currently a LDAP user. Editing their permissions will override the settings from the LDAP group sync.',
'Members|%{userName} is currently an LDAP user. Editing their permissions will override the settings from the LDAP group sync.',
),
toastMessage: s__('Members|LDAP override enabled.'),
},
......
......@@ -75,7 +75,7 @@ module Security
def normalize_report_findings(report_findings, vulnerabilities)
report_findings.map do |report_finding|
finding_hash = report_finding.to_hash
.except(:compare_key, :identifiers, :location, :scanner)
.except(:compare_key, :identifiers, :location, :scanner, :links)
finding = Vulnerabilities::Finding.new(finding_hash)
# assigning Vulnerabilities to Findings to enable the computed state
......@@ -84,6 +84,9 @@ module Security
finding.project = pipeline.project
finding.sha = pipeline.sha
finding.build_scanner(report_finding.scanner&.to_hash)
finding.finding_links = report_finding.links.map do |link|
Vulnerabilities::FindingLink.new(link.to_hash)
end
finding.identifiers = report_finding.identifiers.map do |identifier|
Vulnerabilities::Identifier.new(identifier.to_hash)
end
......
......@@ -26,6 +26,8 @@ module Vulnerabilities
has_many :finding_identifiers, class_name: 'Vulnerabilities::FindingIdentifier', inverse_of: :finding, foreign_key: 'occurrence_id'
has_many :identifiers, through: :finding_identifiers, class_name: 'Vulnerabilities::Identifier'
has_many :finding_links, class_name: 'Vulnerabilities::FindingLink', inverse_of: :finding, foreign_key: 'vulnerability_occurrence_id'
has_many :finding_pipelines, class_name: 'Vulnerabilities::FindingPipeline', inverse_of: :finding, foreign_key: 'occurrence_id'
has_many :pipelines, through: :finding_pipelines, class_name: 'Ci::Pipeline'
......@@ -256,7 +258,9 @@ module Vulnerabilities
end
def links
metadata.fetch('links', [])
return metadata.fetch('links', []) if finding_links.load.empty?
finding_links.as_json(only: [:name, :url])
end
def remediations
......
# frozen_string_literal: true
module Vulnerabilities
class FindingLink < ApplicationRecord
self.table_name = 'vulnerability_finding_links'
belongs_to :finding, class_name: 'Vulnerabilities::Finding', inverse_of: :finding_identifiers, foreign_key: 'vulnerability_occurrence_id'
validates :finding, presence: true
validates :url, presence: true, length: { maximum: 255 }
validates :name, length: { maximum: 2048 }
end
end
......@@ -16,6 +16,7 @@ module EE
params[:search],
elastic_projects,
public_and_internal_projects: elastic_global,
order_by: params[:order_by],
sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] }
)
......
......@@ -30,6 +30,7 @@ module EE
elastic_projects,
group: group,
public_and_internal_projects: elastic_global,
order_by: params[:order_by],
sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] }
)
......
......@@ -15,6 +15,7 @@ module EE
params[:search],
project: project,
repository_ref: repository_ref,
order_by: params[:order_by],
sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] }
)
......
......@@ -47,7 +47,7 @@ module Security
return
end
vulnerability_params = finding.to_hash.except(:compare_key, :identifiers, :location, :scanner, :scan)
vulnerability_params = finding.to_hash.except(:compare_key, :identifiers, :location, :scanner, :scan, :links)
vulnerability_finding = create_or_find_vulnerability_finding(finding, vulnerability_params)
update_vulnerability_scanner(finding)
......@@ -60,6 +60,8 @@ module Security
create_or_update_vulnerability_identifier_object(vulnerability_finding, identifier)
end
create_or_update_vulnerability_links(finding, vulnerability_finding)
create_vulnerability_pipeline_object(vulnerability_finding, pipeline)
create_vulnerability(vulnerability_finding, pipeline)
......@@ -125,6 +127,15 @@ module Security
rescue ActiveRecord::RecordNotUnique
end
def create_or_update_vulnerability_links(finding, vulnerability_finding)
return if finding.links.blank?
finding.links.each do |link|
vulnerability_finding.finding_links.safe_find_or_create_by!(link.to_hash)
end
rescue ActiveRecord::RecordNotUnique
end
def create_vulnerability_pipeline_object(vulnerability_finding, pipeline)
vulnerability_finding.finding_pipelines.find_or_create_by!(pipeline: pipeline)
rescue ActiveRecord::RecordNotUnique
......
---
title: Add Vulnerabilities::FindingLink model
merge_request: 46555
author:
type: added
......@@ -137,14 +137,16 @@ module Elastic
end
def apply_sort(query_hash, options)
case options[:sort]
when 'created_asc'
# Due to different uses of sort param we prefer order_by when
# present
case ::Gitlab::Search::SortOptions.sort_and_direction(options[:order_by], options[:sort])
when :created_at_asc
query_hash.merge(sort: {
created_at: {
order: 'asc'
}
})
when 'created_desc'
when :created_at_desc
query_hash.merge(sort: {
created_at: {
order: 'desc'
......
......@@ -55,6 +55,7 @@ module Gitlab
def create_vulnerability(report, data, version)
identifiers = create_identifiers(report, data['identifiers'])
links = create_links(report, data['links'])
report.add_finding(
::Gitlab::Ci::Reports::Security::Finding.new(
uuid: SecureRandom.uuid,
......@@ -67,6 +68,7 @@ module Gitlab
scanner: create_scanner(report, data['scanner']),
scan: report&.scan,
identifiers: identifiers,
links: links,
raw_metadata: data.to_json,
metadata_version: version))
end
......@@ -106,6 +108,22 @@ module Gitlab
url: identifier['url']))
end
def create_links(report, links)
return [] unless links.is_a?(Array)
links
.map { |link| create_link(report, link) }
.compact
end
def create_link(report, link)
return unless link.is_a?(Hash)
::Gitlab::Ci::Reports::Security::Link.new(
name: link['name'],
url: link['url'])
end
def parse_severity_level(input)
return input if ::Vulnerabilities::Finding::SEVERITY_LEVELS.key?(input)
......
......@@ -10,6 +10,7 @@ module Gitlab
attr_reader :compare_key
attr_reader :confidence
attr_reader :identifiers
attr_reader :links
attr_reader :location
attr_reader :metadata_version
attr_reader :name
......@@ -24,10 +25,11 @@ module Gitlab
delegate :file_path, :start_line, :end_line, to: :location
def initialize(compare_key:, identifiers:, location:, metadata_version:, name:, raw_metadata:, report_type:, scanner:, scan:, uuid:, confidence: nil, severity: nil) # rubocop:disable Metrics/ParameterLists
def initialize(compare_key:, identifiers:, links: [], location:, metadata_version:, name:, raw_metadata:, report_type:, scanner:, scan:, uuid:, confidence: nil, severity: nil) # rubocop:disable Metrics/ParameterLists
@compare_key = compare_key
@confidence = confidence
@identifiers = identifiers
@links = links
@location = location
@metadata_version = metadata_version
@name = name
......@@ -46,6 +48,7 @@ module Gitlab
compare_key
confidence
identifiers
links
location
metadata_version
name
......
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module Security
class Link
attr_accessor :name, :url
def initialize(name: nil, url: nil)
@name = name
@url = url
end
def to_hash
{
name: name,
url: url
}.compact
end
end
end
end
end
end
......@@ -8,13 +8,15 @@ module Gitlab
class GroupSearchResults < Gitlab::Elastic::SearchResults
attr_reader :group, :default_project_filter, :filters
def initialize(current_user, query, limit_project_ids = nil, group:, public_and_internal_projects: false, default_project_filter: false, sort: nil, filters: {})
# rubocop:disable Metrics/ParameterLists
def initialize(current_user, query, limit_project_ids = nil, group:, public_and_internal_projects: false, default_project_filter: false, order_by: nil, sort: nil, filters: {})
@group = group
@default_project_filter = default_project_filter
@filters = filters
super(current_user, query, limit_project_ids, public_and_internal_projects: public_and_internal_projects, sort: sort, filters: filters)
super(current_user, query, limit_project_ids, public_and_internal_projects: public_and_internal_projects, order_by: order_by, sort: sort, filters: filters)
end
# rubocop:enable Metrics/ParameterLists
end
end
end
......@@ -8,11 +8,11 @@ module Gitlab
class ProjectSearchResults < Gitlab::Elastic::SearchResults
attr_reader :project, :repository_ref, :filters
def initialize(current_user, query, project:, repository_ref: nil, sort: nil, filters: {})
def initialize(current_user, query, project:, repository_ref: nil, order_by: nil, sort: nil, filters: {})
@project = project
@repository_ref = repository_ref.presence || project.default_branch
super(current_user, query, [project.id], public_and_internal_projects: false, sort: sort, filters: filters)
super(current_user, query, [project.id], public_and_internal_projects: false, order_by: order_by, sort: sort, filters: filters)
end
private
......
......@@ -7,17 +7,18 @@ module Gitlab
DEFAULT_PER_PAGE = Gitlab::SearchResults::DEFAULT_PER_PAGE
attr_reader :current_user, :query, :public_and_internal_projects, :sort, :filters
attr_reader :current_user, :query, :public_and_internal_projects, :order_by, :sort, :filters
# Limit search results by passed projects
# It allows us to search only for projects user has access to
attr_reader :limit_project_ids
def initialize(current_user, query, limit_project_ids = nil, public_and_internal_projects: true, sort: nil, filters: {})
def initialize(current_user, query, limit_project_ids = nil, public_and_internal_projects: true, order_by: nil, sort: nil, filters: {})
@current_user = current_user
@query = query
@limit_project_ids = limit_project_ids
@public_and_internal_projects = public_and_internal_projects
@order_by = order_by
@sort = sort
@filters = filters
end
......@@ -202,6 +203,7 @@ module Gitlab
current_user: current_user,
project_ids: limit_project_ids,
public_and_internal_projects: public_and_internal_projects,
order_by: order_by,
sort: sort
}
end
......@@ -214,7 +216,7 @@ module Gitlab
def issues
strong_memoize(:issues) do
options = base_options.merge(filters.slice(:sort, :confidential, :state))
options = base_options.merge(filters.slice(:order_by, :sort, :confidential, :state))
Issue.elastic_search(query, options: options)
end
......@@ -235,7 +237,7 @@ module Gitlab
def merge_requests
strong_memoize(:merge_requests) do
options = base_options.merge(filters.slice(:sort, :state))
options = base_options.merge(filters.slice(:order_by, :sort, :state))
MergeRequest.elastic_search(query, options: options)
end
......
# frozen_string_literal: true
FactoryBot.define do
factory :ci_reports_security_link, class: '::Gitlab::Ci::Reports::Security::Link' do
name { 'CVE-2020-0202' }
url { 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-0202' }
skip_create
initialize_with do
::Gitlab::Ci::Reports::Security::Link.new(**attributes)
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :finding_link, class: 'Vulnerabilities::FindingLink' do
finding factory: :vulnerabilities_finding
name { 'CVE-2018-1234' }
url { 'http://cve.mitre.org/cgi-bin/cvename.cgi?name=2018-1234' }
end
end
......@@ -3,13 +3,13 @@
require 'spec_helper'
RSpec.describe 'Groups > Audit Events', :js do
include Spec::Support::Helpers::Features::MembersHelpers
let(:user) { create(:user) }
let(:alex) { create(:user, name: 'Alex') }
let(:group) { create(:group) }
before do
stub_feature_flags(vue_group_members_list: false)
group.add_owner(user)
group.add_developer(alex)
sign_in(user)
......@@ -47,11 +47,9 @@ RSpec.describe 'Groups > Audit Events', :js do
wait_for_requests
group_member = group.members.find_by(user_id: alex)
page.within "#group_member_#{group_member.id}" do
page.within first_row do
click_button 'Developer'
click_link 'Maintainer'
click_button 'Maintainer'
end
find(:link, text: 'Settings').click
......
......@@ -2,14 +2,12 @@
require 'spec_helper'
RSpec.describe 'Groups > Members > List members' do
include Spec::Support::Helpers::Features::MembersHelpers
let(:user1) { create(:user, name: 'John Doe') }
let(:user2) { create(:user, name: 'Mary Jane') }
let(:group) { create(:group) }
before do
stub_feature_flags(vue_group_members_list: false)
end
context 'with Group SAML identity linked for a user' do
let(:saml_provider) { create(:saml_provider) }
let(:group) { saml_provider.group }
......@@ -23,12 +21,10 @@ RSpec.describe 'Groups > Members > List members' do
extern_uid: 'user2@example.com')
end
it 'shows user with SSO status badge' do
it 'shows user with SSO status badge', :js do
visit group_group_members_path(group)
member = GroupMember.find_by(user: user2, group: group)
expect(find("#group_member_#{member.id}").find('.badge-info')).to have_content('SAML')
expect(second_row).to have_content('SAML')
end
end
......@@ -40,12 +36,10 @@ RSpec.describe 'Groups > Members > List members' do
managed_group.add_guest(managed_user)
end
it 'shows user with "Managed Account" badge' do
it 'shows user with "Managed Account" badge', :js do
visit group_group_members_path(managed_group)
member = GroupMember.find_by(user: managed_user, group: managed_group)
expect(page).to have_selector("#group_member_#{member.id} .badge-info", text: 'Managed Account')
expect(first_row).to have_content('Managed Account')
end
end
......
......@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access levels' do
include WaitForRequests
include Spec::Support::Helpers::Features::MembersHelpers
let(:johndoe) { create(:user, name: 'John Doe') }
let(:maryjane) { create(:user, name: 'Mary Jane') }
......@@ -16,8 +17,6 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev
let!(:regular_member) { create(:group_member, :guest, group: group, user: maryjane, ldap: false) }
before do
stub_feature_flags(vue_group_members_list: false)
# We need to actually activate the LDAP config otherwise `Group#ldap_synced?` will always be false!
allow(Gitlab.config.ldap).to receive_messages(enabled: true)
......@@ -35,7 +34,7 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev
visit group_group_members_path(group)
within "#group_member_#{ldap_member.id}" do
within first_row do
expect(page).not_to have_content 'LDAP'
expect(page).not_to have_button 'Guest'
expect(page).not_to have_button 'Edit permissions'
......@@ -47,7 +46,7 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev
visit group_group_members_path(group)
within "#group_member_#{ldap_member.id}" do
within first_row do
expect(page).to have_content 'LDAP'
expect(page).to have_button 'Guest', disabled: true
expect(page).to have_button 'Edit permissions'
......@@ -55,29 +54,26 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev
click_button 'Edit permissions'
end
expect(page).to have_content ldap_override_message
click_button 'Change permissions'
page.within('[role="dialog"]') do
expect(page).to have_content ldap_override_message
click_button 'Edit permissions'
end
expect(page).not_to have_content ldap_override_message
expect(page).not_to have_button 'Change permissions'
within "#group_member_#{ldap_member.id}" do
within first_row do
expect(page).not_to have_button 'Edit permissions'
expect(page).to have_button 'Guest', disabled: false
end
refresh # controls should still be enabled after a refresh
within "#group_member_#{ldap_member.id}" do
within first_row do
expect(page).not_to have_button 'Edit permissions'
expect(page).to have_button 'Guest', disabled: false
click_button 'Guest'
within '.dropdown-menu' do
click_link 'Revert to LDAP group sync settings'
end
click_button 'Revert to LDAP group sync settings'
wait_for_requests
......@@ -85,16 +81,14 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev
expect(page).to have_button 'Edit permissions'
end
within "#group_member_#{regular_member.id}" do
within third_row do
expect(page).not_to have_content 'LDAP'
expect(page).not_to have_button 'Edit permissions'
expect(page).to have_button 'Guest', disabled: false
click_button 'Guest'
within '.dropdown-menu' do
expect(page).not_to have_content 'Revert to LDAP group sync settings'
end
expect(page).not_to have_content 'Revert to LDAP group sync settings'
end
end
end
......@@ -16,7 +16,7 @@
"identifiers": [],
"links": [
{
"url": ""
"url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020"
}
]
},
......@@ -37,7 +37,8 @@
"identifiers": [],
"links": [
{
"url": ""
"name": "CVE-1030",
"url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030"
}
]
},
......@@ -56,12 +57,6 @@
"location": {},
"identifiers": [],
"links": [
{
"url": ""
},
{
"url": ""
}
]
}
],
......@@ -122,4 +117,4 @@
"end_time": "placeholder-value",
"status": "success"
}
}
\ No newline at end of file
}
......@@ -81,7 +81,7 @@ describe('LdapOverrideConfirmationModal', () => {
it('displays modal body', () => {
expect(
getByText(
`${member.user.name} is currently a LDAP user. Editing their permissions will override the settings from the LDAP group sync.`,
`${member.user.name} is currently an LDAP user. Editing their permissions will override the settings from the LDAP group sync.`,
).exists(),
).toBe(true);
});
......
......@@ -16,6 +16,8 @@ RSpec.describe Mutations::DastOnDemandScans::Create do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -33,52 +35,6 @@ RSpec.describe Mutations::DastOnDemandScans::Create do
end
end
context 'when the user is not associated with the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is an owner' do
it 'has no errors' do
group.add_owner(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a maintainer' do
it 'has no errors' do
project.add_maintainer(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a developer' do
it 'has no errors' do
project.add_developer(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a reporter' do
it 'raises an exception' do
project.add_reporter(user)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is a guest' do
it 'raises an exception' do
project.add_guest(user)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user can run a dast scan' do
before do
project.add_developer(user)
......@@ -152,14 +108,6 @@ RSpec.describe Mutations::DastOnDemandScans::Create do
end
end
end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end
end
......
......@@ -16,6 +16,8 @@ RSpec.describe Mutations::DastScannerProfiles::Create do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -35,12 +37,6 @@ RSpec.describe Mutations::DastScannerProfiles::Create do
end
end
context 'when the user is not associated with the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user can run a dast scan' do
before do
group.add_owner(user)
......@@ -83,14 +79,6 @@ RSpec.describe Mutations::DastScannerProfiles::Create do
expect(response[:errors]).to include('Name has already been taken')
end
end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end
end
......@@ -15,6 +15,8 @@ RSpec.describe Mutations::DastScannerProfiles::Delete do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -54,14 +56,6 @@ RSpec.describe Mutations::DastScannerProfiles::Delete do
end
end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when deletion fails' do
it 'returns an error' do
allow_next_instance_of(::DastScannerProfiles::DestroyService) do |service|
......
......@@ -22,6 +22,8 @@ RSpec.describe Mutations::DastScannerProfiles::Update do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -47,20 +49,6 @@ RSpec.describe Mutations::DastScannerProfiles::Update do
end
end
context 'when the user is not associated with the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when user can not run a DAST scan' do
it 'raises an exception' do
project.add_guest(user)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user can run a DAST scan' do
before do
project.add_developer(user)
......@@ -108,14 +96,6 @@ RSpec.describe Mutations::DastScannerProfiles::Update do
expect(subject[:errors]).to include('Scanner profile not found for given parameters')
end
end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end
end
......
......@@ -17,6 +17,8 @@ RSpec.describe Mutations::DastSiteProfiles::Create do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -35,28 +37,6 @@ RSpec.describe Mutations::DastSiteProfiles::Create do
end
end
context 'when the user is not associated with the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is an owner' do
it 'returns the dast_site_profile id' do
group.add_owner(user)
expect(subject[:id]).to eq(dast_site_profile.to_global_id)
end
end
context 'when the user is a maintainer' do
it 'returns the dast_site_profile id' do
project.add_maintainer(user)
expect(subject[:id]).to eq(dast_site_profile.to_global_id)
end
end
context 'when the user can run a dast scan' do
before do
project.add_developer(user)
......@@ -89,14 +69,6 @@ RSpec.describe Mutations::DastSiteProfiles::Create do
expect(response[:errors]).to include('Name has already been taken')
end
end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end
end
......
......@@ -15,6 +15,8 @@ RSpec.describe Mutations::DastSiteProfiles::Delete do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -32,52 +34,6 @@ RSpec.describe Mutations::DastSiteProfiles::Delete do
end
end
context 'when the user is not associated with the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is an owner' do
it 'has no errors' do
group.add_owner(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a maintainer' do
it 'has no errors' do
project.add_maintainer(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a developer' do
it 'has no errors' do
project.add_developer(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a reporter' do
it 'raises an exception' do
project.add_reporter(user)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is a guest' do
it 'raises an exception' do
project.add_guest(user)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user can run a dast scan' do
before do
project.add_developer(user)
......@@ -96,14 +52,6 @@ RSpec.describe Mutations::DastSiteProfiles::Delete do
expect(subject[:errors]).to include('Name is weird')
end
end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end
end
......
......@@ -18,6 +18,8 @@ RSpec.describe Mutations::DastSiteProfiles::Update do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -37,52 +39,6 @@ RSpec.describe Mutations::DastSiteProfiles::Update do
end
end
context 'when the user is not associated with the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is an owner' do
it 'has no errors' do
group.add_owner(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a maintainer' do
it 'has no errors' do
project.add_maintainer(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a developer' do
it 'has no errors' do
project.add_developer(user)
expect(subject[:errors]).to be_empty
end
end
context 'when the user is a reporter' do
it 'raises an exception' do
project.add_reporter(user)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is a guest' do
it 'raises an exception' do
project.add_guest(user)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user can run a dast scan' do
before do
project.add_developer(user)
......@@ -96,14 +52,6 @@ RSpec.describe Mutations::DastSiteProfiles::Update do
expect(dast_site_profile.dast_site.url).to eq(new_target_url)
end
end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end
end
......
......@@ -18,6 +18,8 @@ RSpec.describe Mutations::DastSiteTokens::Create do
allow(SecureRandom).to receive(:uuid).and_return(uuid)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -35,28 +37,6 @@ RSpec.describe Mutations::DastSiteTokens::Create do
end
end
context 'when the user is not associated with the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is an owner' do
it 'returns the dast_site_token id' do
group.add_owner(user)
expect(subject[:id]).to eq(dast_site_token.to_global_id)
end
end
context 'when the user is a maintainer' do
it 'returns the dast_site_token id' do
project.add_maintainer(user)
expect(subject[:id]).to eq(dast_site_token.to_global_id)
end
end
context 'when the user can run a dast scan' do
before do
project.add_developer(user)
......@@ -94,14 +74,6 @@ RSpec.describe Mutations::DastSiteTokens::Create do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end
end
......
......@@ -17,6 +17,8 @@ RSpec.describe Mutations::DastSiteValidations::Create do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject do
mutation.resolve(
......@@ -36,28 +38,6 @@ RSpec.describe Mutations::DastSiteValidations::Create do
end
end
context 'when the user is not associated with the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is an owner' do
it 'returns the dast_site_validation id' do
group.add_owner(user)
expect(subject[:id]).to eq(dast_site_validation.to_global_id)
end
end
context 'when the user is a maintainer' do
it 'returns the dast_site_validation id' do
project.add_maintainer(user)
expect(subject[:id]).to eq(dast_site_validation.to_global_id)
end
end
context 'when the user can run a dast scan' do
before do
project.add_developer(user)
......@@ -78,14 +58,6 @@ RSpec.describe Mutations::DastSiteValidations::Create do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end
end
......
......@@ -78,5 +78,16 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
expect(empty_report.scan).to be(nil)
end
end
context 'parsing links' do
it 'returns links object for each finding', :aggregate_failures do
links = report.findings.flat_map(&:links)
expect(links.map(&:url)).to match_array(['https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020', 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030'])
expect(links.map(&:name)).to match_array([nil, 'CVE-1030'])
expect(links.size).to eq(2)
expect(links.first).to be_a(::Gitlab::Ci::Reports::Security::Link)
end
end
end
end
......@@ -10,6 +10,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
let(:primary_identifier) { create(:ci_reports_security_identifier) }
let(:other_identifier) { create(:ci_reports_security_identifier) }
let(:link) { create(:ci_reports_security_link) }
let(:scanner) { create(:ci_reports_security_scanner) }
let(:location) { create(:ci_reports_security_locations_sast) }
......@@ -18,6 +19,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
compare_key: 'this_is_supposed_to_be_a_unique_value',
confidence: :medium,
identifiers: [primary_identifier, other_identifier],
links: [link],
location: location,
metadata_version: 'sast:1.0',
name: 'Cipher with no integrity',
......@@ -39,6 +41,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
confidence: :medium,
project_fingerprint: '9a73f32d58d87d94e3dc61c4c1a94803f6014258',
identifiers: [primary_identifier, other_identifier],
links: [link],
location: location,
metadata_version: 'sast:1.0',
name: 'Cipher with no integrity',
......@@ -84,6 +87,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
compare_key: occurrence.compare_key,
confidence: occurrence.confidence,
identifiers: occurrence.identifiers,
links: occurrence.links,
location: occurrence.location,
metadata_version: occurrence.metadata_version,
name: occurrence.name,
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::Security::Link do
subject(:security_link) { described_class.new(name: 'CVE-2020-0202', url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-0202') }
describe '#initialize' do
context 'when all params are given' do
it 'initializes an instance' do
expect { subject }.not_to raise_error
expect(subject).to have_attributes(
name: 'CVE-2020-0202',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-0202'
)
end
end
describe '#to_hash' do
it 'returns expected hash' do
expect(security_link.to_hash).to eq(
{
name: 'CVE-2020-0202',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-0202'
}
)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Vulnerabilities::FindingLink do
describe 'associations' do
it { is_expected.to belong_to(:finding).class_name('Vulnerabilities::Finding') }
end
describe 'validations' do
let_it_be(:link) { create(:finding_link) }
it { is_expected.to validate_presence_of(:url) }
it { is_expected.to validate_length_of(:url).is_at_most(255) }
it { is_expected.to validate_length_of(:name).is_at_most(2048) }
it { is_expected.to validate_presence_of(:finding) }
end
end
......@@ -16,6 +16,7 @@ RSpec.describe Vulnerabilities::Finding do
it { is_expected.to have_many(:finding_pipelines).class_name('Vulnerabilities::FindingPipeline').with_foreign_key('occurrence_id') }
it { is_expected.to have_many(:identifiers).class_name('Vulnerabilities::Identifier') }
it { is_expected.to have_many(:finding_identifiers).class_name('Vulnerabilities::FindingIdentifier').with_foreign_key('occurrence_id') }
it { is_expected.to have_many(:finding_links).class_name('Vulnerabilities::FindingLink').with_foreign_key('vulnerability_occurrence_id') }
end
describe 'validations' do
......@@ -405,6 +406,33 @@ RSpec.describe Vulnerabilities::Finding do
end
end
describe '#links' do
let_it_be(:finding, reload: true) do
create(
:vulnerabilities_finding,
raw_metadata: {
links: [{ url: 'https://raw.gitlab.com', name: 'raw_metadata_link' }]
}.to_json
)
end
subject(:links) { finding.links }
context 'when there are no finding links' do
it 'returns links from raw_metadata' do
expect(links).to eq([{ 'url' => 'https://raw.gitlab.com', 'name' => 'raw_metadata_link' }])
end
end
context 'when there are finding links assigned to given finding' do
let_it_be(:finding_link) { create(:finding_link, name: 'finding_link', url: 'https://link.gitlab.com', finding: finding) }
it 'returns links from finding link' do
expect(links).to eq([{ 'url' => 'https://link.gitlab.com', 'name' => 'finding_link' }])
end
end
end
describe 'feedback' do
let_it_be(:project) { create(:project) }
let(:finding) do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DastScannerProfilePolicy do
it_behaves_like 'a dast on-demand scan policy' do
let_it_be(:record) { create(:dast_scanner_profile, project: project) }
end
end
......@@ -3,43 +3,7 @@
require 'spec_helper'
RSpec.describe DastSiteProfilePolicy do
describe 'create_on_demand_dast_scan' do
let(:dast_site_profile) { create(:dast_site_profile) }
let(:project) { dast_site_profile.project }
let(:user) { create(:user) }
subject { described_class.new(user, dast_site_profile) }
before do
stub_licensed_features(security_on_demand_scans: true)
end
context 'when a user does not have access to the project' do
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
context 'when a user does not have access to dast_site_profiles' do
before do
project.add_guest(user)
end
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
context 'when a user has access dast_site_profiles' do
before do
project.add_developer(user)
end
it { is_expected.to be_allowed(:create_on_demand_dast_scan) }
context 'when on demand scan licensed feature is not available' do
before do
stub_licensed_features(security_on_demand_scans: false)
end
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
end
it_behaves_like 'a dast on-demand scan policy' do
let_it_be(:record) { create(:dast_site_profile, project: project) }
end
end
......@@ -3,43 +3,7 @@
require 'spec_helper'
RSpec.describe DastSiteValidationPolicy do
describe 'create_on_demand_dast_scan' do
let_it_be(:dast_site_validation, reload: true) { create(:dast_site_validation) }
let_it_be(:project) { dast_site_validation.dast_site_token.project }
let_it_be(:user) { create(:user) }
subject { described_class.new(user, dast_site_validation) }
before do
stub_licensed_features(security_on_demand_scans: true)
end
context 'when a user does not have access to the project' do
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
context 'when a user does not have access to dast_site_validations' do
before do
project.add_guest(user)
end
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
context 'when a user has access dast_site_validations' do
before do
project.add_developer(user)
end
it { is_expected.to be_allowed(:create_on_demand_dast_scan) }
context 'when on demand scan licensed feature is not available' do
before do
stub_licensed_features(security_on_demand_scans: false)
end
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
end
it_behaves_like 'a dast on-demand scan policy' do
let_it_be(:record) { create(:dast_site_validation, dast_site_token: create(:dast_site_token, project: project)) }
end
end
......@@ -22,6 +22,7 @@ RSpec.describe API::Search, factory_default: :keep do
it 'returns a different result for each page' do
get api(endpoint, user), params: { scope: scope, search: search, page: 1, per_page: 1 }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.count).to eq(1)
first = json_response.first
......@@ -37,6 +38,30 @@ RSpec.describe API::Search, factory_default: :keep do
end
end
shared_examples 'orderable by created_at' do |scope:|
it 'allows ordering results by created_at asc' do
get api(endpoint, user), params: { scope: scope, search: '*', order_by: 'created_at', sort: 'asc' }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.count).to be > 1
created_ats = json_response.map { |r| Time.parse(r['created_at']) }
expect(created_ats).to eq(created_ats.sort)
end
it 'allows ordering results by created_at desc' do
get api(endpoint, user), params: { scope: scope, search: '*', order_by: 'created_at', sort: 'desc' }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.count).to be > 1
created_ats = json_response.map { |r| Time.parse(r['created_at']) }
expect(created_ats).to eq(created_ats.sort.reverse)
end
end
shared_examples 'elasticsearch disabled' do
it 'returns 400 error for wiki_blobs, blobs and commits scope' do
get api(endpoint, user), params: { scope: 'wiki_blobs', search: 'awesome' }
......@@ -61,6 +86,7 @@ RSpec.describe API::Search, factory_default: :keep do
end
it_behaves_like 'pagination', scope: 'merge_requests'
it_behaves_like 'orderable by created_at', scope: 'merge_requests'
it 'avoids N+1 queries' do
control = ActiveRecord::QueryRecorder.new { get api(endpoint, user), params: { scope: 'merge_requests', search: '*' } }
......@@ -213,6 +239,7 @@ RSpec.describe API::Search, factory_default: :keep do
end
it_behaves_like 'pagination', scope: 'issues'
it_behaves_like 'orderable by created_at', scope: 'issues'
end
unless level == :project
......
......@@ -24,11 +24,11 @@ RSpec.describe Security::StoreReportService, '#execute' do
using RSpec::Parameterized::TableSyntax
where(:case_name, :trait, :scanners, :identifiers, :findings, :finding_identifiers, :finding_pipelines) do
'with SAST report' | :sast | 3 | 17 | 33 | 39 | 33
'with exceeding identifiers' | :with_exceeding_identifiers | 1 | 20 | 1 | 20 | 1
'with Dependency Scanning report' | :dependency_scanning | 2 | 7 | 4 | 7 | 4
'with Container Scanning report' | :container_scanning | 1 | 8 | 8 | 8 | 8
where(:case_name, :trait, :scanners, :identifiers, :findings, :finding_identifiers, :finding_pipelines, :finding_links) do
'with SAST report' | :sast | 3 | 17 | 33 | 39 | 33 | 0
'with exceeding identifiers' | :with_exceeding_identifiers | 1 | 20 | 1 | 20 | 1 | 0
'with Dependency Scanning report' | :dependency_scanning | 2 | 7 | 4 | 7 | 4 | 6
'with Container Scanning report' | :container_scanning | 1 | 8 | 8 | 8 | 8 | 8
end
with_them do
......
# frozen_string_literal: true
RSpec.shared_examples 'a dast on-demand scan policy' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }
subject { described_class.new(user, record) }
before do
stub_licensed_features(security_on_demand_scans: true)
end
describe 'create_on_demand_dast_scan' do
context 'when a user does not have access to the project' do
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
context 'when the user is a guest' do
before do
project.add_guest(user)
end
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
context 'when the user is a reporter' do
before do
project.add_reporter(user)
end
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
context 'when the user is a developer' do
before do
project.add_developer(user)
end
it { is_expected.to be_allowed(:create_on_demand_dast_scan) }
end
context 'when the user is a maintainer' do
before do
project.add_maintainer(user)
end
it { is_expected.to be_allowed(:create_on_demand_dast_scan) }
end
context 'when the user is an owner' do
before do
group.add_owner(user)
end
it { is_expected.to be_allowed(:create_on_demand_dast_scan) }
end
context 'when the user is allowed' do
before do
project.add_developer(user)
end
context 'when on demand scan licensed feature is not available' do
let(:project) { create(:project, group: group) } # allows license stub to work correctly
before do
stub_licensed_features(security_on_demand_scans: false)
end
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
end
end
end
......@@ -186,6 +186,7 @@ module API
mount ::API::ImportBitbucketServer
mount ::API::ImportGithub
mount ::API::IssueLinks
mount ::API::Invitations
mount ::API::Issues
mount ::API::JobArtifacts
mount ::API::Jobs
......
# frozen_string_literal: true
module API
module Entities
class Invitation < Grape::Entity
expose :access_level
expose :requested_at
expose :expires_at
expose :invite_email
expose :invite_token
expose :user_id
end
end
end
# frozen_string_literal: true
module API
class Invitations < ::API::Base
feature_category :users
before { authenticate! }
helpers ::API::Helpers::MembersHelpers
%w[group project].each do |source_type|
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Invite non-members by email address to a group or project.' do
detail 'This feature was introduced in GitLab 13.6'
success Entities::Invitation
end
params do
requires :email, types: [String, Array[String]], email_or_email_list: true, desc: 'The email address to invite, or multiple emails separated by comma'
requires :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)'
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
end
post ":id/invitations" do
source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source)
::Members::InviteService.new(current_user, params).execute(source)
end
end
end
end
end
......@@ -39,7 +39,9 @@ module API
snippets: snippets?,
basic_search: params[:basic_search],
page: params[:page],
per_page: params[:per_page]
per_page: params[:per_page],
order_by: params[:order_by],
sort: params[:sort]
}.merge(additional_params)
results = SearchService.new(current_user, search_params).search_objects(preload_method)
......
# frozen_string_literal: true
module API
module Validations
module Validators
class EmailOrEmailList < Grape::Validations::Base
def validate_param!(attr_name, params)
value = params[attr_name]
return unless value
return if value.split(',').map { |v| ValidateEmail.valid?(v) }.all?
raise Grape::Exceptions::Validation,
params: [@scope.full_name(attr_name)],
message: "contains an invalid email address"
end
end
end
end
end
......@@ -81,11 +81,15 @@ module Gitlab
# We are ignoring connections and built in types for now,
# they should be added when queries are generated.
def objects
graphql_object_types.select do |object_type|
object_types = graphql_object_types.select do |object_type|
!object_type[:name]["Connection"] &&
!object_type[:name]["Edge"] &&
!object_type[:name]["__"]
end
object_types.each do |type|
type[:fields] += type[:connections]
end
end
# We ignore the built-in enum types.
......
......@@ -4,10 +4,10 @@ module Gitlab
class GroupSearchResults < SearchResults
attr_reader :group
def initialize(current_user, query, limit_projects = nil, group:, default_project_filter: false, sort: nil, filters: {})
def initialize(current_user, query, limit_projects = nil, group:, default_project_filter: false, order_by: nil, sort: nil, filters: {})
@group = group
super(current_user, query, limit_projects, default_project_filter: default_project_filter, sort: sort, filters: filters)
super(current_user, query, limit_projects, default_project_filter: default_project_filter, order_by: order_by, sort: sort, filters: filters)
end
# rubocop:disable CodeReuse/ActiveRecord
......
......@@ -4,11 +4,11 @@ module Gitlab
class ProjectSearchResults < SearchResults
attr_reader :project, :repository_ref
def initialize(current_user, query, project:, repository_ref: nil, sort: nil, filters: {})
def initialize(current_user, query, project:, repository_ref: nil, order_by: nil, sort: nil, filters: {})
@project = project
@repository_ref = repository_ref.presence
super(current_user, query, [project], sort: sort, filters: filters)
super(current_user, query, [project], order_by: order_by, sort: sort, filters: filters)
end
def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil)
......
# frozen_string_literal: true
module Gitlab
module Search
module SortOptions
def sort_and_direction(order_by, sort)
# Due to different uses of sort param in web vs. API requests we prefer
# order_by when present
case [order_by, sort]
when %w[created_at asc], [nil, 'created_asc']
:created_at_asc
when %w[created_at desc], [nil, 'created_desc']
:created_at_desc
else
:unknown
end
end
module_function :sort_and_direction # rubocop: disable Style/AccessModifierDeclarations
end
end
end
......@@ -7,7 +7,7 @@ module Gitlab
DEFAULT_PAGE = 1
DEFAULT_PER_PAGE = 20
attr_reader :current_user, :query, :sort, :filters
attr_reader :current_user, :query, :order_by, :sort, :filters
# Limit search results by passed projects
# It allows us to search only for projects user has access to
......@@ -19,11 +19,12 @@ module Gitlab
# query
attr_reader :default_project_filter
def initialize(current_user, query, limit_projects = nil, sort: nil, default_project_filter: false, filters: {})
def initialize(current_user, query, limit_projects = nil, order_by: nil, sort: nil, default_project_filter: false, filters: {})
@current_user = current_user
@query = query
@limit_projects = limit_projects || Project.all
@default_project_filter = default_project_filter
@order_by = order_by
@sort = sort
@filters = filters
end
......@@ -128,10 +129,12 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def apply_sort(scope)
case sort
when 'created_asc'
# Due to different uses of sort param we prefer order_by when
# present
case ::Gitlab::Search::SortOptions.sort_and_direction(order_by, sort)
when :created_at_asc
scope.reorder('created_at ASC')
when 'created_desc'
when :created_at_desc
scope.reorder('created_at DESC')
else
scope.reorder('created_at DESC')
......
......@@ -9866,6 +9866,9 @@ msgstr ""
msgid "Email Notification"
msgstr ""
msgid "Email cannot be blank"
msgstr ""
msgid "Email could not be sent"
msgstr ""
......@@ -16545,7 +16548,7 @@ msgstr ""
msgid "Members|%{time} by %{user}"
msgstr ""
msgid "Members|%{userName} is currently a LDAP user. Editing their permissions will override the settings from the LDAP group sync."
msgid "Members|%{userName} is currently an LDAP user. Editing their permissions will override the settings from the LDAP group sync."
msgstr ""
msgid "Members|An error occurred while trying to enable LDAP override, please try again."
......@@ -28202,6 +28205,9 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
msgid "Too many users specified (limit is %{user_limit})"
msgstr ""
msgid "Too much data"
msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::GroupInvitationType do
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Group) }
specify { expect(described_class.graphql_name).to eq('GroupInvitation') }
specify { expect(described_class).to require_graphql_authorizations(:read_group) }
it 'has the expected fields' do
expected_fields = %w[
email access_level created_by created_at updated_at expires_at group
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::InvitationInterface do
it 'exposes the expected fields' do
expected_fields = %i[
email
access_level
created_by
created_at
updated_at
expires_at
user
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
describe '.resolve_type' do
subject { described_class.resolve_type(object, {}) }
context 'for project member' do
let(:object) { build(:project_member) }
it { is_expected.to be Types::ProjectInvitationType }
end
context 'for group member' do
let(:object) { build(:group_member) }
it { is_expected.to be Types::GroupInvitationType }
end
context 'for an unknown type' do
let(:object) { build(:user) }
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::BaseError)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::ProjectInvitationType do
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Project) }
specify { expect(described_class.graphql_name).to eq('ProjectInvitation') }
specify { expect(described_class).to require_graphql_authorizations(:read_project) }
it 'has the expected fields' do
expected_fields = %w[
access_level created_by created_at updated_at expires_at project user
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Validations::Validators::EmailOrEmailList do
include ApiValidatorsHelpers
subject do
described_class.new(['email'], {}, false, scope.new)
end
context 'with valid email addresses' do
it 'does not raise a validation error' do
expect_no_validation_error('test' => 'test@example.org')
expect_no_validation_error('test' => 'test1@example.com,test2@example.org')
expect_no_validation_error('test' => 'test1@example.com,test2@example.org,test3@example.co.uk')
end
end
context 'including any invalid email address' do
it 'raises a validation error' do
expect_validation_error('test' => 'not')
expect_validation_error('test' => '@example.com')
expect_validation_error('test' => 'test1@example.com,asdf')
expect_validation_error('test' => 'asdf,testa1@example.com,asdf')
end
end
end
# frozen_string_literal: true
require 'fast_spec_helper'
require 'gitlab/search/sort_options'
RSpec.describe ::Gitlab::Search::SortOptions do
describe '.sort_and_direction' do
context 'using order_by and sort' do
it 'returns matched options' do
expect(described_class.sort_and_direction('created_at', 'asc')).to eq(:created_at_asc)
expect(described_class.sort_and_direction('created_at', 'desc')).to eq(:created_at_desc)
end
end
context 'using just sort' do
it 'returns matched options' do
expect(described_class.sort_and_direction(nil, 'created_asc')).to eq(:created_at_asc)
expect(described_class.sort_and_direction(nil, 'created_desc')).to eq(:created_at_desc)
end
end
context 'when unknown option' do
it 'returns unknown' do
expect(described_class.sort_and_direction(nil, 'foo_asc')).to eq(:unknown)
expect(described_class.sort_and_direction(nil, 'bar_desc')).to eq(:unknown)
expect(described_class.sort_and_direction(nil, 'created_bar')).to eq(:unknown)
expect(described_class.sort_and_direction('created_at', 'foo')).to eq(:unknown)
expect(described_class.sort_and_direction('foo', 'desc')).to eq(:unknown)
expect(described_class.sort_and_direction('created_at', nil)).to eq(:unknown)
end
end
end
end
......@@ -3,13 +3,5 @@
require 'spec_helper'
RSpec.describe FromUnion do
[true, false].each do |sql_set_operator|
context "when sql-set-operators feature flag is #{sql_set_operator}" do
before do
stub_feature_flags(sql_set_operators: sql_set_operator)
end
it_behaves_like 'from set operator', Gitlab::SQL::Union
end
end
it_behaves_like 'from set operator', Gitlab::SQL::Union
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Invitations do
let(:maintainer) { create(:user, username: 'maintainer_user') }
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
let(:stranger) { create(:user) }
let(:email) { 'email@example.org' }
let(:project) do
create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
project.add_developer(developer)
project.add_maintainer(maintainer)
project.request_access(access_requester)
end
end
let!(:group) do
create(:group, :public) do |group|
group.add_developer(developer)
group.add_owner(maintainer)
group.request_access(access_requester)
end
end
def invitations_url(source, user)
api("/#{source.model_name.plural}/#{source.id}/invitations", user)
end
shared_examples 'POST /:source_type/:id/invitations' do |source_type|
context "with :source_type == #{source_type.pluralize}" do
it_behaves_like 'a 404 response when source is private' do
let(:route) do
post invitations_url(source, stranger),
params: { email: email, access_level: Member::MAINTAINER }
end
end
context 'when authenticated as a non-member or member with insufficient rights' do
%i[access_requester stranger developer].each do |type|
context "as a #{type}" do
it 'returns 403' do
user = public_send(type)
post invitations_url(source, user), params: { email: email, access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
context 'when authenticated as a maintainer/owner' do
context 'and new member is already a requester' do
it 'does not transform the requester into a proper member' do
expect do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: email, access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:created)
end.not_to change { source.members.count }
end
end
it 'invites a new member' do
expect do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: email, access_level: Member::DEVELOPER }
expect(response).to have_gitlab_http_status(:created)
end.to change { source.requesters.count }.by(1)
end
it 'invites a list of new email addresses' do
expect do
email_list = 'email1@example.com,email2@example.com'
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: email_list, access_level: Member::DEVELOPER }
expect(response).to have_gitlab_http_status(:created)
end.to change { source.requesters.count }.by(2)
end
end
context 'access levels' do
it 'does not create the member if group level is higher' do
parent = create(:group)
group.update!(parent: parent)
project.update!(group: group)
parent.add_developer(stranger)
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: stranger.email, access_level: Member::REPORTER }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['message'][stranger.email]).to eq("Access level should be greater than or equal to Developer inherited membership from group #{parent.name}")
end
it 'creates the member if group level is lower' do
parent = create(:group)
group.update!(parent: parent)
project.update!(group: group)
parent.add_developer(stranger)
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: stranger.email, access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:created)
end
end
context 'access expiry date' do
subject do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: email, access_level: Member::DEVELOPER, expires_at: expires_at }
end
context 'when set to a date in the past' do
let(:expires_at) { 2.days.ago.to_date }
it 'does not create a member' do
expect do
subject
end.not_to change { source.members.count }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['message'][email]).to eq('Expires at cannot be a date in the past')
end
end
context 'when set to a date in the future' do
let(:expires_at) { 2.days.from_now.to_date }
it 'invites a member' do
expect do
subject
end.to change { source.requesters.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
end
end
end
it "returns a message if member already exists" do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: maintainer.email, access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['message'][maintainer.email]).to eq("Already a member of #{source.name}")
end
it 'returns 404 when the email is not valid' do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: '', access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['message']).to eq('Email cannot be blank')
end
it 'returns 404 when the email list is not a valid format' do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: 'email1@example.com,not-an-email', access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('email contains an invalid email address')
end
it 'returns 400 when email is not given' do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'returns 400 when access_level is not given' do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: email }
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'returns 400 when access_level is not valid' do
post invitations_url(source, maintainer),
params: { email: email, access_level: non_existing_record_access_level }
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
describe 'POST /projects/:id/invitations' do
it_behaves_like 'POST /:source_type/:id/invitations', 'project' do
let(:source) { project }
end
end
describe 'POST /groups/:id/invitations' do
it_behaves_like 'POST /:source_type/:id/invitations', 'group' do
let(:source) { group }
end
end
end
......@@ -23,6 +23,48 @@ RSpec.describe API::Search do
end
end
shared_examples 'orderable by created_at' do |scope:|
it 'allows ordering results by created_at asc' do
get api(endpoint, user), params: { scope: scope, search: 'sortable', order_by: 'created_at', sort: 'asc' }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.count).to be > 1
created_ats = json_response.map { |r| Time.parse(r['created_at']) }
expect(created_ats.uniq.count).to be > 1
expect(created_ats).to eq(created_ats.sort)
end
it 'allows ordering results by created_at desc' do
get api(endpoint, user), params: { scope: scope, search: 'sortable', order_by: 'created_at', sort: 'desc' }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.count).to be > 1
created_ats = json_response.map { |r| Time.parse(r['created_at']) }
expect(created_ats.uniq.count).to be > 1
expect(created_ats).to eq(created_ats.sort.reverse)
end
end
shared_examples 'issues orderable by created_at' do
before do
create_list(:issue, 3, title: 'sortable item', project: project)
end
it_behaves_like 'orderable by created_at', scope: :issues
end
shared_examples 'merge_requests orderable by created_at' do
before do
create_list(:merge_request, 3, :unique_branches, title: 'sortable item', target_project: repo_project, source_project: repo_project)
end
it_behaves_like 'orderable by created_at', scope: :merge_requests
end
shared_examples 'pagination' do |scope:, search: ''|
it 'returns a different result for each page' do
get api(endpoint, user), params: { scope: scope, search: search, page: 1, per_page: 1 }
......@@ -121,6 +163,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :issues
it_behaves_like 'issues orderable by created_at'
describe 'pagination' do
before do
create(:issue, project: project, title: 'another issue')
......@@ -181,6 +225,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :merge_requests
it_behaves_like 'merge_requests orderable by created_at'
describe 'pagination' do
before do
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
......@@ -354,6 +400,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :issues
it_behaves_like 'issues orderable by created_at'
describe 'pagination' do
before do
create(:issue, project: project, title: 'another issue')
......@@ -374,6 +422,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :merge_requests
it_behaves_like 'merge_requests orderable by created_at'
describe 'pagination' do
before do
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
......@@ -506,6 +556,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :issues
it_behaves_like 'issues orderable by created_at'
describe 'pagination' do
before do
create(:issue, project: project, title: 'another issue')
......@@ -536,6 +588,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :merge_requests
it_behaves_like 'merge_requests orderable by created_at'
describe 'pagination' do
before do
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
......
# 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
......
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