summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-12-13 09:08:01 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-12-13 09:08:01 +0000
commit17b91a3c6ab73fff087e91665e9afb8046cbf045 (patch)
tree04655a8630478d9846571875f69469f018d4bdcc
parentb3db40398ce9ad335270617e834fde96d46f90ea (diff)
downloadgitlab-ce-17b91a3c6ab73fff087e91665e9afb8046cbf045.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js37
-rw-r--r--app/controllers/projects/jobs_controller.rb4
-rw-r--r--app/graphql/mutations/snippets/base.rb30
-rw-r--r--app/graphql/mutations/snippets/create.rb77
-rw-r--r--app/graphql/mutations/snippets/destroy.rb33
-rw-r--r--app/graphql/mutations/snippets/update.rb54
-rw-r--r--app/graphql/resolvers/base_resolver.rb10
-rw-r--r--app/graphql/types/mutation_type.rb3
-rw-r--r--app/graphql/types/snippet_type.rb4
-rw-r--r--app/graphql/types/visibility_levels_enum.rb9
-rw-r--r--app/presenters/snippet_presenter.rb2
-rw-r--r--app/views/admin/runners/index.html.haml4
-rw-r--r--changelogs/unreleased/29165-confusing-wording-fix.yml5
-rw-r--r--changelogs/unreleased/fj-36079-snippet-graphql-endpoints-with-mutations.yml5
-rw-r--r--changelogs/unreleased/toggle-job-log-json-flag-on.yml5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql168
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json532
-rw-r--r--doc/api/graphql/reference/index.md26
-rw-r--r--doc/ci/img/collapsible_log.pngbin60771 -> 278074 bytes
-rw-r--r--doc/ci/pipelines.md7
-rw-r--r--doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_4.pngbin62965 -> 0 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_6.pngbin0 -> 69145 bytes
-rw-r--r--doc/user/application_security/security_dashboard/index.md13
-rw-r--r--lib/gitlab/graphql/authorize/authorize_resource.rb9
-rw-r--r--locale/gitlab.pot56
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb3
-rw-r--r--spec/features/projects/jobs/permissions_spec.rb1
-rw-r--r--spec/features/projects/jobs/user_browses_job_spec.rb4
-rw-r--r--spec/features/projects/jobs_spec.rb3
-rw-r--r--spec/features/security/project/internal_access_spec.rb4
-rw-r--r--spec/features/security/project/private_access_spec.rb4
-rw-r--r--spec/features/security/project/public_access_spec.rb4
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js24
-rw-r--r--spec/graphql/resolvers/base_resolver_spec.rb26
-rw-r--r--spec/graphql/types/snippet_type_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/snippets/create_spec.rb144
-rw-r--r--spec/requests/api/graphql/mutations/snippets/destroy_spec.rb89
-rw-r--r--spec/requests/api/graphql/mutations/snippets/update_spec.rb144
38 files changed, 1501 insertions, 44 deletions
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 202a44d5694..6a61d92d9e8 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,4 +1,6 @@
-import { join as joinPaths } from 'path';
+const PATH_SEPARATOR = '/';
+const PATH_SEPARATOR_LEADING_REGEX = new RegExp(`^${PATH_SEPARATOR}+`);
+const PATH_SEPARATOR_ENDING_REGEX = new RegExp(`${PATH_SEPARATOR}+$`);
// Returns a decoded url parameter value
// - Treats '+' as '%20'
@@ -6,6 +8,37 @@ function decodeUrlParameter(val) {
return decodeURIComponent(val.replace(/\+/g, '%20'));
}
+function cleanLeadingSeparator(path) {
+ return path.replace(PATH_SEPARATOR_LEADING_REGEX, '');
+}
+
+function cleanEndingSeparator(path) {
+ return path.replace(PATH_SEPARATOR_ENDING_REGEX, '');
+}
+
+/**
+ * Safely joins the given paths which might both start and end with a `/`
+ *
+ * Example:
+ * - `joinPaths('abc/', '/def') === 'abc/def'`
+ * - `joinPaths(null, 'abc/def', 'zoo) === 'abc/def/zoo'`
+ *
+ * @param {...String} paths
+ * @returns {String}
+ */
+export function joinPaths(...paths) {
+ return paths.reduce((acc, path) => {
+ if (!path) {
+ return acc;
+ }
+ if (!acc) {
+ return path;
+ }
+
+ return [cleanEndingSeparator(acc), PATH_SEPARATOR, cleanLeadingSeparator(path)].join('');
+ }, '');
+}
+
// Returns an array containing the value(s) of the
// of the key passed as an argument
export function getParameterValues(sParam, url = window.location) {
@@ -212,5 +245,3 @@ export function objectToQuery(obj) {
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(obj[k])}`)
.join('&');
}
-
-export { joinPaths };
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 9480900b57a..796f3ff603f 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -12,7 +12,7 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize]
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action only: [:show] do
- push_frontend_feature_flag(:job_log_json, project)
+ push_frontend_feature_flag(:job_log_json, project, default_enabled: true)
end
layout 'project'
@@ -53,7 +53,7 @@ class Projects::JobsController < Projects::ApplicationController
format.json do
# TODO: when the feature flag is removed we should not pass
# content_format to serialize method.
- content_format = Feature.enabled?(:job_log_json, @project) ? :json : :html
+ content_format = Feature.enabled?(:job_log_json, @project, default_enabled: true) ? :json : :html
build_trace = Ci::BuildTrace.new(
build: @build,
diff --git a/app/graphql/mutations/snippets/base.rb b/app/graphql/mutations/snippets/base.rb
new file mode 100644
index 00000000000..9dc6d49774e
--- /dev/null
+++ b/app/graphql/mutations/snippets/base.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Snippets
+ class Base < BaseMutation
+ field :snippet,
+ Types::SnippetType,
+ null: true,
+ description: 'The snippet after mutation'
+
+ private
+
+ def find_object(id:)
+ GitlabSchema.object_from_id(id)
+ end
+
+ def authorized_resource?(snippet)
+ Ability.allowed?(context[:current_user], ability_for(snippet), snippet)
+ end
+
+ def ability_for(snippet)
+ "#{ability_name}_#{snippet.to_ability_name}".to_sym
+ end
+
+ def ability_name
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/snippets/create.rb b/app/graphql/mutations/snippets/create.rb
new file mode 100644
index 00000000000..fe1f543ea1a
--- /dev/null
+++ b/app/graphql/mutations/snippets/create.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Snippets
+ class Create < BaseMutation
+ include Mutations::ResolvesProject
+
+ graphql_name 'CreateSnippet'
+
+ field :snippet,
+ Types::SnippetType,
+ null: true,
+ description: 'The snippet after mutation'
+
+ argument :title, GraphQL::STRING_TYPE,
+ required: true,
+ description: 'Title of the snippet'
+
+ argument :file_name, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'File name of the snippet'
+
+ argument :content, GraphQL::STRING_TYPE,
+ required: true,
+ description: 'Content of the snippet'
+
+ argument :description, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Description of the snippet'
+
+ argument :visibility_level, Types::VisibilityLevelsEnum,
+ description: 'The visibility level of the snippet',
+ required: true
+
+ argument :project_path, GraphQL::ID_TYPE,
+ required: false,
+ description: 'The project full path the snippet is associated with'
+
+ def resolve(args)
+ project_path = args.delete(:project_path)
+
+ if project_path.present?
+ project = find_project!(project_path: project_path)
+ elsif !can_create_personal_snippet?
+ raise_resource_not_avaiable_error!
+ end
+
+ snippet = CreateSnippetService.new(project,
+ context[:current_user],
+ args).execute
+
+ {
+ snippet: snippet.valid? ? snippet : nil,
+ errors: errors_on_object(snippet)
+ }
+ end
+
+ private
+
+ def find_project!(project_path:)
+ authorized_find!(full_path: project_path)
+ end
+
+ def find_object(full_path:)
+ resolve_project(full_path: full_path)
+ end
+
+ def authorized_resource?(project)
+ Ability.allowed?(context[:current_user], :create_project_snippet, project)
+ end
+
+ def can_create_personal_snippet?
+ Ability.allowed?(context[:current_user], :create_personal_snippet)
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/snippets/destroy.rb b/app/graphql/mutations/snippets/destroy.rb
new file mode 100644
index 00000000000..115fcfd6488
--- /dev/null
+++ b/app/graphql/mutations/snippets/destroy.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Snippets
+ class Destroy < Base
+ graphql_name 'DestroySnippet'
+
+ ERROR_MSG = 'Error deleting the snippet'
+
+ argument :id,
+ GraphQL::ID_TYPE,
+ required: true,
+ description: 'The global id of the snippet to destroy'
+
+ def resolve(id:)
+ snippet = authorized_find!(id: id)
+
+ result = snippet.destroy
+ errors = result ? [] : [ERROR_MSG]
+
+ {
+ errors: errors
+ }
+ end
+
+ private
+
+ def ability_name
+ "admin"
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/snippets/update.rb b/app/graphql/mutations/snippets/update.rb
new file mode 100644
index 00000000000..27c232bc7f8
--- /dev/null
+++ b/app/graphql/mutations/snippets/update.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Snippets
+ class Update < Base
+ graphql_name 'UpdateSnippet'
+
+ argument :id,
+ GraphQL::ID_TYPE,
+ required: true,
+ description: 'The global id of the snippet to update'
+
+ argument :title, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Title of the snippet'
+
+ argument :file_name, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'File name of the snippet'
+
+ argument :content, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Content of the snippet'
+
+ argument :description, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Description of the snippet'
+
+ argument :visibility_level, Types::VisibilityLevelsEnum,
+ description: 'The visibility level of the snippet',
+ required: false
+
+ def resolve(args)
+ snippet = authorized_find!(id: args.delete(:id))
+
+ result = UpdateSnippetService.new(snippet.project,
+ context[:current_user],
+ snippet,
+ args).execute
+
+ {
+ snippet: result ? snippet : snippet.reset,
+ errors: errors_on_object(snippet)
+ }
+ end
+
+ private
+
+ def ability_name
+ "update"
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
index 85d6b377934..62dcc41dd9c 100644
--- a/app/graphql/resolvers/base_resolver.rb
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -2,6 +2,8 @@
module Resolvers
class BaseResolver < GraphQL::Schema::Resolver
+ extend ::Gitlab::Utils::Override
+
def self.single
@single ||= Class.new(self) do
def resolve(**args)
@@ -36,5 +38,13 @@ module Resolvers
# complexity difference is minimal in this case.
[args[:iid], args[:iids]].any? ? 0 : 0.01
end
+
+ override :object
+ def object
+ super.tap do |obj|
+ # If the field this resolver is used in is wrapped in a presenter, go back to it's subject
+ break obj.subject if obj.is_a?(Gitlab::View::Presenter::Base)
+ end
+ end
end
end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index e8f4ec06177..998dfdc7815 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -25,6 +25,9 @@ module Types
mount_mutation Mutations::Todos::MarkDone
mount_mutation Mutations::Todos::Restore
mount_mutation Mutations::Todos::MarkAllDone
+ mount_mutation Mutations::Snippets::Destroy
+ mount_mutation Mutations::Snippets::Update
+ mount_mutation Mutations::Snippets::Create
end
end
diff --git a/app/graphql/types/snippet_type.rb b/app/graphql/types/snippet_type.rb
index 3b4dce1d486..3f780528945 100644
--- a/app/graphql/types/snippet_type.rb
+++ b/app/graphql/types/snippet_type.rb
@@ -44,8 +44,8 @@ module Types
description: 'Description of the snippet',
null: true
- field :visibility, GraphQL::STRING_TYPE,
- description: 'Visibility of the snippet',
+ field :visibility_level, Types::VisibilityLevelsEnum,
+ description: 'Visibility Level of the snippet',
null: false
field :created_at, Types::TimeType,
diff --git a/app/graphql/types/visibility_levels_enum.rb b/app/graphql/types/visibility_levels_enum.rb
new file mode 100644
index 00000000000..d5ace24455e
--- /dev/null
+++ b/app/graphql/types/visibility_levels_enum.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Types
+ class VisibilityLevelsEnum < BaseEnum
+ Gitlab::VisibilityLevel.string_options.each do |name, int_value|
+ value name.downcase, value: int_value
+ end
+ end
+end
diff --git a/app/presenters/snippet_presenter.rb b/app/presenters/snippet_presenter.rb
index ca8ae8d60c4..37c9ebd3305 100644
--- a/app/presenters/snippet_presenter.rb
+++ b/app/presenters/snippet_presenter.rb
@@ -30,6 +30,6 @@ class SnippetPresenter < Gitlab::View::Presenter::Delegated
end
def ability_name(ability_prefix)
- "#{ability_prefix}_#{snippet.class.underscore}".to_sym
+ "#{ability_prefix}_#{snippet.to_ability_name}".to_sym
end
end
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 2bf2b5fce8d..f8ef7a45f7f 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -10,7 +10,7 @@
%br
%div
- %span= _('Each Runner can be in one of the following states:')
+ %span= _('Each Runner can be in one of the following states and/or belong to one of the following types:')
%ul
%li
%span.badge.badge-success shared
@@ -120,7 +120,7 @@
.runners-content.content-list
.table-holder
.gl-responsive-table-row.table-row-header{ role: 'row' }
- .table-section.section-10{ role: 'rowheader' }= _('Type')
+ .table-section.section-10{ role: 'rowheader' }= _('Type/State')
.table-section.section-10{ role: 'rowheader' }= _('Runner token')
.table-section.section-20{ role: 'rowheader' }= _('Description')
.table-section.section-10{ role: 'rowheader' }= _('Version')
diff --git a/changelogs/unreleased/29165-confusing-wording-fix.yml b/changelogs/unreleased/29165-confusing-wording-fix.yml
new file mode 100644
index 00000000000..95d64156833
--- /dev/null
+++ b/changelogs/unreleased/29165-confusing-wording-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes wording on runner admin
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/fj-36079-snippet-graphql-endpoints-with-mutations.yml b/changelogs/unreleased/fj-36079-snippet-graphql-endpoints-with-mutations.yml
new file mode 100644
index 00000000000..930e9cbdb56
--- /dev/null
+++ b/changelogs/unreleased/fj-36079-snippet-graphql-endpoints-with-mutations.yml
@@ -0,0 +1,5 @@
+---
+title: Added Snippets GraphQL mutations
+merge_request: 20956
+author:
+type: added
diff --git a/changelogs/unreleased/toggle-job-log-json-flag-on.yml b/changelogs/unreleased/toggle-job-log-json-flag-on.yml
new file mode 100644
index 00000000000..2881bf7671e
--- /dev/null
+++ b/changelogs/unreleased/toggle-job-log-json-flag-on.yml
@@ -0,0 +1,5 @@
+---
+title: Enable new job log by default
+merge_request: 21543
+author:
+type: added
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 7089ee8e51d..1c3902aafcc 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -442,6 +442,66 @@ type CreateNotePayload {
note: Note
}
+"""
+Autogenerated input type of CreateSnippet
+"""
+input CreateSnippetInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Content of the snippet
+ """
+ content: String!
+
+ """
+ Description of the snippet
+ """
+ description: String
+
+ """
+ File name of the snippet
+ """
+ fileName: String
+
+ """
+ The project full path the snippet is associated with
+ """
+ projectPath: ID
+
+ """
+ Title of the snippet
+ """
+ title: String!
+
+ """
+ The visibility level of the snippet
+ """
+ visibilityLevel: VisibilityLevelsEnum!
+}
+
+"""
+Autogenerated return type of CreateSnippet
+"""
+type CreateSnippetPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The snippet after mutation
+ """
+ snippet: Snippet
+}
+
type Design implements Noteable {
diffRefs: DiffRefs!
@@ -861,6 +921,41 @@ type DestroyNotePayload {
note: Note
}
+"""
+Autogenerated input type of DestroySnippet
+"""
+input DestroySnippetInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The global id of the snippet to destroy
+ """
+ id: ID!
+}
+
+"""
+Autogenerated return type of DestroySnippet
+"""
+type DestroySnippetPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The snippet after mutation
+ """
+ snippet: Snippet
+}
+
type DetailedStatus {
detailsPath: String!
favicon: String!
@@ -3737,9 +3832,11 @@ type Mutation {
createEpic(input: CreateEpicInput!): CreateEpicPayload
createImageDiffNote(input: CreateImageDiffNoteInput!): CreateImageDiffNotePayload
createNote(input: CreateNoteInput!): CreateNotePayload
+ createSnippet(input: CreateSnippetInput!): CreateSnippetPayload
designManagementDelete(input: DesignManagementDeleteInput!): DesignManagementDeletePayload
designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload
destroyNote(input: DestroyNoteInput!): DestroyNotePayload
+ destroySnippet(input: DestroySnippetInput!): DestroySnippetPayload
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
issueSetConfidential(input: IssueSetConfidentialInput!): IssueSetConfidentialPayload
@@ -3757,6 +3854,7 @@ type Mutation {
toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload
updateEpic(input: UpdateEpicInput!): UpdateEpicPayload
updateNote(input: UpdateNoteInput!): UpdateNotePayload
+ updateSnippet(input: UpdateSnippetInput!): UpdateSnippetPayload
}
"""
@@ -5396,9 +5494,9 @@ type Snippet implements Noteable {
userPermissions: SnippetPermissions!
"""
- Visibility of the snippet
+ Visibility Level of the snippet
"""
- visibility: String!
+ visibilityLevel: VisibilityLevelsEnum!
"""
Web URL of the snippet
@@ -6120,6 +6218,66 @@ type UpdateNotePayload {
note: Note
}
+"""
+Autogenerated input type of UpdateSnippet
+"""
+input UpdateSnippetInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Content of the snippet
+ """
+ content: String
+
+ """
+ Description of the snippet
+ """
+ description: String
+
+ """
+ File name of the snippet
+ """
+ fileName: String
+
+ """
+ The global id of the snippet to update
+ """
+ id: ID!
+
+ """
+ Title of the snippet
+ """
+ title: String
+
+ """
+ The visibility level of the snippet
+ """
+ visibilityLevel: VisibilityLevelsEnum
+}
+
+"""
+Autogenerated return type of UpdateSnippet
+"""
+type UpdateSnippetPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The snippet after mutation
+ """
+ snippet: Snippet
+}
+
scalar Upload
type User {
@@ -6286,6 +6444,12 @@ type UserPermissions {
createSnippet: Boolean!
}
+enum VisibilityLevelsEnum {
+ internal
+ private
+ public
+}
+
enum VisibilityScopesEnum {
internal
private
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index aa7951e6bf7..0d24f80cde0 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -6442,8 +6442,8 @@
"deprecationReason": null
},
{
- "name": "visibility",
- "description": "Visibility of the snippet",
+ "name": "visibilityLevel",
+ "description": "Visibility Level of the snippet",
"args": [
],
@@ -6451,8 +6451,8 @@
"kind": "NON_NULL",
"name": null,
"ofType": {
- "kind": "SCALAR",
- "name": "String",
+ "kind": "ENUM",
+ "name": "VisibilityLevelsEnum",
"ofType": null
}
},
@@ -6830,6 +6830,35 @@
},
{
"kind": "ENUM",
+ "name": "VisibilityLevelsEnum",
+ "description": null,
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "private",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "internal",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "public",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
"name": "VisibilityScopesEnum",
"description": null,
"fields": null,
@@ -15805,6 +15834,33 @@
"deprecationReason": null
},
{
+ "name": "createSnippet",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "CreateSnippetInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CreateSnippetPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "designManagementDelete",
"description": null,
"args": [
@@ -15886,6 +15942,33 @@
"deprecationReason": null
},
{
+ "name": "destroySnippet",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "DestroySnippetInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "DestroySnippetPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "epicSetSubscription",
"description": null,
"args": [
@@ -16343,6 +16426,33 @@
},
"isDeprecated": false,
"deprecationReason": null
+ },
+ {
+ "name": "updateSnippet",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "UpdateSnippetInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "UpdateSnippetPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
}
],
"inputFields": null,
@@ -19095,6 +19205,420 @@
},
{
"kind": "OBJECT",
+ "name": "DestroySnippetPayload",
+ "description": "Autogenerated return type of DestroySnippet",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Reasons why the mutation failed.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "snippet",
+ "description": "The snippet after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Snippet",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "DestroySnippetInput",
+ "description": "Autogenerated input type of DestroySnippet",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "id",
+ "description": "The global id of the snippet to destroy",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "UpdateSnippetPayload",
+ "description": "Autogenerated return type of UpdateSnippet",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Reasons why the mutation failed.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "snippet",
+ "description": "The snippet after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Snippet",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "UpdateSnippetInput",
+ "description": "Autogenerated input type of UpdateSnippet",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "id",
+ "description": "The global id of the snippet to update",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "title",
+ "description": "Title of the snippet",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "fileName",
+ "description": "File name of the snippet",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "content",
+ "description": "Content of the snippet",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "description",
+ "description": "Description of the snippet",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "visibilityLevel",
+ "description": "The visibility level of the snippet",
+ "type": {
+ "kind": "ENUM",
+ "name": "VisibilityLevelsEnum",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CreateSnippetPayload",
+ "description": "Autogenerated return type of CreateSnippet",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Reasons why the mutation failed.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "snippet",
+ "description": "The snippet after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Snippet",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "CreateSnippetInput",
+ "description": "Autogenerated input type of CreateSnippet",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "title",
+ "description": "Title of the snippet",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "fileName",
+ "description": "File name of the snippet",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "content",
+ "description": "Content of the snippet",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "description",
+ "description": "Description of the snippet",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "visibilityLevel",
+ "description": "The visibility level of the snippet",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "VisibilityLevelsEnum",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "projectPath",
+ "description": "The project full path the snippet is associated with",
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "DesignManagementUploadPayload",
"description": "Autogenerated return type of DesignManagementUpload",
"fields": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index ec9b586d065..7e4ce169862 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -92,6 +92,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `errors` | String! => Array | Reasons why the mutation failed. |
| `note` | Note | The note after mutation |
+### CreateSnippetPayload
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `snippet` | Snippet | The snippet after mutation |
+
### Design
| Name | Type | Description |
@@ -145,6 +153,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `errors` | String! => Array | Reasons why the mutation failed. |
| `note` | Note | The note after mutation |
+### DestroySnippetPayload
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `snippet` | Snippet | The snippet after mutation |
+
### DetailedStatus
| Name | Type | Description |
@@ -802,7 +818,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `fileName` | String | File Name of the snippet |
| `content` | String! | Content of the snippet |
| `description` | String | Description of the snippet |
-| `visibility` | String! | Visibility of the snippet |
+| `visibilityLevel` | VisibilityLevelsEnum! | Visibility Level of the snippet |
| `createdAt` | Time! | Timestamp this snippet was created |
| `updatedAt` | Time! | Timestamp this snippet was updated |
| `webUrl` | String! | Web URL of the snippet |
@@ -929,6 +945,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `errors` | String! => Array | Reasons why the mutation failed. |
| `note` | Note | The note after mutation |
+### UpdateSnippetPayload
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `snippet` | Snippet | The snippet after mutation |
+
### User
| Name | Type | Description |
diff --git a/doc/ci/img/collapsible_log.png b/doc/ci/img/collapsible_log.png
index d2a570e246e..088928ddede 100644
--- a/doc/ci/img/collapsible_log.png
+++ b/doc/ci/img/collapsible_log.png
Binary files differ
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index c4cca944804..4538d1e7849 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -149,12 +149,13 @@ The union of A, B, and C is (1, 4) and (6, 7). Therefore, the total running time
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/14664) in GitLab
> 12.0.
-Job logs are divided into sections that can be collapsed or expanded.
+Job logs are divided into sections that can be collapsed or expanded. Each section will display
+the duration.
In the following example:
-- Two sections are expanded and can be collapsed.
-- One section is collapsed and can be expanded.
+- Two sections are collapsed and can be expanded.
+- Three sections are expanded and can be collapsed.
![Collapsible sections](img/collapsible_log.png)
diff --git a/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_4.png b/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_4.png
deleted file mode 100644
index 682dcbec63f..00000000000
--- a/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_4.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_6.png b/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_6.png
new file mode 100644
index 00000000000..c93a3ce8c35
--- /dev/null
+++ b/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_6.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md
index e3044fccafb..bb2bf0b7806 100644
--- a/doc/user/application_security/security_dashboard/index.md
+++ b/doc/user/application_security/security_dashboard/index.md
@@ -76,7 +76,7 @@ To the right of the filters, you should see a **Hide dismissed** toggle button.
NOTE: **Note:**
The dashboard only shows projects with [security reports](#supported-reports) enabled in a group.
-![dashboard with action buttons and metrics](img/group_security_dashboard_v12_4.png)
+![dashboard with action buttons and metrics](img/group_security_dashboard_v12_6.png)
Selecting one or more filters will filter the results in this page. Disabling the **Hide dismissed**
toggle button will let you also see vulnerabilities that have been dismissed.
@@ -97,6 +97,17 @@ vulnerabilities your projects had at various points in time. You can filter amon
90 days, with the default being 90. Hover over the chart to get more details about
the open vulnerabilities at a specific time.
+Below the timeline chart is a list of projects, grouped and sorted by the severity of the vulnerability found:
+
+- F: 1 or more "critical"
+- D: 1 or more "high" or "unknown"
+- C: 1 or more "medium"
+- B: 1 or more "low"
+- A: 0 vulnerabilities
+
+Projects with no vulnerability tests configured will not appear in the list. Additionally, dismissed
+vulnerabilities are not included either.
+
Read more on how to [interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities).
## Keeping the dashboards up to date
diff --git a/lib/gitlab/graphql/authorize/authorize_resource.rb b/lib/gitlab/graphql/authorize/authorize_resource.rb
index df60b9d8346..26e8c53032f 100644
--- a/lib/gitlab/graphql/authorize/authorize_resource.rb
+++ b/lib/gitlab/graphql/authorize/authorize_resource.rb
@@ -6,6 +6,8 @@ module Gitlab
module AuthorizeResource
extend ActiveSupport::Concern
+ RESOURCE_ACCESS_ERROR = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+
class_methods do
def required_permissions
# If the `#authorize` call is used on multiple classes, we add the
@@ -38,8 +40,7 @@ module Gitlab
def authorize!(object)
unless authorized_resource?(object)
- raise Gitlab::Graphql::Errors::ResourceNotAvailable,
- "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+ raise_resource_not_avaiable_error!
end
end
@@ -61,6 +62,10 @@ module Gitlab
Ability.allowed?(current_user, ability, object, scope: :user)
end
end
+
+ def raise_resource_not_avaiable_error!
+ raise Gitlab::Graphql::Errors::ResourceNotAvailable, RESOURCE_ACCESS_ERROR
+ end
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4a787a55e31..405d6948bad 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -153,6 +153,11 @@ msgid_plural "%d more comments"
msgstr[0] ""
msgstr[1] ""
+msgid "%d project"
+msgid_plural "%d projects"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d request with warnings"
msgid_plural "%d requests with warnings"
msgstr[0] ""
@@ -5139,6 +5144,9 @@ msgstr ""
msgid "Creation date"
msgstr ""
+msgid "Critical vulnerabilities present"
+msgstr ""
+
msgid "Cron Timezone"
msgstr ""
@@ -6225,6 +6233,9 @@ msgstr ""
msgid "Dynamic Application Security Testing (DAST)"
msgstr ""
+msgid "Each Runner can be in one of the following states and/or belong to one of the following types:"
+msgstr ""
+
msgid "Each Runner can be in one of the following states:"
msgstr ""
@@ -9205,6 +9216,9 @@ msgstr ""
msgid "Hiding all labels"
msgstr ""
+msgid "High or unknown vulnerabilities present"
+msgstr ""
+
msgid "Highest number of requests per minute for each raw path, default to 300. To disable throttling set to 0."
msgstr ""
@@ -10638,6 +10652,9 @@ msgstr ""
msgid "Logs|To see the pod logs, deploy your code to an environment."
msgstr ""
+msgid "Low vulnerabilities present"
+msgstr ""
+
msgid "MB"
msgstr ""
@@ -10887,6 +10904,9 @@ msgstr ""
msgid "Median"
msgstr ""
+msgid "Medium vulnerabilities present"
+msgstr ""
+
msgid "Member lock"
msgstr ""
@@ -11818,6 +11838,9 @@ msgstr ""
msgid "No vulnerabilities found for this project"
msgstr ""
+msgid "No vulnerabilities present"
+msgstr ""
+
msgid "No, directly import the existing email addresses and usernames."
msgstr ""
@@ -13575,6 +13598,12 @@ msgstr ""
msgid "Project path"
msgstr ""
+msgid "Project security status"
+msgstr ""
+
+msgid "Project security status help page"
+msgstr ""
+
msgid "Project slug"
msgstr ""
@@ -13944,6 +13973,9 @@ msgstr ""
msgid "Projects Successfully Retrieved"
msgstr ""
+msgid "Projects are graded based on the highest severity vulnerability present"
+msgstr ""
+
msgid "Projects shared with %{group_name}"
msgstr ""
@@ -13953,6 +13985,21 @@ msgstr ""
msgid "Projects to index"
msgstr ""
+msgid "Projects with critical vulnerabilities"
+msgstr ""
+
+msgid "Projects with high or unknown vulnerabilities"
+msgstr ""
+
+msgid "Projects with low vulnerabilities"
+msgstr ""
+
+msgid "Projects with medium vulnerabilities"
+msgstr ""
+
+msgid "Projects with no vulnerabilities and security scanning enabled"
+msgstr ""
+
msgid "Projects with write access"
msgstr ""
@@ -18870,6 +18917,9 @@ msgstr ""
msgid "Type"
msgstr ""
+msgid "Type/State"
+msgstr ""
+
msgid "U2F Devices (%{length})"
msgstr ""
@@ -18903,6 +18953,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to fetch vulnerable projects"
+msgstr ""
+
msgid "Unable to generate new instance ID"
msgstr ""
@@ -21756,6 +21809,9 @@ msgstr ""
msgid "severity|Medium"
msgstr ""
+msgid "severity|None"
+msgstr ""
+
msgid "severity|Undefined"
msgstr ""
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index ac13c0f2d9e..3e0a894e72e 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -11,7 +11,6 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
before do
stub_feature_flags(ci_enable_live_trace: true)
- stub_feature_flags(job_log_json: false)
stub_not_protect_default_branch
end
@@ -527,7 +526,6 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
describe 'GET trace.json' do
before do
- stub_feature_flags(job_log_json: true)
get_trace
end
@@ -634,6 +632,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
describe 'GET legacy trace.json' do
before do
+ stub_feature_flags(job_log_json: false)
get_trace
end
diff --git a/spec/features/projects/jobs/permissions_spec.rb b/spec/features/projects/jobs/permissions_spec.rb
index ae506b66a86..d78cf674dc6 100644
--- a/spec/features/projects/jobs/permissions_spec.rb
+++ b/spec/features/projects/jobs/permissions_spec.rb
@@ -10,7 +10,6 @@ describe 'Project Jobs Permissions' do
let!(:job) { create(:ci_build, :running, :coverage, :trace_artifact, pipeline: pipeline) }
before do
- stub_feature_flags(job_log_json: true)
sign_in(user)
project.enable_ci
diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb
index 856c39df8b3..16ba1c1b73d 100644
--- a/spec/features/projects/jobs/user_browses_job_spec.rb
+++ b/spec/features/projects/jobs/user_browses_job_spec.rb
@@ -10,8 +10,6 @@ describe 'User browses a job', :js do
let!(:build) { create(:ci_build, :success, :trace_artifact, :coverage, pipeline: pipeline) }
before do
- stub_feature_flags(job_log_json: false)
-
project.add_maintainer(user)
project.enable_ci
@@ -24,7 +22,7 @@ describe 'User browses a job', :js do
wait_for_requests
expect(page).to have_content("Job ##{build.id}")
- expect(page).to have_css('.js-build-trace')
+ expect(page).to have_css('.job-log')
# scroll to the top of the page first
execute_script "window.scrollTo(0,0)"
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index c9568dbb7ce..26ba7ae7a29 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -22,7 +22,6 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
before do
project.add_role(user, user_access_level)
sign_in(user)
- stub_feature_flags(job_log_json: false)
end
describe "GET /:project/jobs" do
@@ -810,7 +809,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'renders job log' do
wait_for_all_requests
- expect(page).to have_selector('.js-build-trace')
+ expect(page).to have_selector('.job-log')
end
end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index e2ecf1e3b7e..20a320e5b92 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -7,10 +7,6 @@ describe "Internal Project Access" do
set(:project) { create(:project, :internal, :repository) }
- before do
- stub_feature_flags(job_log_json: false)
- end
-
describe "Project should be internal" do
describe '#internal?' do
subject { project.internal? }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index f692fa3f8ee..62f9a96305d 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -7,10 +7,6 @@ describe "Private Project Access" do
set(:project) { create(:project, :private, :repository, public_builds: false) }
- before do
- stub_feature_flags(job_log_json: false)
- end
-
describe "Project should be private" do
describe '#private?' do
subject { project.private? }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 1bb9f766719..317c7bae084 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -7,10 +7,6 @@ describe "Public Project Access" do
set(:project) { create(:project, :public, :repository) }
- before do
- stub_feature_flags(job_log_json: false)
- end
-
describe "Project should be public" do
describe '#public?' do
subject { project.public? }
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 8244acbceea..97f7f05cd85 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -298,4 +298,28 @@ describe('URL utility', () => {
expect(urlUtils.objectToQuery(searchQueryObject)).toEqual('one=1&two=2');
});
});
+
+ describe('joinPaths', () => {
+ it.each`
+ paths | expected
+ ${['foo', 'bar']} | ${'foo/bar'}
+ ${['foo/', 'bar']} | ${'foo/bar'}
+ ${['foo//', 'bar']} | ${'foo/bar'}
+ ${['abc/', '/def']} | ${'abc/def'}
+ ${['foo', '/bar']} | ${'foo/bar'}
+ ${['foo', '/bar/']} | ${'foo/bar/'}
+ ${['foo', '//bar/']} | ${'foo/bar/'}
+ ${['foo', '', '/bar']} | ${'foo/bar'}
+ ${['foo', '/bar', '']} | ${'foo/bar'}
+ ${['/', '', 'foo/bar/ ', '', '/ninja']} | ${'/foo/bar/ /ninja'}
+ ${['', '/ninja', '/', ' ', '', 'bar', ' ']} | ${'/ninja/ /bar/ '}
+ ${['http://something/bar/', 'foo']} | ${'http://something/bar/foo'}
+ ${['foo/bar', null, 'ninja', null]} | ${'foo/bar/ninja'}
+ ${[null, 'abc/def', 'zoo']} | ${'abc/def/zoo'}
+ ${['', '', '']} | ${''}
+ ${['///', '/', '//']} | ${'/'}
+ `('joins paths $paths => $expected', ({ paths, expected }) => {
+ expect(urlUtils.joinPaths(...paths)).toBe(expected);
+ });
+ });
});
diff --git a/spec/graphql/resolvers/base_resolver_spec.rb b/spec/graphql/resolvers/base_resolver_spec.rb
index a212bd07f35..0a21b2797ee 100644
--- a/spec/graphql/resolvers/base_resolver_spec.rb
+++ b/spec/graphql/resolvers/base_resolver_spec.rb
@@ -8,8 +8,12 @@ describe Resolvers::BaseResolver do
let(:resolver) do
Class.new(described_class) do
def resolve(**args)
+ process(object)
+
[args, args]
end
+
+ def process(obj); end
end
end
@@ -69,4 +73,26 @@ describe Resolvers::BaseResolver do
expect(field.to_graphql.complexity.call({}, { sort: 'foo', iids: [1, 2, 3] }, 1)).to eq 3
end
end
+
+ describe '#object' do
+ let_it_be(:user) { create(:user) }
+
+ it 'returns object' do
+ expect_next_instance_of(resolver) do |r|
+ expect(r).to receive(:process).with(user)
+ end
+
+ resolve(resolver, obj: user)
+ end
+
+ context 'when object is a presenter' do
+ it 'returns presented object' do
+ expect_next_instance_of(resolver) do |r|
+ expect(r).to receive(:process).with(user)
+ end
+
+ resolve(resolver, obj: UserPresenter.new(user))
+ end
+ end
+ end
end
diff --git a/spec/graphql/types/snippet_type_spec.rb b/spec/graphql/types/snippet_type_spec.rb
index 3c3250a5fa2..5524e7a415d 100644
--- a/spec/graphql/types/snippet_type_spec.rb
+++ b/spec/graphql/types/snippet_type_spec.rb
@@ -6,7 +6,7 @@ describe GitlabSchema.types['Snippet'] do
it 'has the correct fields' do
expected_fields = [:id, :title, :project, :author,
:file_name, :content, :description,
- :visibility, :created_at, :updated_at,
+ :visibility_level, :created_at, :updated_at,
:web_url, :raw_url, :notes, :discussions,
:user_permissions, :description_html]
diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
new file mode 100644
index 00000000000..9ef45c0f6bc
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Creating a Snippet' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let(:content) { 'Initial content' }
+ let(:description) { 'Initial description' }
+ let(:title) { 'Initial title' }
+ let(:file_name) { 'Initial file_name' }
+ let(:visibility_level) { 'public' }
+ let(:project_path) { nil }
+
+ let(:mutation) do
+ variables = {
+ content: content,
+ description: description,
+ visibility_level: visibility_level,
+ file_name: file_name,
+ title: title,
+ project_path: project_path
+ }
+
+ graphql_mutation(:create_snippet, variables)
+ end
+
+ def mutation_response
+ graphql_mutation_response(:create_snippet)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { nil }
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+
+ it 'does not create the Snippet' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.not_to change { Snippet.count }
+ end
+
+ context 'when user is not authorized in the project' do
+ let(:project_path) { project.full_path }
+
+ it 'does not create the snippet when the user is not authorized' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.not_to change { Snippet.count }
+ end
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { user }
+
+ context 'with PersonalSnippet' do
+ it 'creates the Snippet' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change { Snippet.count }.by(1)
+ end
+
+ it 'returns the created Snippet' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['snippet']['content']).to eq(content)
+ expect(mutation_response['snippet']['title']).to eq(title)
+ expect(mutation_response['snippet']['description']).to eq(description)
+ expect(mutation_response['snippet']['fileName']).to eq(file_name)
+ expect(mutation_response['snippet']['visibilityLevel']).to eq(visibility_level)
+ expect(mutation_response['snippet']['project']).to be_nil
+ end
+ end
+
+ context 'with ProjectSnippet' do
+ let(:project_path) { project.full_path }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'creates the Snippet' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change { Snippet.count }.by(1)
+ end
+
+ it 'returns the created Snippet' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['snippet']['content']).to eq(content)
+ expect(mutation_response['snippet']['title']).to eq(title)
+ expect(mutation_response['snippet']['description']).to eq(description)
+ expect(mutation_response['snippet']['fileName']).to eq(file_name)
+ expect(mutation_response['snippet']['visibilityLevel']).to eq(visibility_level)
+ expect(mutation_response['snippet']['project']['fullPath']).to eq(project_path)
+ end
+
+ context 'when the project path is invalid' do
+ let(:project_path) { 'foobar' }
+
+ it 'returns an an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+ errors = json_response['errors']
+
+ expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
+ end
+ end
+
+ context 'when the feature is disabled' do
+ it 'returns an an error' do
+ project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::DISABLED)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+ errors = json_response['errors']
+
+ expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
+ end
+ end
+ end
+
+ context 'when there are ActiveRecord validation errors' do
+ let(:title) { '' }
+
+ it_behaves_like 'a mutation that returns errors in the response', errors: ["Title can't be blank"]
+
+ it 'does not create the Snippet' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.not_to change { Snippet.count }
+ end
+
+ it 'does not return Snippet' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['snippet']).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
new file mode 100644
index 00000000000..351d2db8973
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Destroying a Snippet' do
+ include GraphqlHelpers
+
+ let(:current_user) { snippet.author }
+ let(:mutation) do
+ variables = {
+ id: snippet.to_global_id.to_s
+ }
+
+ graphql_mutation(:destroy_snippet, variables)
+ end
+
+ def mutation_response
+ graphql_mutation_response(:destroy_snippet)
+ end
+
+ shared_examples 'graphql delete actions' do
+ context 'when the user does not have permission' do
+ let(:current_user) { create(:user) }
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+
+ it 'does not destroy the Snippet' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.not_to change { Snippet.count }
+ end
+ end
+
+ context 'when the user has permission' do
+ it 'destroys the Snippet' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change { Snippet.count }.by(-1)
+ end
+
+ it 'returns an empty Snippet' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response).to have_key('snippet')
+ expect(mutation_response['snippet']).to be_nil
+ end
+ end
+ end
+
+ describe 'PersonalSnippet' do
+ it_behaves_like 'graphql delete actions' do
+ let_it_be(:snippet) { create(:personal_snippet) }
+ end
+ end
+
+ describe 'ProjectSnippet' do
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:snippet) { create(:project_snippet, :private, project: project, author: create(:user)) }
+
+ context 'when the author is not a member of the project' do
+ it 'returns an an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+ errors = json_response['errors']
+
+ expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
+ end
+ end
+
+ context 'when the author is a member of the project' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'graphql delete actions'
+
+ context 'when the snippet project feature is disabled' do
+ it 'returns an an error' do
+ project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::DISABLED)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+ errors = json_response['errors']
+
+ expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
new file mode 100644
index 00000000000..deaa9e8a237
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Updating a Snippet' do
+ include GraphqlHelpers
+
+ let_it_be(:original_content) { 'Initial content' }
+ let_it_be(:original_description) { 'Initial description' }
+ let_it_be(:original_title) { 'Initial title' }
+ let_it_be(:original_file_name) { 'Initial file_name' }
+ let(:updated_content) { 'Updated content' }
+ let(:updated_description) { 'Updated description' }
+ let(:updated_title) { 'Updated_title' }
+ let(:updated_file_name) { 'Updated file_name' }
+ let(:current_user) { snippet.author }
+
+ let(:mutation) do
+ variables = {
+ id: GitlabSchema.id_from_object(snippet).to_s,
+ content: updated_content,
+ description: updated_description,
+ visibility_level: 'public',
+ file_name: updated_file_name,
+ title: updated_title
+ }
+
+ graphql_mutation(:update_snippet, variables)
+ end
+
+ def mutation_response
+ graphql_mutation_response(:update_snippet)
+ end
+
+ shared_examples 'graphql update actions' do
+ context 'when the user does not have permission' do
+ let(:current_user) { create(:user) }
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+
+ it 'does not update the Snippet' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.not_to change { snippet.reload }
+ end
+ end
+
+ context 'when the user has permission' do
+ it 'updates the Snippet' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(snippet.reload.title).to eq(updated_title)
+ end
+
+ it 'returns the updated Snippet' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['snippet']['content']).to eq(updated_content)
+ expect(mutation_response['snippet']['title']).to eq(updated_title)
+ expect(mutation_response['snippet']['description']).to eq(updated_description)
+ expect(mutation_response['snippet']['fileName']).to eq(updated_file_name)
+ expect(mutation_response['snippet']['visibilityLevel']).to eq('public')
+ end
+
+ context 'when there are ActiveRecord validation errors' do
+ let(:updated_title) { '' }
+
+ it_behaves_like 'a mutation that returns errors in the response', errors: ["Title can't be blank"]
+
+ it 'does not update the Snippet' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(snippet.reload.title).to eq(original_title)
+ end
+
+ it 'returns the Snippet with its original values' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['snippet']['content']).to eq(original_content)
+ expect(mutation_response['snippet']['title']).to eq(original_title)
+ expect(mutation_response['snippet']['description']).to eq(original_description)
+ expect(mutation_response['snippet']['fileName']).to eq(original_file_name)
+ expect(mutation_response['snippet']['visibilityLevel']).to eq('private')
+ end
+ end
+ end
+ end
+
+ describe 'PersonalSnippet' do
+ it_behaves_like 'graphql update actions' do
+ let_it_be(:snippet) do
+ create(:personal_snippet,
+ :private,
+ file_name: original_file_name,
+ title: original_title,
+ content: original_content,
+ description: original_description)
+ end
+ end
+ end
+
+ describe 'ProjectSnippet' do
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:snippet) do
+ create(:project_snippet,
+ :private,
+ project: project,
+ author: create(:user),
+ file_name: original_file_name,
+ title: original_title,
+ content: original_content,
+ description: original_description)
+ end
+
+ context 'when the author is not a member of the project' do
+ it 'returns an an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+ errors = json_response['errors']
+
+ expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
+ end
+ end
+
+ context 'when the author is a member of the project' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'graphql update actions'
+
+ context 'when the snippet project feature is disabled' do
+ it 'returns an an error' do
+ project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::DISABLED)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+ errors = json_response['errors']
+
+ expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
+ end
+ end
+ end
+ end
+end