summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarin Jankovski <marin@gitlab.com>2019-04-23 17:05:26 +0000
committerMarin Jankovski <marin@gitlab.com>2019-04-23 17:05:26 +0000
commit8907ef5116565cf6e6a01f0e9734faacda324585 (patch)
tree595a6b5bfca7afdef9bd0787e0685289006f97e2
parent8a802d1c6b7ee660fbf701b9758ccff85f065e68 (diff)
parent46eed98e81ec4c7e6058d916e703a2d749d66436 (diff)
downloadgitlab-ce-8907ef5116565cf6e6a01f0e9734faacda324585.tar.gz
Merge branch '11-10-stable-patch-1' into '11-10-stable'
Prepare 11.10.1 release See merge request gitlab-org/gitlab-ce!27570
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js3
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js7
-rw-r--r--app/controllers/clusters/clusters_controller.rb3
-rw-r--r--app/finders/issuable_finder.rb1
-rw-r--r--app/services/clusters/refresh_service.rb6
-rw-r--r--app/views/projects/issues/show.html.haml2
-rw-r--r--app/workers/cluster_configure_worker.rb6
-rw-r--r--app/workers/cluster_project_configure_worker.rb2
-rw-r--r--changelogs/unreleased/60500-disable-jit-kubernetes-resource-creation-for-project-level-clusters.yml5
-rw-r--r--changelogs/unreleased/60569-timeline-entry-label-link-is-not-applying-the-filter-on-issues.yml5
-rw-r--r--changelogs/unreleased/jc-upgrade-gitaly-1-34-0.yml5
-rw-r--r--changelogs/unreleased/sh-backport-list-commits-by-oid-rugged.yml5
-rw-r--r--changelogs/unreleased/sh-bump-ruby-required-version-check.yml5
-rw-r--r--doc/ci/merge_request_pipelines/index.md2
-rw-r--r--doc/ci/variables/README.md21
-rw-r--r--doc/development/gitaly.md2
-rw-r--r--doc/user/admin_area/settings/external_authorization.md112
-rw-r--r--doc/user/admin_area/settings/img/classification_label_on_project_page.pngbin0 -> 19568 bytes
-rw-r--r--doc/user/admin_area/settings/img/external_authorization_service_settings.pngbin0 -> 74753 bytes
-rw-r--r--doc/user/project/clusters/index.md4
-rw-r--r--lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb4
-rw-r--r--lib/gitlab/git/rugged_impl/commit.rb20
-rw-r--r--lib/gitlab/git/rugged_impl/repository.rb2
-rw-r--r--lib/gitlab/graphql/authorize.rb2
-rw-r--r--lib/gitlab/graphql/authorize/authorize_field_service.rb86
-rw-r--r--lib/gitlab/graphql/connections/keyset_connection.rb16
-rw-r--r--lib/gitlab/graphql/errors.rb1
-rw-r--r--lib/system_check/app/ruby_version_check.rb2
-rw-r--r--spec/controllers/concerns/issuable_collections_spec.rb110
-rw-r--r--spec/graphql/features/authorization_spec.rb77
-rw-r--r--spec/graphql/gitlab_schema_spec.rb2
-rw-r--r--spec/javascripts/monitoring/monitoring_store_spec.js24
-rw-r--r--spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb10
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb37
-rw-r--r--spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb130
-rw-r--r--spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb5
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb28
-rw-r--r--spec/services/clusters/refresh_service_spec.rb26
-rw-r--r--spec/workers/cluster_configure_worker_spec.rb67
-rw-r--r--spec/workers/cluster_project_configure_worker_spec.rb15
41 files changed, 620 insertions, 242 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 7aa332e4163..2b17ffd5042 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.33.0
+1.34.0
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index 2b4ddd7afbc..ed794779ff2 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import Dashboard from './components/dashboard.vue';
-export default () => {
+export default (props = {}) => {
const el = document.getElementById('prometheus-graphs');
if (el && el.dataset) {
@@ -15,6 +15,7 @@ export default () => {
...el.dataset,
hasMetrics: parseBoolean(el.dataset.hasMetrics),
showTimeWindowDropdown: gon.features.metricsTimeWindow,
+ ...props,
},
});
},
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 9761fe168be..013fb0d4540 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -45,14 +45,13 @@ function removeTimeSeriesNoData(queries) {
// ]
function groupQueriesByChartInfo(metrics) {
const metricsByChart = metrics.reduce((accumulator, metric) => {
- const { id, queries, ...chart } = metric;
+ const { queries, ...chart } = metric;
+ const metricId = chart.id ? chart.id.toString() : null;
const chartKey = `${chart.title}|${chart.y_label}`;
accumulator[chartKey] = accumulator[chartKey] || { ...chart, queries: [] };
- queries.forEach(queryAttrs =>
- accumulator[chartKey].queries.push({ metricId: id.toString(), ...queryAttrs }),
- );
+ queries.forEach(queryAttrs => accumulator[chartKey].queries.push({ metricId, ...queryAttrs }));
return accumulator;
}, {});
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index e82756e4643..edaf07063ec 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -12,6 +12,9 @@ class Clusters::ClustersController < Clusters::BaseController
before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy]
before_action :update_applications_status, only: [:cluster_status]
+ before_action only: [:show] do
+ push_frontend_feature_flag(:metrics_time_window)
+ end
helper_method :token_in_session
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 64c88505a16..fa9dda2ab31 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -53,6 +53,7 @@ class IssuableFinder
assignee_username
author_id
author_username
+ label_name
milestone_title
my_reaction_emoji
search
diff --git a/app/services/clusters/refresh_service.rb b/app/services/clusters/refresh_service.rb
index 7c82b98a33f..76ad8dd0fb0 100644
--- a/app/services/clusters/refresh_service.rb
+++ b/app/services/clusters/refresh_service.rb
@@ -21,7 +21,11 @@ module Clusters
private_class_method :projects_with_missing_kubernetes_namespaces_for_cluster
def self.clusters_with_missing_kubernetes_namespaces_for_project(project)
- project.all_clusters.missing_kubernetes_namespace(project.kubernetes_namespaces)
+ if Feature.enabled?(:ci_preparing_state, default_enabled: true)
+ project.clusters.missing_kubernetes_namespace(project.kubernetes_namespaces)
+ else
+ project.all_clusters.missing_kubernetes_namespace(project.kubernetes_namespaces)
+ end
end
private_class_method :clusters_with_missing_kubernetes_namespaces_for_project
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 2c95ac6dbb3..4bf1d8702af 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -77,7 +77,7 @@
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
- #js-related-merge-requests{ data: { endpoint: api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid), project_namespace: @project.namespace.path, project_path: @project.path } }
+ #js-related-merge-requests{ data: { endpoint: expose_url(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } }
- if can?(current_user, :download_code, @project)
#related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } }
diff --git a/app/workers/cluster_configure_worker.rb b/app/workers/cluster_configure_worker.rb
index b984dee5b21..22681157b62 100644
--- a/app/workers/cluster_configure_worker.rb
+++ b/app/workers/cluster_configure_worker.rb
@@ -5,10 +5,10 @@ class ClusterConfigureWorker
include ClusterQueue
def perform(cluster_id)
- return if Feature.enabled?(:ci_preparing_state, default_enabled: true)
-
Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
- Clusters::RefreshService.create_or_update_namespaces_for_cluster(cluster)
+ if cluster.project_type? || Feature.disabled?(:ci_preparing_state, default_enabled: true)
+ Clusters::RefreshService.create_or_update_namespaces_for_cluster(cluster)
+ end
end
end
end
diff --git a/app/workers/cluster_project_configure_worker.rb b/app/workers/cluster_project_configure_worker.rb
index d7bea69a01c..497e57c0d0b 100644
--- a/app/workers/cluster_project_configure_worker.rb
+++ b/app/workers/cluster_project_configure_worker.rb
@@ -5,8 +5,6 @@ class ClusterProjectConfigureWorker
include ClusterQueue
def perform(project_id)
- return if Feature.enabled?(:ci_preparing_state, default_enabled: true)
-
project = Project.find(project_id)
::Clusters::RefreshService.create_or_update_namespaces_for_project(project)
diff --git a/changelogs/unreleased/60500-disable-jit-kubernetes-resource-creation-for-project-level-clusters.yml b/changelogs/unreleased/60500-disable-jit-kubernetes-resource-creation-for-project-level-clusters.yml
new file mode 100644
index 00000000000..df6e6ea4be3
--- /dev/null
+++ b/changelogs/unreleased/60500-disable-jit-kubernetes-resource-creation-for-project-level-clusters.yml
@@ -0,0 +1,5 @@
+---
+title: Disable just-in-time Kubernetes resource creation for project level clusters
+merge_request: 27352
+author:
+type: changed
diff --git a/changelogs/unreleased/60569-timeline-entry-label-link-is-not-applying-the-filter-on-issues.yml b/changelogs/unreleased/60569-timeline-entry-label-link-is-not-applying-the-filter-on-issues.yml
new file mode 100644
index 00000000000..5319373ec4b
--- /dev/null
+++ b/changelogs/unreleased/60569-timeline-entry-label-link-is-not-applying-the-filter-on-issues.yml
@@ -0,0 +1,5 @@
+---
+title: Fix filtering of labels from system note link
+merge_request: 27507
+author:
+type: fixed
diff --git a/changelogs/unreleased/jc-upgrade-gitaly-1-34-0.yml b/changelogs/unreleased/jc-upgrade-gitaly-1-34-0.yml
new file mode 100644
index 00000000000..c1669a484aa
--- /dev/null
+++ b/changelogs/unreleased/jc-upgrade-gitaly-1-34-0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade Gitaly to 1.34.0
+merge_request: 27494
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-backport-list-commits-by-oid-rugged.yml b/changelogs/unreleased/sh-backport-list-commits-by-oid-rugged.yml
new file mode 100644
index 00000000000..eb8774d652f
--- /dev/null
+++ b/changelogs/unreleased/sh-backport-list-commits-by-oid-rugged.yml
@@ -0,0 +1,5 @@
+---
+title: Bring back Rugged implementation of ListCommitsByOid
+merge_request: 27441
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-bump-ruby-required-version-check.yml b/changelogs/unreleased/sh-bump-ruby-required-version-check.yml
new file mode 100644
index 00000000000..b5b6eb87650
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-ruby-required-version-check.yml
@@ -0,0 +1,5 @@
+---
+title: Bump required Ruby version check to 2.5.3
+merge_request: 27495
+author:
+type: other
diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md
index 2de751c9e62..6a03ab910fc 100644
--- a/doc/ci/merge_request_pipelines/index.md
+++ b/doc/ci/merge_request_pipelines/index.md
@@ -69,7 +69,7 @@ when a merge request was created or updated. For example:
## Combined ref pipelines **[PREMIUM]**
-> [GitLab Premium](https://about.gitlab.com/pricing/) 11.10.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7380) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.10.
It's possible for your source and target branches to diverge, which can result
in the scenario that source branch's pipeline was green, the target's pipeline was green,
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 592fdfd2873..2832bf80ccf 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -137,12 +137,21 @@ The output will be:
![Output custom variable](img/custom_variable_output.png)
-CAUTION: **Important:**
-Be aware that variables are not masked, and their values can be shown
-in the job logs if explicitly asked to do so. If your project is public or
-internal, you can set the pipelines private from your [project's Pipelines
-settings](../../user/project/pipelines/settings.md#visibility-of-pipelines).
-Follow the discussion in issue [#13784][ce-13784] for masking the variables.
+### Masked Variables
+
+By default, variables will be created as masked variables.
+This means that the value of the variable will be hidden in job logs,
+though it must match certain requirements to do so:
+
+- The value must be a single line.
+- The value must not have escape characters.
+- The value must not use variables.
+- The value must not have any whitespace.
+- The value must be at least 8 characters long.
+
+If the value does not meet the requirements above, then the CI variable will fail to save.
+In order to save, either alter the value to meet the masking requirements
+or disable `Masked` for the variable.
### Syntax of environment variables in job scripts
{: #syntax-of-variables-in-job-scripts}
diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md
index 27b69ba8278..94d0c9b69a6 100644
--- a/doc/development/gitaly.md
+++ b/doc/development/gitaly.md
@@ -80,6 +80,8 @@ most commonly-used RPCs can be enabled via feature flags:
* `rugged_get_tree_entries`
* `rugged_tree_entry`
* `rugged_commit_is_ancestor`
+* `rugged_commit_tree_entry`
+* `rugged_list_commits_by_oid`
A convenience Rake task can be used to enable or disable these flags
all together. To enable:
diff --git a/doc/user/admin_area/settings/external_authorization.md b/doc/user/admin_area/settings/external_authorization.md
new file mode 100644
index 00000000000..72e76ac2a84
--- /dev/null
+++ b/doc/user/admin_area/settings/external_authorization.md
@@ -0,0 +1,112 @@
+# External authorization control
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/4216) in
+> [GitLab Premium](https://about.gitlab.com/pricing) 10.6.
+> [Moved](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/27056) to
+> [GitLab Core](https://about.gitlab.com/pricing/) in 11.10.
+
+In highly controlled environments, it may be necessary for access policy to be
+controlled by an external service that permits access based on project
+classification and user access. GitLab provides a way to check project
+authorization with your own defined service.
+
+## Overview
+
+Once the external service is configured and enabled, when a project is accessed,
+a request is made to the external service with the user information and project
+classification label assigned to the project. When the service replies with a
+known response, the result is cached for 6 hours.
+
+If the external authorization is enabled, GitLab will further block pages and
+functionality that render cross-project data. That includes:
+
+- most pages under Dashboard (Activity, Milestones, Snippets, Assigned merge
+ requests, Assigned issues, Todos)
+- under a specific group (Activity, Contribution analytics, Issues, Issue boards,
+ Labels, Milestones, Merge requests)
+- Global and Group search will be disabled
+
+This is to prevent performing to many requests at once to the external
+authorization service.
+
+Whenever access is granted or denied this is logged in a logfile called
+`external-policy-access-control.log`.
+Read more about logs GitLab keeps in the [omnibus documentation][omnibus-log-docs].
+
+## Configuration
+
+The external authorization service can be enabled by an admin on the GitLab's
+admin area under the settings page:
+
+![Enable external authorization service](img/external_authorization_service_settings.png)
+
+The available required properties are:
+
+- **Service URL**: The URL to make authorization requests to. When leaving the
+ URL blank, cross project features will remain available while still being able
+ to specify classification labels for projects.
+- **External authorization request timeout**: The timeout after which an
+ authorization request is aborted. When a request times out, access is denied
+ to the user.
+- **Client authentication certificate**: The certificate to use to authenticate
+ with the external authorization service.
+- **Client authentication key**: Private key for the certificate when
+ authentication is required for the external authorization service, this is
+ encrypted when stored.
+- **Client authentication key password**: Passphrase to use for the private key when authenticating with the external service this is encrypted when stored.
+- **Default classification label**: The classification label to use when
+ requesting authorization if no specific label is defined on the project
+
+When using TLS Authentication with a self signed certificate, the CA certificate
+needs to be trused by the openssl installation. When using GitLab installed using
+Omnibus, learn to install a custom CA in the
+[omnibus documentation][omnibus-ssl-docs]. Alternatively learn where to install
+custom certificates using `openssl version -d`.
+
+## How it works
+
+When GitLab requests access, it will send a JSON POST request to the external
+service with this body:
+
+```json
+{
+ "user_identifier": "jane@acme.org",
+ "project_classification_label": "project-label",
+ "user_ldap_dn": "CN=Jane Doe,CN=admin,DC=acme"
+}
+```
+
+The `user_ldap_dn` is optional and is only sent when the user is logged in
+through LDAP.
+
+When the external authorization service responds with a status code 200, the
+user is granted access. When the external service responds with a status code
+401 or 403, the user is denied access. In any case, the request is cached for 6 hours.
+
+When denying access, a `reason` can be optionally specified in the JSON body:
+
+```json
+{
+ "reason": "You are not allowed access to this project."
+}
+```
+
+Any other status code than 200, 401 or 403 will also deny access to the user, but the
+response will not be cached.
+
+If the service times out (after 500ms), a message "External Policy Server did
+not respond" will be displayed.
+
+## Classification labels
+
+You can use your own classification label in the project's
+**Settings > General > General project settings** page in the "Classification
+label" box. When no classification label is specified on a project, the default
+label defined in the [global settings](#configuration) will be used.
+
+The label will be shown on all project pages in the upper right corner.
+
+![classification label on project page](img/classification_label_on_project_page.png)
+
+[omnibus-ssl-docs]: https://docs.gitlab.com/omnibus/settings/ssl.html
+[omnibus-log-docs]: https://docs.gitlab.com/omnibus/settings/logs.html
diff --git a/doc/user/admin_area/settings/img/classification_label_on_project_page.png b/doc/user/admin_area/settings/img/classification_label_on_project_page.png
new file mode 100644
index 00000000000..4aedb332cec
--- /dev/null
+++ b/doc/user/admin_area/settings/img/classification_label_on_project_page.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/external_authorization_service_settings.png b/doc/user/admin_area/settings/img/external_authorization_service_settings.png
new file mode 100644
index 00000000000..9b8658fd1a1
--- /dev/null
+++ b/doc/user/admin_area/settings/img/external_authorization_service_settings.png
Binary files differ
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 878d30dddaa..3f673ebb35a 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -565,7 +565,9 @@ service account of the cluster integration.
### Troubleshooting failed deployment jobs
GitLab will create a namespace and service account specifically for your
-deployment jobs, immediately before the jobs starts.
+deployment jobs. On project level clusters, this happens when the cluster
+is created. On group level clusters, resources are created immediately
+before the deployment job starts.
However, sometimes GitLab can not create them. In such instances, your job will fail with the message:
diff --git a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb
index 41135ae62bb..bb2b209e793 100644
--- a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb
+++ b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb
@@ -6,7 +6,9 @@ module Gitlab
module Prerequisite
class KubernetesNamespace < Base
def unmet?
- deployment_cluster.present? && kubernetes_namespace.new_record?
+ deployment_cluster.present? &&
+ !deployment_cluster.project_type? &&
+ kubernetes_namespace.new_record?
end
def complete!
diff --git a/lib/gitlab/git/rugged_impl/commit.rb b/lib/gitlab/git/rugged_impl/commit.rb
index f6777dfa0c3..bce4fa14fb4 100644
--- a/lib/gitlab/git/rugged_impl/commit.rb
+++ b/lib/gitlab/git/rugged_impl/commit.rb
@@ -21,6 +21,17 @@ module Gitlab
nil
end
+ # This needs to return an array of Gitlab::Git:Commit objects
+ # instead of Rugged::Commit objects to ensure upstream models
+ # operate on a consistent interface. Unlike
+ # Gitlab::Git::Commit.find, Gitlab::Git::Commit.batch_by_oid
+ # doesn't attempt to decorate the result.
+ def rugged_batch_by_oid(repo, oids)
+ oids.map { |oid| rugged_find(repo, oid) }
+ .compact
+ .map { |commit| decorate(repo, commit) }
+ end
+
override :find_commit
def find_commit(repo, commit_id)
if Feature.enabled?(:rugged_find_commit)
@@ -29,6 +40,15 @@ module Gitlab
super
end
end
+
+ override :batch_by_oid
+ def batch_by_oid(repo, oids)
+ if Feature.enabled?(:rugged_list_commits_by_oid)
+ rugged_batch_by_oid(repo, oids)
+ else
+ super
+ end
+ end
end
extend ::Gitlab::Utils::Override
diff --git a/lib/gitlab/git/rugged_impl/repository.rb b/lib/gitlab/git/rugged_impl/repository.rb
index c0a91f59ab9..e91b0ddcd31 100644
--- a/lib/gitlab/git/rugged_impl/repository.rb
+++ b/lib/gitlab/git/rugged_impl/repository.rb
@@ -12,7 +12,7 @@ module Gitlab
module Repository
extend ::Gitlab::Utils::Override
- FEATURE_FLAGS = %i(rugged_find_commit rugged_tree_entries rugged_tree_entry rugged_commit_is_ancestor rugged_commit_tree_entry).freeze
+ FEATURE_FLAGS = %i(rugged_find_commit rugged_tree_entries rugged_tree_entry rugged_commit_is_ancestor rugged_commit_tree_entry rugged_list_commits_by_oid).freeze
def alternate_object_directories
relative_object_directories.map { |d| File.join(path, d) }
diff --git a/lib/gitlab/graphql/authorize.rb b/lib/gitlab/graphql/authorize.rb
index f62813db82c..f8d0208e275 100644
--- a/lib/gitlab/graphql/authorize.rb
+++ b/lib/gitlab/graphql/authorize.rb
@@ -8,7 +8,7 @@ module Gitlab
extend ActiveSupport::Concern
def self.use(schema_definition)
- schema_definition.instrument(:field, Instrumentation.new)
+ schema_definition.instrument(:field, Instrumentation.new, after_built_ins: true)
end
end
end
diff --git a/lib/gitlab/graphql/authorize/authorize_field_service.rb b/lib/gitlab/graphql/authorize/authorize_field_service.rb
index 8deff79fc84..03d6aabb0e3 100644
--- a/lib/gitlab/graphql/authorize/authorize_field_service.rb
+++ b/lib/gitlab/graphql/authorize/authorize_field_service.rb
@@ -15,15 +15,10 @@ module Gitlab
def authorized_resolve
proc do |parent_typed_object, args, ctx|
- resolved_obj = @old_resolve_proc.call(parent_typed_object, args, ctx)
- authorizing_obj = authorize_against(parent_typed_object)
- checker = build_checker(ctx[:current_user], authorizing_obj)
-
- if resolved_obj.respond_to?(:then)
- resolved_obj.then(&checker)
- else
- checker.call(resolved_obj)
- end
+ resolved_type = @old_resolve_proc.call(parent_typed_object, args, ctx)
+ authorizing_object = authorize_against(parent_typed_object, resolved_type)
+
+ filter_allowed(ctx[:current_user], resolved_type, authorizing_object)
end
end
@@ -38,7 +33,7 @@ module Gitlab
type = @field.type
# When the return type of @field is a collection, find the singular type
- if type.get_field('edges')
+ if @field.connection?
type = node_type_for_relay_connection(type)
elsif type.list?
type = node_type_for_basic_connection(type)
@@ -52,43 +47,60 @@ module Gitlab
Array.wrap(@field.metadata[:authorize])
end
- # If it's a built-in/scalar type, authorize using its parent object.
- # nil means authorize using the resolved object
- def authorize_against(parent_typed_object)
- parent_typed_object.object if built_in_type? && parent_typed_object.respond_to?(:object)
+ def authorize_against(parent_typed_object, resolved_type)
+ if built_in_type?
+ # The field is a built-in/scalar type, or a list of scalars
+ # authorize using the parent's object
+ parent_typed_object.object
+ elsif resolved_type.respond_to?(:object)
+ # The field is a type representing a single object, we'll authorize
+ # against the object directly
+ resolved_type.object
+ elsif @field.connection? || resolved_type.is_a?(Array)
+ # The field is a connection or a list of non-built-in types, we'll
+ # authorize each element when rendering
+ nil
+ else
+ # Resolved type is a single object that might not be loaded yet by
+ # the batchloader, we'll authorize that
+ resolved_type
+ end
end
- def build_checker(current_user, authorizing_obj)
- lambda do |resolved_obj|
- # Load the elements if they were not loaded by BatchLoader yet
- resolved_obj = resolved_obj.sync if resolved_obj.respond_to?(:sync)
-
- check = lambda do |object|
- authorizations.all? do |ability|
- Ability.allowed?(current_user, ability, authorizing_obj || object)
- end
+ def filter_allowed(current_user, resolved_type, authorizing_object)
+ if authorizing_object
+ # Authorizing fields representing scalars, or a simple field with an object
+ resolved_type if allowed_access?(current_user, authorizing_object)
+ elsif @field.connection?
+ # A connection with pagination, modify the visible nodes in on the
+ # connection type in place
+ resolved_type.edge_nodes.to_a.keep_if { |node| allowed_access?(current_user, node) }
+ resolved_type
+ elsif resolved_type.is_a? Array
+ # A simple list of rendered types each object being an object to authorize
+ resolved_type.select do |single_object_type|
+ allowed_access?(current_user, single_object_type.object)
end
+ elsif resolved_type.nil?
+ # We're not rendering anything, for example when a record was not found
+ # no need to do anything
+ else
+ raise "Can't authorize #{@field}"
+ end
+ end
- case resolved_obj
- when Array, ActiveRecord::Relation
- resolved_obj.select(&check)
- else
- resolved_obj if check.call(resolved_obj)
- end
+ def allowed_access?(current_user, object)
+ object = object.sync if object.respond_to?(:sync)
+
+ authorizations.all? do |ability|
+ Ability.allowed?(current_user, ability, object)
end
end
# Returns the singular type for relay connections.
# This will be the type class of edges.node
def node_type_for_relay_connection(type)
- type = type.get_field('edges').type.unwrap.get_field('node')&.type
-
- if type.nil?
- raise Gitlab::Graphql::Errors::ConnectionDefinitionError,
- 'Connection Type must conform to the Relay Cursor Connections Specification'
- end
-
- type
+ type.unwrap.get_field('edges').type.unwrap.get_field('node').type
end
# Returns the singular type for basic connections, for example `[Types::ProjectType]`
diff --git a/lib/gitlab/graphql/connections/keyset_connection.rb b/lib/gitlab/graphql/connections/keyset_connection.rb
index 851054c0393..715963a44c1 100644
--- a/lib/gitlab/graphql/connections/keyset_connection.rb
+++ b/lib/gitlab/graphql/connections/keyset_connection.rb
@@ -22,8 +22,17 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def paged_nodes
+ # These are the nodes that will be loaded into memory for rendering
+ # So we're ok loading them into memory here as that's bound to happen
+ # anyway. Having them ready means we can modify the result while
+ # rendering the fields.
+ @paged_nodes ||= load_paged_nodes.to_a
+ end
+
+ private
+
+ def load_paged_nodes
if first && last
raise Gitlab::Graphql::Errors::ArgumentError.new("Can only provide either `first` or `last`, not both")
end
@@ -31,12 +40,9 @@ module Gitlab
if last
sliced_nodes.last(limit_value)
else
- sliced_nodes.limit(limit_value)
+ sliced_nodes.limit(limit_value) # rubocop: disable CodeReuse/ActiveRecord
end
end
- # rubocop: enable CodeReuse/ActiveRecord
-
- private
def before_slice
if sort_direction == :asc
diff --git a/lib/gitlab/graphql/errors.rb b/lib/gitlab/graphql/errors.rb
index bcbba72e017..fe74549e322 100644
--- a/lib/gitlab/graphql/errors.rb
+++ b/lib/gitlab/graphql/errors.rb
@@ -6,7 +6,6 @@ module Gitlab
BaseError = Class.new(GraphQL::ExecutionError)
ArgumentError = Class.new(BaseError)
ResourceNotAvailable = Class.new(BaseError)
- ConnectionDefinitionError = Class.new(BaseError)
end
end
end
diff --git a/lib/system_check/app/ruby_version_check.rb b/lib/system_check/app/ruby_version_check.rb
index 60e07718338..53da62df176 100644
--- a/lib/system_check/app/ruby_version_check.rb
+++ b/lib/system_check/app/ruby_version_check.rb
@@ -7,7 +7,7 @@ module SystemCheck
set_check_pass -> { "yes (#{self.current_version})" }
def self.required_version
- @required_version ||= Gitlab::VersionInfo.new(2, 3, 5)
+ @required_version ||= Gitlab::VersionInfo.new(2, 5, 3)
end
def self.current_version
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb
index a82b66361ca..f098ec2e167 100644
--- a/spec/controllers/concerns/issuable_collections_spec.rb
+++ b/spec/controllers/concerns/issuable_collections_spec.rb
@@ -106,51 +106,77 @@ describe IssuableCollections do
end
describe '#finder_options' do
- let(:params) do
- {
- assignee_id: '1',
- assignee_username: 'user1',
- author_id: '2',
- author_username: 'user2',
- authorized_only: 'yes',
- confidential: true,
- due_date: '2017-01-01',
- group_id: '3',
- iids: '4',
- label_name: ['foo'],
- milestone_title: 'bar',
- my_reaction_emoji: 'thumbsup',
- non_archived: 'true',
- project_id: '5',
- scope: 'all',
- search: 'baz',
- sort: 'priority',
- state: 'opened',
- invalid_param: 'invalid_param'
- }
- end
-
- it 'only allows whitelisted params' do
+ before do
allow(controller).to receive(:cookies).and_return({})
allow(controller).to receive(:current_user).and_return(nil)
+ end
+
+ subject { controller.send(:finder_options).to_h }
+
+ context 'scalar params' do
+ let(:params) do
+ {
+ assignee_id: '1',
+ assignee_username: 'user1',
+ author_id: '2',
+ author_username: 'user2',
+ authorized_only: 'yes',
+ confidential: true,
+ due_date: '2017-01-01',
+ group_id: '3',
+ iids: '4',
+ label_name: 'foo',
+ milestone_title: 'bar',
+ my_reaction_emoji: 'thumbsup',
+ non_archived: 'true',
+ project_id: '5',
+ scope: 'all',
+ search: 'baz',
+ sort: 'priority',
+ state: 'opened',
+ invalid_param: 'invalid_param'
+ }
+ end
+
+ it 'only allows whitelisted params' do
+ is_expected.to include({
+ 'assignee_id' => '1',
+ 'assignee_username' => 'user1',
+ 'author_id' => '2',
+ 'author_username' => 'user2',
+ 'confidential' => true,
+ 'label_name' => 'foo',
+ 'milestone_title' => 'bar',
+ 'my_reaction_emoji' => 'thumbsup',
+ 'due_date' => '2017-01-01',
+ 'scope' => 'all',
+ 'search' => 'baz',
+ 'sort' => 'priority',
+ 'state' => 'opened'
+ })
+
+ is_expected.not_to include('invalid_param')
+ end
+ end
+
+ context 'array params' do
+ let(:params) do
+ {
+ assignee_username: %w[user1 user2],
+ label_name: %w[label1 label2],
+ invalid_param: 'invalid_param',
+ invalid_array: ['param']
+ }
+ end
+
+ it 'only allows whitelisted params' do
+ is_expected.to include({
+ 'label_name' => %w[label1 label2],
+ 'assignee_username' => %w[user1 user2]
+ })
- finder_options = controller.send(:finder_options)
-
- expect(finder_options).to eq(ActionController::Parameters.new({
- 'assignee_id' => '1',
- 'assignee_username' => 'user1',
- 'author_id' => '2',
- 'author_username' => 'user2',
- 'confidential' => true,
- 'label_name' => ['foo'],
- 'milestone_title' => 'bar',
- 'my_reaction_emoji' => 'thumbsup',
- 'due_date' => '2017-01-01',
- 'scope' => 'all',
- 'search' => 'baz',
- 'sort' => 'priority',
- 'state' => 'opened'
- }).permit!)
+ is_expected.not_to include('invalid_param', 'invalid_array')
+ end
end
end
end
diff --git a/spec/graphql/features/authorization_spec.rb b/spec/graphql/features/authorization_spec.rb
index 00e31568a9e..f5eb628a982 100644
--- a/spec/graphql/features/authorization_spec.rb
+++ b/spec/graphql/features/authorization_spec.rb
@@ -177,6 +177,7 @@ describe 'Gitlab::Graphql::Authorization' do
describe 'type authorizations when applied to a relay connection' do
let(:query_string) { '{ object() { edges { node { name } } } }' }
+ let(:second_test_object) { double(name: 'Second thing') }
let(:type) do
type_factory do |type|
@@ -186,22 +187,41 @@ describe 'Gitlab::Graphql::Authorization' do
let(:query_type) do
query_factory do |query|
- query.field :object, type.connection_type, null: true, resolve: ->(obj, args, ctx) { [test_object] }
+ query.field :object, type.connection_type, null: true, resolve: ->(obj, args, ctx) { [test_object, second_test_object] }
end
end
subject { result.dig('object', 'edges') }
- it 'returns the protected field when user has permission' do
+ it 'returns only the elements visible to the user' do
permit(permission_single)
- expect(subject).not_to be_empty
+ expect(subject.size).to eq 1
expect(subject.first['node']).to eq('name' => test_object.name)
end
it 'returns nil when user is not authorized' do
expect(subject).to be_empty
end
+
+ describe 'limiting connections with multiple objects' do
+ let(:query_type) do
+ query_factory do |query|
+ query.field :object, type.connection_type, null: true, resolve: ->(obj, args, ctx) do
+ [test_object, second_test_object]
+ end
+ end
+ end
+
+ let(:query_string) { '{ object(first: 1) { edges { node { name } } } }' }
+
+ it 'only checks permissions for the first object' do
+ expect(Ability).to receive(:allowed?).with(user, permission_single, test_object) { true }
+ expect(Ability).not_to receive(:allowed?).with(user, permission_single, second_test_object)
+
+ expect(subject.size).to eq(1)
+ end
+ end
end
describe 'type authorizations when applied to a basic connection' do
@@ -222,28 +242,53 @@ describe 'Gitlab::Graphql::Authorization' do
include_examples 'authorization with a single permission'
end
- describe 'when connections do not follow the correct specification' do
- let(:query_string) { '{ object() { edges { node { name }} } }' }
+ describe 'Authorizations on active record relations' do
+ let!(:visible_project) { create(:project, :private) }
+ let!(:other_project) { create(:project, :private) }
+ let!(:visible_issues) { create_list(:issue, 2, project: visible_project) }
+ let!(:other_issues) { create_list(:issue, 2, project: other_project) }
+ let!(:user) { visible_project.owner }
- let(:type) do
- bad_node = type_factory do |type|
- type.graphql_name 'BadNode'
- type.field :bad_node, GraphQL::STRING_TYPE, null: true
+ let(:issue_type) do
+ type_factory do |type|
+ type.graphql_name 'FakeIssueType'
+ type.authorize :read_issue
+ type.field :id, GraphQL::ID_TYPE, null: false
end
-
+ end
+ let(:project_type) do |type|
type_factory do |type|
- type.field :edges, [bad_node], null: true
+ type.graphql_name 'FakeProjectType'
+ type.field :test_issues, issue_type.connection_type, null: false, resolve: -> (_, _, _) { Issue.where(project: [visible_project, other_project]) }
end
end
-
let(:query_type) do
query_factory do |query|
- query.field :object, type, null: true
+ query.field :test_project, project_type, null: false, resolve: -> (_, _, _) { visible_project }
end
end
+ let(:query_string) do
+ <<~QRY
+ { testProject { testIssues(first: 3) { edges { node { id } } } } }
+ QRY
+ end
+
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ end
+
+ it 'renders the issues the user has access to' do
+ issue_edges = result['testProject']['testIssues']['edges']
+ issue_ids = issue_edges.map { |issue_edge| issue_edge['node']&.fetch('id') }
+
+ expect(issue_edges.size).to eq(visible_issues.size)
+ expect(issue_ids).to eq(visible_issues.map { |i| i.id.to_s })
+ end
+
+ it 'does not check access on fields that will not be rendered' do
+ expect(Ability).not_to receive(:allowed?).with(user, :read_issue, other_issues.last)
- it 'throws an error' do
- expect { result }.to raise_error(Gitlab::Graphql::Errors::ConnectionDefinitionError)
+ result
end
end
@@ -276,6 +321,8 @@ describe 'Gitlab::Graphql::Authorization' do
def execute_query(query_type)
schema = Class.new(GraphQL::Schema) do
use Gitlab::Graphql::Authorize
+ use Gitlab::Graphql::Connections
+
query(query_type)
end
diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb
index 74e93b2c4df..05f10fb40f0 100644
--- a/spec/graphql/gitlab_schema_spec.rb
+++ b/spec/graphql/gitlab_schema_spec.rb
@@ -74,6 +74,6 @@ describe GitlabSchema do
end
def field_instrumenters
- described_class.instrumenters[:field]
+ described_class.instrumenters[:field] + described_class.instrumenters[:field_after_built_ins]
end
end
diff --git a/spec/javascripts/monitoring/monitoring_store_spec.js b/spec/javascripts/monitoring/monitoring_store_spec.js
index d8a980c874d..5bf6937c92e 100644
--- a/spec/javascripts/monitoring/monitoring_store_spec.js
+++ b/spec/javascripts/monitoring/monitoring_store_spec.js
@@ -32,4 +32,28 @@ describe('MonitoringStore', () => {
it('removes the data if all the values from a query are not defined', () => {
expect(store.groups[1].metrics[0].queries[0].result.length).toEqual(0);
});
+
+ it('assigns queries a metric id', () => {
+ expect(store.groups[1].metrics[0].queries[0].metricId).toEqual('100');
+ });
+
+ it('assigns metric id of null if metric has no id', () => {
+ const noId = MonitoringMock.data.map(group => ({
+ ...group,
+ ...{
+ metrics: group.metrics.map(metric => {
+ const { id, ...metricWithoutId } = metric;
+
+ return metricWithoutId;
+ }),
+ },
+ }));
+ store.storeMetrics(noId);
+
+ store.groups.forEach(group => {
+ group.metrics.forEach(metric => {
+ expect(metric.queries.every(query => query.metricId === null)).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb
index 62dcd80fad7..e8332b14627 100644
--- a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb
+++ b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb
@@ -20,7 +20,7 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
let!(:deployment) { create(:deployment, deployable: build) }
context 'and a cluster to deploy to' do
- let(:cluster) { create(:cluster, projects: [build.project]) }
+ let(:cluster) { create(:cluster, :group) }
before do
allow(build.deployment).to receive(:cluster).and_return(cluster)
@@ -33,6 +33,12 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
it { is_expected.to be_falsey }
end
+
+ context 'and cluster is project type' do
+ let(:cluster) { create(:cluster, :project) }
+
+ it { is_expected.to be_falsey }
+ end
end
context 'and no cluster to deploy to' do
@@ -52,7 +58,7 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
subject { described_class.new(build).complete! }
context 'completion is required' do
- let(:cluster) { create(:cluster, projects: [build.project]) }
+ let(:cluster) { create(:cluster, :group) }
before do
allow(build.deployment).to receive(:cluster).and_return(cluster)
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 507bf222810..25052a79916 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -380,7 +380,32 @@ describe Gitlab::Git::Commit, :seed_helper do
end
end
- describe '#batch_by_oid' do
+ shared_examples '.batch_by_oid' do
+ context 'with multiple OIDs' do
+ let(:oids) { [SeedRepo::Commit::ID, SeedRepo::FirstCommit::ID] }
+
+ it 'returns multiple commits' do
+ commits = described_class.batch_by_oid(repository, oids)
+
+ expect(commits.count).to eq(2)
+ expect(commits).to all( be_a(Gitlab::Git::Commit) )
+ expect(commits.first.sha).to eq(SeedRepo::Commit::ID)
+ expect(commits.second.sha).to eq(SeedRepo::FirstCommit::ID)
+ end
+ end
+
+ context 'when oids is empty' do
+ it 'returns empty commits' do
+ commits = described_class.batch_by_oid(repository, [])
+
+ expect(commits.count).to eq(0)
+ end
+ end
+ end
+
+ describe '.batch_by_oid with Gitaly enabled' do
+ it_should_behave_like '.batch_by_oid'
+
context 'when oids is empty' do
it 'makes no Gitaly request' do
expect(Gitlab::GitalyClient).not_to receive(:call)
@@ -390,6 +415,16 @@ describe Gitlab::Git::Commit, :seed_helper do
end
end
+ describe '.batch_by_oid with Rugged enabled', :enable_rugged do
+ it_should_behave_like '.batch_by_oid'
+
+ it 'calls out to the Rugged implementation' do
+ allow_any_instance_of(Rugged).to receive(:rev_parse).with(SeedRepo::Commit::ID).and_call_original
+
+ described_class.batch_by_oid(repository, [SeedRepo::Commit::ID])
+ end
+ end
+
shared_examples 'extracting commit signature' do
context 'when the commit is signed' do
let(:commit_id) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
diff --git a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
index 6114aca0616..95a4eb296fb 100644
--- a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
+++ b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
@@ -5,92 +5,104 @@ require 'spec_helper'
# Also see spec/graphql/features/authorization_spec.rb for
# integration tests of AuthorizeFieldService
describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
- describe '#build_checker' do
- let(:current_user) { double(:current_user) }
- let(:abilities) { [double(:first_ability), double(:last_ability)] }
-
- context 'when authorizing against the object' do
- let(:checker) do
- service = described_class.new(double(resolve_proc: proc {}))
- allow(service).to receive(:authorizations).and_return(abilities)
- service.__send__(:build_checker, current_user, nil)
- end
+ def type(type_authorizations = [])
+ Class.new(Types::BaseObject) do
+ graphql_name "TestType"
- it 'returns a checker which checks for a single object' do
- object = double(:object)
+ authorize type_authorizations
+ end
+ end
- abilities.each do |ability|
- spy_ability_check_for(ability, object, passed: true)
- end
+ def type_with_field(field_type, field_authorizations = [], resolved_value = "Resolved value")
+ Class.new(Types::BaseObject) do
+ graphql_name "TestTypeWithField"
+ field :test_field, field_type, null: true, authorize: field_authorizations, resolve: -> (_, _, _) { resolved_value}
+ end
+ end
- expect(checker.call(object)).to eq(object)
- end
+ let(:current_user) { double(:current_user) }
+ subject(:service) { described_class.new(field) }
- it 'returns a checker which checks for all objects' do
- objects = [double(:first), double(:last)]
+ describe "#authorized_resolve" do
+ let(:presented_object) { double("presented object") }
+ let(:presented_type) { double("parent type", object: presented_object) }
+ subject(:resolved) { service.authorized_resolve.call(presented_type, {}, { current_user: current_user }) }
- abilities.each do |ability|
- objects.each do |object|
- spy_ability_check_for(ability, object, passed: true)
+ context "scalar types" do
+ shared_examples "checking permissions on the presented object" do
+ it "checks the abilities on the object being presented and returns the value" do
+ expected_permissions.each do |permission|
+ spy_ability_check_for(permission, presented_object, passed: true)
end
+
+ expect(resolved).to eq("Resolved value")
end
- expect(checker.call(objects)).to eq(objects)
+ it "returns nil if the value wasn't authorized" do
+ allow(Ability).to receive(:allowed?).and_return false
+
+ expect(resolved).to be_nil
+ end
end
- context 'when some objects would not pass the check' do
- it 'returns nil when it is single object' do
- disallowed = double(:object)
+ context "when the field is a scalar type" do
+ let(:field) { type_with_field(GraphQL::STRING_TYPE, :read_field).fields["testField"].to_graphql }
+ let(:expected_permissions) { [:read_field] }
- spy_ability_check_for(abilities.first, disallowed, passed: false)
+ it_behaves_like "checking permissions on the presented object"
+ end
- expect(checker.call(disallowed)).to be_nil
- end
+ context "when the field is a list of scalar types" do
+ let(:field) { type_with_field([GraphQL::STRING_TYPE], :read_field).fields["testField"].to_graphql }
+ let(:expected_permissions) { [:read_field] }
- it 'returns only objects which passed when there are more than one' do
- allowed = double(:allowed)
- disallowed = double(:disallowed)
+ it_behaves_like "checking permissions on the presented object"
+ end
+ end
- spy_ability_check_for(abilities.first, disallowed, passed: false)
+ context "when the field is a specific type" do
+ let(:custom_type) { type(:read_type) }
+ let(:object_in_field) { double("presented in field") }
+ let(:field) { type_with_field(custom_type, :read_field, object_in_field).fields["testField"].to_graphql }
- abilities.each do |ability|
- spy_ability_check_for(ability, allowed, passed: true)
- end
+ it "checks both field & type permissions" do
+ spy_ability_check_for(:read_field, object_in_field, passed: true)
+ spy_ability_check_for(:read_type, object_in_field, passed: true)
- expect(checker.call([disallowed, allowed])).to contain_exactly(allowed)
- end
+ expect(resolved).to eq(object_in_field)
end
- end
- context 'when authorizing against another object' do
- let(:authorizing_obj) { double(:object) }
+ it "returns nil if viewing was not allowed" do
+ spy_ability_check_for(:read_field, object_in_field, passed: false)
+ spy_ability_check_for(:read_type, object_in_field, passed: true)
- let(:checker) do
- service = described_class.new(double(resolve_proc: proc {}))
- allow(service).to receive(:authorizations).and_return(abilities)
- service.__send__(:build_checker, current_user, authorizing_obj)
+ expect(resolved).to be_nil
end
- it 'returns a checker which checks for a single object' do
- object = double(:object)
+ context "when the field is a list" do
+ let(:object_1) { double("presented in field 1") }
+ let(:object_2) { double("presented in field 2") }
+ let(:presented_types) { [double(object: object_1), double(object: object_2)] }
+ let(:field) { type_with_field([custom_type], :read_field, presented_types).fields["testField"].to_graphql }
- abilities.each do |ability|
- spy_ability_check_for(ability, authorizing_obj, passed: true)
+ it "checks all permissions" do
+ allow(Ability).to receive(:allowed?) { true }
+
+ spy_ability_check_for(:read_field, object_1, passed: true)
+ spy_ability_check_for(:read_type, object_1, passed: true)
+ spy_ability_check_for(:read_field, object_2, passed: true)
+ spy_ability_check_for(:read_type, object_2, passed: true)
+
+ expect(resolved).to eq(presented_types)
end
- expect(checker.call(object)).to eq(object)
- end
+ it "filters out objects that the user cannot see" do
+ allow(Ability).to receive(:allowed?) { true }
- it 'returns a checker which checks for all objects' do
- objects = [double(:first), double(:last)]
+ spy_ability_check_for(:read_type, object_1, passed: false)
- abilities.each do |ability|
- objects.each do |object|
- spy_ability_check_for(ability, authorizing_obj, passed: true)
- end
+ expect(resolved.map(&:object)).to contain_exactly(object_2)
end
-
- expect(checker.call(objects)).to eq(objects)
end
end
end
diff --git a/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb b/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb
index 9bcc1e78a78..fefa2881b18 100644
--- a/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb
+++ b/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb
@@ -85,6 +85,11 @@ describe Gitlab::Graphql::Connections::KeysetConnection do
expect(subject.paged_nodes.size).to eq(3)
end
+ it 'is a loaded memoized array' do
+ expect(subject.paged_nodes).to be_an(Array)
+ expect(subject.paged_nodes.object_id).to eq(subject.paged_nodes.object_id)
+ end
+
context 'when `first` is passed' do
let(:arguments) { { first: 2 } }
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index c2934430821..4f9f916f22e 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -7,8 +7,8 @@ describe 'getting an issue list for a project' do
let(:current_user) { create(:user) }
let(:issues_data) { graphql_data['project']['issues']['edges'] }
let!(:issues) do
- create(:issue, project: project, discussion_locked: true)
- create(:issue, project: project)
+ [create(:issue, project: project, discussion_locked: true),
+ create(:issue, project: project)]
end
let(:fields) do
<<~QUERY
@@ -47,6 +47,30 @@ describe 'getting an issue list for a project' do
expect(issues_data[1]['node']['discussionLocked']).to eq true
end
+ context 'when limiting the number of results' do
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ "issues(first: 1) { #{fields} }"
+ )
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ it "is expected to check permissions on the first issue only" do
+ allow(Ability).to receive(:allowed?).and_call_original
+ # Newest first, we only want to see the newest checked
+ expect(Ability).not_to receive(:allowed?).with(current_user, :read_issue, issues.first)
+
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
context 'when the user does not have access to the issue' do
it 'returns nil' do
project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
diff --git a/spec/services/clusters/refresh_service_spec.rb b/spec/services/clusters/refresh_service_spec.rb
index 58ab3c3cf73..9e442ebf4e9 100644
--- a/spec/services/clusters/refresh_service_spec.rb
+++ b/spec/services/clusters/refresh_service_spec.rb
@@ -93,14 +93,32 @@ describe Clusters::RefreshService do
let(:group) { cluster.group }
let(:project) { create(:project, group: group) }
- include_examples 'creates a kubernetes namespace'
+ context 'when ci_preparing_state feature flag is enabled' do
+ include_examples 'does not create a kubernetes namespace'
- context 'when project already has kubernetes namespace' do
+ context 'when project already has kubernetes namespace' do
+ before do
+ create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+ end
+
+ include_examples 'does not create a kubernetes namespace'
+ end
+ end
+
+ context 'when ci_preparing_state feature flag is disabled' do
before do
- create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+ stub_feature_flags(ci_preparing_state: false)
end
- include_examples 'does not create a kubernetes namespace'
+ include_examples 'creates a kubernetes namespace'
+
+ context 'when project already has kubernetes namespace' do
+ before do
+ create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+ end
+
+ include_examples 'does not create a kubernetes namespace'
+ end
end
end
end
diff --git a/spec/workers/cluster_configure_worker_spec.rb b/spec/workers/cluster_configure_worker_spec.rb
index 83f76809435..bdb8e0e9c84 100644
--- a/spec/workers/cluster_configure_worker_spec.rb
+++ b/spec/workers/cluster_configure_worker_spec.rb
@@ -10,25 +10,35 @@ describe ClusterConfigureWorker, '#perform' do
stub_feature_flags(ci_preparing_state: ci_preparing_state_enabled)
end
- context 'when group cluster' do
- let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
- let(:group) { cluster.group }
+ shared_examples 'configured cluster' do
+ it 'creates a namespace' do
+ expect(Clusters::RefreshService).to receive(:create_or_update_namespaces_for_cluster).with(cluster).once
- context 'when group has no projects' do
- it 'does not create a namespace' do
- expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:execute)
+ worker.perform(cluster.id)
+ end
+ end
- worker.perform(cluster.id)
- end
+ shared_examples 'unconfigured cluster' do
+ it 'does not create a namespace' do
+ expect(Clusters::RefreshService).not_to receive(:create_or_update_namespaces_for_cluster)
+
+ worker.perform(cluster.id)
end
+ end
+
+ context 'group cluster' do
+ let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
+ let(:group) { cluster.group }
context 'when group has a project' do
let!(:project) { create(:project, group: group) }
- it 'creates a namespace for the project' do
- expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).once
+ it_behaves_like 'configured cluster'
+
+ context 'ci_preparing_state feature is enabled' do
+ let(:ci_preparing_state_enabled) { true }
- worker.perform(cluster.id)
+ it_behaves_like 'unconfigured cluster'
end
end
@@ -36,32 +46,26 @@ describe ClusterConfigureWorker, '#perform' do
let!(:subgroup) { create(:group, parent: group) }
let!(:project) { create(:project, group: subgroup) }
- it 'creates a namespace for the project' do
- expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).once
+ it_behaves_like 'configured cluster'
- worker.perform(cluster.id)
+ context 'ci_preparing_state feature is enabled' do
+ let(:ci_preparing_state_enabled) { true }
+
+ it_behaves_like 'unconfigured cluster'
end
end
end
context 'when provider type is gcp' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
-
- it 'configures kubernetes platform' do
- expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- described_class.new.perform(cluster.id)
- end
+ it_behaves_like 'configured cluster'
end
context 'when provider type is user' do
- let(:cluster) { create(:cluster, :project, :provided_by_user) }
+ let!(:cluster) { create(:cluster, :project, :provided_by_user) }
- it 'configures kubernetes platform' do
- expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
-
- described_class.new.perform(cluster.id)
- end
+ it_behaves_like 'configured cluster'
end
context 'when cluster does not exist' do
@@ -71,15 +75,4 @@ describe ClusterConfigureWorker, '#perform' do
described_class.new.perform(123)
end
end
-
- context 'ci_preparing_state feature is enabled' do
- let(:cluster) { create(:cluster) }
- let(:ci_preparing_state_enabled) { true }
-
- it 'does not configure the cluster' do
- expect(Clusters::RefreshService).not_to receive(:create_or_update_namespaces_for_cluster)
-
- described_class.new.perform(cluster.id)
- end
- end
end
diff --git a/spec/workers/cluster_project_configure_worker_spec.rb b/spec/workers/cluster_project_configure_worker_spec.rb
index afdea55adf4..2ac9d0f61b4 100644
--- a/spec/workers/cluster_project_configure_worker_spec.rb
+++ b/spec/workers/cluster_project_configure_worker_spec.rb
@@ -4,18 +4,11 @@ require 'spec_helper'
describe ClusterProjectConfigureWorker, '#perform' do
let(:worker) { described_class.new }
+ let(:cluster) { create(:cluster, :project) }
- context 'ci_preparing_state feature is enabled' do
- let(:cluster) { create(:cluster) }
+ it 'configures the cluster' do
+ expect(Clusters::RefreshService).to receive(:create_or_update_namespaces_for_project)
- before do
- stub_feature_flags(ci_preparing_state: true)
- end
-
- it 'does not configure the cluster' do
- expect(Clusters::RefreshService).not_to receive(:create_or_update_namespaces_for_project)
-
- described_class.new.perform(cluster.id)
- end
+ described_class.new.perform(cluster.projects.first.id)
end
end