diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-21 12:10:22 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-21 12:10:22 +0000 |
commit | 9e68395a98e71e2a0e9a6200f15ad1e7bae9ea87 (patch) | |
tree | b8508b7503f056b2438cc2fef6f2f9b1edfa0279 | |
parent | 202268ad93e9a1556f5700326be5ec89bd641a97 (diff) | |
download | gitlab-ce-9e68395a98e71e2a0e9a6200f15ad1e7bae9ea87.tar.gz |
Add latest changes from gitlab-org/gitlab@master
57 files changed, 1029 insertions, 264 deletions
diff --git a/.gitignore b/.gitignore index 38beab164cb..04384be78c4 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ eslint-report.html /builds* /.gitlab_workhorse_secret /.gitlab_pages_secret +/.gitlab_kas_secret /webpack-report/ /knapsack/ /rspec_flaky/ diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 2df7975119c..6020e69afd8 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0fe0cfaccc979592610cbf65807f19b307957750 +b2e2ec1069625c4a4f220b0ec72154b1f2bdf834 diff --git a/Gemfile.lock b/Gemfile.lock index 78fcf73133e..40dc62cbc97 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -338,7 +338,7 @@ GEM fast_blank (1.0.0) fast_gettext (1.6.0) ffaker (2.10.0) - ffi (1.12.2) + ffi (1.13.1) ffi-compiler (1.0.1) ffi (>= 1.0.0) rake @@ -695,7 +695,7 @@ GEM faraday (>= 0.9, < 2.0.0) faraday-cookie_jar (~> 0.0.6) ms_rest (~> 0.7.6) - msgpack (1.3.1) + msgpack (1.3.3) multi_json (1.14.1) multi_xml (0.6.0) multipart-post (2.1.1) @@ -804,7 +804,7 @@ GEM validate_url webfinger (>= 1.0.1) opentracing (0.5.0) - optimist (3.0.0) + optimist (3.0.1) org-ruby (0.9.12) rubypants (~> 0.2) orm_adapter (0.5.0) @@ -904,7 +904,7 @@ GEM ffi (>= 0.5.0, < 2) rblineprof (0.3.6) debugger-ruby_core_source (~> 1.3) - rbtrace (0.4.11) + rbtrace (0.4.14) ffi (>= 1.0.6) msgpack (>= 0.4.3) optimist (>= 3.0.0) diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue index d0cc617d755..fd70a6419fc 100644 --- a/app/assets/javascripts/repository/components/table/index.vue +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -1,5 +1,5 @@ <script> -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlSkeletonLoading, GlButton } from '@gitlab/ui'; import { sprintf, __ } from '../../../locale'; import getRefMixin from '../../mixins/get_ref'; import projectPathQuery from '../../queries/project_path.query.graphql'; @@ -13,6 +13,7 @@ export default { TableHeader, TableRow, ParentRow, + GlButton, }, mixins: [getRefMixin], apollo: { @@ -39,6 +40,10 @@ export default { required: false, default: '', }, + hasMore: { + type: Boolean, + required: true, + }, }, data() { return { @@ -65,6 +70,11 @@ export default { return !this.isLoading && ['', '/'].indexOf(this.path) === -1; }, }, + methods: { + showMore() { + this.$emit('showMore'); + }, + }, }; </script> @@ -110,6 +120,20 @@ export default { <td><gl-skeleton-loading :lines="1" class="ml-auto h-auto w-50" /></td> </tr> </template> + <template v-if="hasMore"> + <tr> + <td align="center" colspan="3" class="gl-p-0!"> + <gl-button + variant="link" + class="gl-display-flex gl-w-full gl-py-4!" + :loading="isLoading" + @click="showMore" + > + {{ s__('ProjectFileTree|Show more') }} + </gl-button> + </td> + </tr> + </template> </tbody> </table> </div> diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue index fe3065a2145..365b6cbb550 100644 --- a/app/assets/javascripts/repository/components/tree_content.vue +++ b/app/assets/javascripts/repository/components/tree_content.vue @@ -1,5 +1,4 @@ <script> -import { GlButton } from '@gitlab/ui'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { __ } from '../../locale'; import FileTable from './table/index.vue'; @@ -17,7 +16,6 @@ export default { components: { FileTable, FilePreview, - GlButton, }, mixins: [getRefMixin], apollo: { @@ -127,7 +125,7 @@ export default { .concat(data.trees.pageInfo, data.submodules.pageInfo, data.blobs.pageInfo) .find(({ hasNextPage }) => hasNextPage); }, - showMore() { + handleShowMore() { this.clickedShowMore = true; this.fetchFiles(); }, @@ -142,20 +140,9 @@ export default { :entries="entries" :is-loading="isLoadingFiles" :loading-path="loadingPath" + :has-more="hasShowMore" + @showMore="handleShowMore" /> - <div - v-if="hasShowMore" - class="gl-border-1 gl-border-gray-100 gl-rounded-base gl-border-t-none gl-border-b-solid gl-border-l-solid gl-border-r-solid gl-rounded-top-right-none gl-rounded-top-left-none gl-mt-n1" - > - <gl-button - variant="link" - class="gl-display-flex gl-w-full gl-py-4!" - :loading="isLoadingFiles" - @click="showMore" - > - {{ s__('ProjectFileTree|Show more') }} - </gl-button> - </div> <file-preview v-if="readme" :blob="readme" /> </div> </template> diff --git a/app/graphql/resolvers/project_members_resolver.rb b/app/graphql/resolvers/project_members_resolver.rb index 3846531762e..7ee635014f4 100644 --- a/app/graphql/resolvers/project_members_resolver.rb +++ b/app/graphql/resolvers/project_members_resolver.rb @@ -2,19 +2,23 @@ module Resolvers class ProjectMembersResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + argument :search, GraphQL::STRING_TYPE, required: false, description: 'Search query' - type Types::ProjectMemberType, null: true + type Types::MemberInterface, null: true + + authorize :read_project_member alias_method :project, :object def resolve(**args) - return Member.none unless project.present? + authorize!(project) MembersFinder - .new(project, context[:current_user], params: args) + .new(project, current_user, params: args) .execute end end diff --git a/app/graphql/types/group_member_type.rb b/app/graphql/types/group_member_type.rb index ffffa3247db..6cca0a50647 100644 --- a/app/graphql/types/group_member_type.rb +++ b/app/graphql/types/group_member_type.rb @@ -8,7 +8,7 @@ module Types implements MemberInterface graphql_name 'GroupMember' - description 'Represents a Group Member' + description 'Represents a Group Membership' field :group, Types::GroupType, null: true, description: 'Group that a User is a member of', diff --git a/app/graphql/types/member_interface.rb b/app/graphql/types/member_interface.rb index 976836221bc..fed7272c0e3 100644 --- a/app/graphql/types/member_interface.rb +++ b/app/graphql/types/member_interface.rb @@ -4,6 +4,9 @@ module Types module MemberInterface include BaseInterface + field :id, GraphQL::ID_TYPE, null: false, + description: 'ID of the member' + field :access_level, Types::AccessLevelType, null: true, description: 'GitLab::Access level' @@ -18,5 +21,22 @@ module Types field :expires_at, Types::TimeType, null: true, description: 'Date and time the membership expires' + + field :user, Types::UserType, null: false, + description: 'User that is associated with the member object', + resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.user_id).find } + + definition_methods do + def resolve_type(object, context) + case object + when GroupMember + Types::GroupMemberType + when ProjectMember + Types::ProjectMemberType + else + raise ::Gitlab::Graphql::Errors::BaseError, "Unknown member type #{object.class.name}" + end + end + end end end diff --git a/app/graphql/types/project_member_type.rb b/app/graphql/types/project_member_type.rb index e9ccb51886b..f08781238d0 100644 --- a/app/graphql/types/project_member_type.rb +++ b/app/graphql/types/project_member_type.rb @@ -3,7 +3,7 @@ module Types class ProjectMemberType < BaseObject graphql_name 'ProjectMember' - description 'Represents a Project Member' + description 'Represents a Project Membership' expose_permissions Types::PermissionTypes::Project @@ -11,13 +11,6 @@ module Types authorize :read_project - field :id, GraphQL::ID_TYPE, null: false, - description: 'ID of the member' - - field :user, Types::UserType, null: false, - description: 'User that is associated with the member object', - resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.user_id).find } - field :project, Types::ProjectType, null: true, description: 'Project that User is a member of', resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, obj.source_id).find } diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 5562db69de6..36e37654812 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -159,7 +159,7 @@ module Types resolver: Resolvers::ProjectMilestonesResolver field :project_members, - Types::ProjectMemberType.connection_type, + Types::MemberInterface.connection_type, description: 'Members of the project', resolver: Resolvers::ProjectMembersResolver diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index 39be8ae9f60..7f0c59f65a0 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -12,8 +12,8 @@ module EnvironmentsHelper def environments_folder_list_view_data { "endpoint" => folder_project_environments_path(@project, @folder, format: :json), - "folder-name" => @folder, - "can-read-environment" => can?(current_user, :read_environment, @project).to_s + "folder_name" => @folder, + "can_read_environment" => can?(current_user, :read_environment, @project).to_s } end @@ -33,11 +33,11 @@ module EnvironmentsHelper def environment_logs_data(project, environment) { - "environment-name": environment.name, - "environments-path": project_environments_path(project, format: :json), - "environment-id": environment.id, - "cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack'), - "clusters-path": project_clusters_path(project, format: :json) + "environment_name": environment.name, + "environments_path": project_environments_path(project, format: :json), + "environment_id": environment.id, + "cluster_applications_documentation_path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack'), + "clusters_path": project_clusters_path(project, format: :json) } end @@ -51,18 +51,18 @@ module EnvironmentsHelper return {} unless project { - 'settings-path' => edit_project_service_path(project, 'prometheus'), - 'clusters-path' => project_clusters_path(project), - 'dashboards-endpoint' => project_performance_monitoring_dashboards_path(project, format: :json), - 'default-branch' => project.default_branch, - 'project-path' => project_path(project), - 'tags-path' => project_tags_path(project), - 'external-dashboard-url' => project.metrics_setting_external_dashboard_url, - 'custom-metrics-path' => project_prometheus_metrics_path(project), - 'validate-query-path' => validate_query_project_prometheus_metrics_path(project), - 'custom-metrics-available' => "#{custom_metrics_available?(project)}", - 'prometheus-alerts-available' => "#{can?(current_user, :read_prometheus_alerts, project)}", - 'dashboard-timezone' => project.metrics_setting_dashboard_timezone.to_s.upcase + 'settings_path' => edit_project_service_path(project, 'prometheus'), + 'clusters_path' => project_clusters_path(project), + 'dashboards_endpoint' => project_performance_monitoring_dashboards_path(project, format: :json), + 'default_branch' => project.default_branch, + 'project_path' => project_path(project), + 'tags_path' => project_tags_path(project), + 'external_dashboard_url' => project.metrics_setting_external_dashboard_url, + 'custom_metrics_path' => project_prometheus_metrics_path(project), + 'validate_query_path' => validate_query_project_prometheus_metrics_path(project), + 'custom_metrics_available' => "#{custom_metrics_available?(project)}", + 'prometheus_alerts_available' => "#{can?(current_user, :read_prometheus_alerts, project)}", + 'dashboard_timezone' => project.metrics_setting_dashboard_timezone.to_s.upcase } end @@ -70,11 +70,11 @@ module EnvironmentsHelper return {} unless environment { - 'metrics-dashboard-base-path' => metrics_dashboard_base_path(environment, project), - 'current-environment-name' => environment.name, - 'has-metrics' => "#{environment.has_metrics?}", - 'prometheus-status' => "#{environment.prometheus_status}", - 'environment-state' => "#{environment.state}" + 'metrics_dashboard_base_path' => metrics_dashboard_base_path(environment, project), + 'current_environment_name' => environment.name, + 'has_metrics' => "#{environment.has_metrics?}", + 'prometheus_status' => "#{environment.prometheus_status}", + 'environment_state' => "#{environment.state}" } end @@ -93,26 +93,26 @@ module EnvironmentsHelper return {} unless project && environment { - 'metrics-endpoint' => additional_metrics_project_environment_path(project, environment, format: :json), - 'dashboard-endpoint' => metrics_dashboard_project_environment_path(project, environment, format: :json), - 'deployments-endpoint' => project_environment_deployments_path(project, environment, format: :json), - 'alerts-endpoint' => project_prometheus_alerts_path(project, environment_id: environment.id, format: :json), - 'operations-settings-path' => project_settings_operations_path(project), - 'can-access-operations-settings' => can?(current_user, :admin_operations, project).to_s, - 'panel-preview-endpoint' => project_metrics_dashboards_builder_path(project, format: :json) + 'metrics_endpoint' => additional_metrics_project_environment_path(project, environment, format: :json), + 'dashboard_endpoint' => metrics_dashboard_project_environment_path(project, environment, format: :json), + 'deployments_endpoint' => project_environment_deployments_path(project, environment, format: :json), + 'alerts_endpoint' => project_prometheus_alerts_path(project, environment_id: environment.id, format: :json), + 'operations_settings_path' => project_settings_operations_path(project), + 'can_access_operations_settings' => can?(current_user, :admin_operations, project).to_s, + 'panel_preview_endpoint' => project_metrics_dashboards_builder_path(project, format: :json) } end def static_metrics_data { - 'documentation-path' => help_page_path('administration/monitoring/prometheus/index.md'), - 'add-dashboard-documentation-path' => help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'), - 'empty-getting-started-svg-path' => image_path('illustrations/monitoring/getting_started.svg'), - 'empty-loading-svg-path' => image_path('illustrations/monitoring/loading.svg'), - 'empty-no-data-svg-path' => image_path('illustrations/monitoring/no_data.svg'), - 'empty-no-data-small-svg-path' => image_path('illustrations/chart-empty-state-small.svg'), - 'empty-unable-to-connect-svg-path' => image_path('illustrations/monitoring/unable_to_connect.svg'), - 'custom-dashboard-base-path' => Gitlab::Metrics::Dashboard::RepoDashboardFinder::DASHBOARD_ROOT + 'documentation_path' => help_page_path('administration/monitoring/prometheus/index.md'), + 'add_dashboard_documentation_path' => help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'), + 'empty_getting_started_svg_path' => image_path('illustrations/monitoring/getting_started.svg'), + 'empty_loading_svg_path' => image_path('illustrations/monitoring/loading.svg'), + 'empty_no_data_svg_path' => image_path('illustrations/monitoring/no_data.svg'), + 'empty_no_data_small_svg_path' => image_path('illustrations/chart-empty-state-small.svg'), + 'empty_unable_to_connect_svg_path' => image_path('illustrations/monitoring/unable_to_connect.svg'), + 'custom_dashboard_base_path' => Gitlab::Metrics::Dashboard::RepoDashboardFinder::DASHBOARD_ROOT } end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 36e458d9353..ff72aa7f563 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -105,7 +105,6 @@ class MergeRequest < ApplicationRecord after_create :ensure_merge_request_diff after_update :clear_memoized_shas - after_update :clear_memoized_source_branch_exists after_update :reload_diff_if_branch_changed after_commit :ensure_metrics, on: [:create, :update], unless: :importing? after_commit :expire_etag_cache, unless: :importing? @@ -871,10 +870,6 @@ class MergeRequest < ApplicationRecord clear_memoization(:target_branch_head) end - def clear_memoized_source_branch_exists - clear_memoization(:source_branch_exists) - end - def reload_diff_if_branch_changed if (saved_change_to_source_branch? || saved_change_to_target_branch?) && (source_branch_head && target_branch_head) @@ -1126,11 +1121,9 @@ class MergeRequest < ApplicationRecord end def source_branch_exists? - strong_memoize(:source_branch_exists) do - next false unless self.source_project + return false unless self.source_project - self.source_project.repository.branch_exists?(self.source_branch) - end + self.source_project.repository.branch_exists?(self.source_branch) end def target_branch_exists? diff --git a/changelogs/unreleased/229297-migrate-bootstrap-button-to-gitlab-ui-glbutton-in-ee-app-assets-ja.yml b/changelogs/unreleased/229297-migrate-bootstrap-button-to-gitlab-ui-glbutton-in-ee-app-assets-ja.yml new file mode 100644 index 00000000000..82edc6a1ac3 --- /dev/null +++ b/changelogs/unreleased/229297-migrate-bootstrap-button-to-gitlab-ui-glbutton-in-ee-app-assets-ja.yml @@ -0,0 +1,5 @@ +--- +title: GlButton migrations for pipeline security tab +merge_request: 39651 +author: +type: performance diff --git a/changelogs/unreleased/232636-add-support-for-fixing-composer-404.yml b/changelogs/unreleased/232636-add-support-for-fixing-composer-404.yml new file mode 100644 index 00000000000..775268e3af9 --- /dev/null +++ b/changelogs/unreleased/232636-add-support-for-fixing-composer-404.yml @@ -0,0 +1,5 @@ +--- +title: Fix composer 404 issues with http auth +merge_request: 38641 +author: +type: fixed diff --git a/changelogs/unreleased/235672-convert-show-more-to-table-row.yml b/changelogs/unreleased/235672-convert-show-more-to-table-row.yml new file mode 100644 index 00000000000..b95a7293ae0 --- /dev/null +++ b/changelogs/unreleased/235672-convert-show-more-to-table-row.yml @@ -0,0 +1,5 @@ +--- +title: Change show more button to be a table row so to remove manual CSS styling +merge_request: 39788 +author: +type: changed diff --git a/changelogs/unreleased/docs-file_hook.yml b/changelogs/unreleased/docs-file_hook.yml new file mode 100644 index 00000000000..576029a13b6 --- /dev/null +++ b/changelogs/unreleased/docs-file_hook.yml @@ -0,0 +1,5 @@ +--- +title: Fix example within file_hooks documentation +merge_request: 40071 +author: Roger Meier +type: fixed diff --git a/changelogs/unreleased/project_member_list.yml b/changelogs/unreleased/project_member_list.yml new file mode 100644 index 00000000000..f448fdf092b --- /dev/null +++ b/changelogs/unreleased/project_member_list.yml @@ -0,0 +1,5 @@ +--- +title: Include also inherited project members in GraphQL API +merge_request: 39444 +author: +type: fixed diff --git a/config/feature_flags/development/personal_snippet_reference_filters.yml b/config/feature_flags/development/personal_snippet_reference_filters.yml index 6a9aefbb379..44b9ac2f862 100644 --- a/config/feature_flags/development/personal_snippet_reference_filters.yml +++ b/config/feature_flags/development/personal_snippet_reference_filters.yml @@ -1,7 +1,7 @@ --- name: personal_snippet_reference_filters -introduced_by_url: +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38571 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/235155 group: group::editor type: development -default_enabled: false
\ No newline at end of file +default_enabled: false diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 5d217332634..19dcea84d86 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -1093,6 +1093,11 @@ production: &base # Default is '.gitlab_workhorse_secret' relative to Rails.root (i.e. root of the GitLab app). # secret_file: /home/git/gitlab/.gitlab_workhorse_secret + gitlab_kas: + # File that contains the secret key for verifying access for gitlab-kas. + # Default is '.gitlab_kas_secret' relative to Rails.root (i.e. root of the GitLab app). + # secret_file: /home/git/gitlab/.gitlab_kas_secret + ## GitLab Elasticsearch settings elasticsearch: indexer_path: /home/git/gitlab-elasticsearch-indexer/ diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 628d9c65ce0..da275b34f5d 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -635,6 +635,12 @@ Settings['workhorse'] ||= Settingslogic.new({}) Settings.workhorse['secret_file'] ||= Rails.root.join('.gitlab_workhorse_secret') # +# GitLab KAS +# +Settings['gitlab_kas'] ||= Settingslogic.new({}) +Settings.gitlab_kas['secret_file'] ||= Rails.root.join('.gitlab_kas_secret') + +# # Repositories # Settings['repositories'] ||= Settingslogic.new({}) diff --git a/config/initializers/gitlab_kas_secret.rb b/config/initializers/gitlab_kas_secret.rb new file mode 100644 index 00000000000..5e86e954684 --- /dev/null +++ b/config/initializers/gitlab_kas_secret.rb @@ -0,0 +1 @@ +Gitlab::Kas.ensure_secret! diff --git a/doc/administration/file_hooks.md b/doc/administration/file_hooks.md index c0b31769e7f..a6610188381 100644 --- a/doc/administration/file_hooks.md +++ b/doc/administration/file_hooks.md @@ -71,9 +71,9 @@ Below is an example that will only response on the event `project_create` and will inform the admins from the GitLab instance that a new project has been created. ```ruby +#!/opt/gitlab/embedded/bin/ruby # By using the embedded ruby version we eliminate the possibility that our chosen language # would be unavailable from -#!/opt/gitlab/embedded/bin/ruby require 'json' require 'mail' diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md index c513dea239a..7e87d520ce3 100644 --- a/doc/api/graphql/index.md +++ b/doc/api/graphql/index.md @@ -59,6 +59,23 @@ There are no plans to deprecate the REST API. To reduce the technical burden of supporting two APIs in parallel, they should share implementations as much as possible. +### Deprecation process + +Fields marked for removal from the GitLab GraphQL API are first **deprecated** but still available +for at least six releases, and then **removed entirely**. +Removals occur at X.0 and X.6 releases. + +For example, a field can be marked as deprecated (but still usable) in %12.7, but can be used until its removal in %13.6. +When marked as deprecated, an alternative should be provided if there is one. +That gives consumers of the GraphQL API a minimum of six months to update their GraphQL queries. + +The process is as follows: + +1. The field is listed as deprecated in [GraphQL API Reference](reference/index.md). +1. Removals are announced at least one release prior in the Deprecation Warnings section of the + release post (at or prior to X.11 and X.5 releases). +1. Fields meeting criteria are removed in X.0 or X.6. + ## Available queries The GraphQL API includes the following queries at the root level: diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index f1bc1f49353..c6c5bd15b5c 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -1655,6 +1655,41 @@ type ClusterAgent { updatedAt: Time } +""" +Autogenerated input type of ClusterAgentDelete +""" +input ClusterAgentDeleteInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Global id of the cluster agent that will be deleted + """ + id: ClustersAgentID! +} + +""" +Autogenerated return type of ClusterAgentDelete +""" +type ClusterAgentDeletePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Errors encountered during execution of the mutation. + """ + errors: [String!]! +} + +""" +Identifier of Clusters::Agent +""" +scalar ClustersAgentID + type Commit { """ Author of the commit @@ -6658,7 +6693,7 @@ type Group { } """ -Represents a Group Member +Represents a Group Membership """ type GroupMember implements MemberInterface { """ @@ -6687,11 +6722,21 @@ type GroupMember implements MemberInterface { group: Group """ + ID of the member + """ + id: ID! + + """ Date and time the membership was last updated """ updatedAt: Time """ + User that is associated with the member object + """ + user: User! + + """ Permissions for the current user on the resource """ userPermissions: GroupPermissions! @@ -8341,9 +8386,54 @@ interface MemberInterface { expiresAt: Time """ + ID of the member + """ + id: ID! + + """ Date and time the membership was last updated """ updatedAt: Time + + """ + User that is associated with the member object + """ + user: User! +} + +""" +The connection type for MemberInterface. +""" +type MemberInterfaceConnection { + """ + A list of edges. + """ + edges: [MemberInterfaceEdge] + + """ + A list of nodes. + """ + nodes: [MemberInterface] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! +} + +""" +An edge in a connection. +""" +type MemberInterfaceEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: MemberInterface } type MergeRequest implements Noteable { @@ -9591,6 +9681,7 @@ type Mutation { awardEmojiToggle(input: AwardEmojiToggleInput!): AwardEmojiTogglePayload boardListCreate(input: BoardListCreateInput!): BoardListCreatePayload boardListUpdateLimitMetrics(input: BoardListUpdateLimitMetricsInput!): BoardListUpdateLimitMetricsPayload + clusterAgentDelete(input: ClusterAgentDeleteInput!): ClusterAgentDeletePayload commitCreate(input: CommitCreateInput!): CommitCreatePayload configureSast(input: ConfigureSastInput!): ConfigureSastPayload createAlertIssue(input: CreateAlertIssueInput!): CreateAlertIssuePayload @@ -11567,7 +11658,7 @@ type Project { Search query """ search: String - ): ProjectMemberConnection + ): MemberInterfaceConnection """ Indicates if there is public access to pipelines and job details of the project, including output logs and artifacts @@ -12001,7 +12092,7 @@ type ProjectEdge { } """ -Represents a Project Member +Represents a Project Membership """ type ProjectMember implements MemberInterface { """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 91b7eef7816..53adde5bdc3 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -4519,6 +4519,104 @@ "possibleTypes": null }, { + "kind": "INPUT_OBJECT", + "name": "ClusterAgentDeleteInput", + "description": "Autogenerated input type of ClusterAgentDelete", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": "Global id of the cluster agent that will be deleted", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ClustersAgentID", + "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": "ClusterAgentDeletePayload", + "description": "Autogenerated return type of ClusterAgentDelete", + "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": "Errors encountered during execution of the mutation.", + "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 + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ClustersAgentID", + "description": "Identifier of Clusters::Agent", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { "kind": "OBJECT", "name": "Commit", "description": null, @@ -18300,7 +18398,7 @@ { "kind": "OBJECT", "name": "GroupMember", - "description": "Represents a Group Member", + "description": "Represents a Group Membership", "fields": [ { "name": "accessLevel", @@ -18373,6 +18471,24 @@ "deprecationReason": null }, { + "name": "id", + "description": "ID of the member", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "updatedAt", "description": "Date and time the membership was last updated", "args": [ @@ -18387,6 +18503,24 @@ "deprecationReason": null }, { + "name": "user", + "description": "User that is associated with the member object", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "userPermissions", "description": "Permissions for the current user on the resource", "args": [ @@ -23178,6 +23312,24 @@ "deprecationReason": null }, { + "name": "id", + "description": "ID of the member", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "updatedAt", "description": "Date and time the membership was last updated", "args": [ @@ -23190,6 +23342,24 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "user", + "description": "User that is associated with the member object", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -23210,6 +23380,118 @@ }, { "kind": "OBJECT", + "name": "MemberInterfaceConnection", + "description": "The connection type for MemberInterface.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MemberInterfaceEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "INTERFACE", + "name": "MemberInterface", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MemberInterfaceEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [ + + ], + "type": { + "kind": "INTERFACE", + "name": "MemberInterface", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "MergeRequest", "description": null, "fields": [ @@ -27090,6 +27372,33 @@ "deprecationReason": null }, { + "name": "clusterAgentDelete", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ClusterAgentDeleteInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ClusterAgentDeletePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "commitCreate", "description": null, "args": [ @@ -33999,7 +34308,7 @@ ], "type": { "kind": "OBJECT", - "name": "ProjectMemberConnection", + "name": "MemberInterfaceConnection", "ofType": null }, "isDeprecated": false, @@ -35133,7 +35442,7 @@ { "kind": "OBJECT", "name": "ProjectMember", - "description": "Represents a Project Member", + "description": "Represents a Project Membership", "fields": [ { "name": "accessLevel", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index c33ee00a959..f36156411b7 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -274,6 +274,15 @@ Autogenerated return type of BoardListUpdateLimitMetrics | `project` | Project | The project this cluster agent is associated with | | `updatedAt` | Time | Timestamp the cluster agent was updated | +## ClusterAgentDeletePayload + +Autogenerated return type of ClusterAgentDelete + +| Name | Type | Description | +| --- | ---- | ---------- | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `errors` | String! => Array | Errors encountered during execution of the mutation. | + ## Commit | Name | Type | Description | @@ -999,7 +1008,7 @@ Autogenerated return type of EpicTreeReorder ## GroupMember -Represents a Group Member +Represents a Group Membership | Name | Type | Description | | --- | ---- | ---------- | @@ -1008,7 +1017,9 @@ Represents a Group Member | `createdBy` | User | User that authorized membership | | `expiresAt` | Time | Date and time the membership expires | | `group` | Group | Group that a User is a member of | +| `id` | ID! | ID of the member | | `updatedAt` | Time | Date and time the membership was last updated | +| `user` | User! | User that is associated with the member object | | `userPermissions` | GroupPermissions! | Permissions for the current user on the resource | ## GroupPermissions @@ -1690,7 +1701,7 @@ Information about pagination in a connection. ## ProjectMember -Represents a Project Member +Represents a Project Membership | Name | Type | Description | | --- | ---- | ---------- | diff --git a/doc/api/issues.md b/doc/api/issues.md index 2ff84a79d62..d471fe7092e 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -428,7 +428,7 @@ GET /projects/:id/issues?confidential=true | `updated_after` | datetime | no | Return issues updated on or after the given time | | `updated_before` | datetime | no | Return issues updated on or before the given time | | `weight` **(STARTER)** | integer | no | Return issues with the specified `weight`. `None` returns issues with no weight assigned. `Any` returns issues with a weight assigned. | -| `with_labels_details` | boolean | no | If `true`, the response returns more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. `description_html` Introduced in [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413) | +| `with_labels_details` | boolean | no | If `true`, the response returns more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. `description_html` was introduced in [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413) | ```shell curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/issues" diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 694754a33d1..295cdbe8b38 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -2046,7 +2046,7 @@ This example creates four paths of execution: because of `only/except` rules or otherwise does not exist, the pipeline will be created with YAML error. - The maximum number of jobs that a single job can need in the `needs:` array is limited: - - For GitLab.com, the limit is ten. For more information, see our + - For GitLab.com, the limit is 50. For more information, see our [infrastructure issue](https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/7541). - For self-managed instances, the limit is: 50. This limit [can be changed](#changing-the-needs-job-limit-core-only). - If `needs:` refers to a job that is marked as `parallel:`. diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md index 76175cb7b66..ea24e216d58 100644 --- a/doc/development/contributing/issue_workflow.md +++ b/doc/development/contributing/issue_workflow.md @@ -40,7 +40,7 @@ scheduling into milestones. Labeling is a task for everyone. Most issues will have labels for at least one of the following: -- Type: `~feature`, `~bug`, `~backstage`, `~documentation`, etc. +- Type: `~feature`, `~bug`, `~tooling`, `~documentation`, etc. - Stage: `~"devops::plan"`, `~"devops::create"`, etc. - Group: `~"group::source code"`, `~"group::knowledge"`, `~"group::editor"`, etc. - Category: `~"Category:Code Analytics"`, `~"Category:DevOps Score"`, `~"Category:Templates"`, etc. @@ -67,7 +67,7 @@ The current type labels are: - ~feature - ~bug -- ~backstage +- ~tooling - ~"support request" - ~meta - ~documentation @@ -93,9 +93,9 @@ Following is a non-exhaustive list of facet labels: - ~enhancement: This label can refine an issue that has the ~feature label. - ~"master:broken": This label can refine an issue that has the ~bug label. - ~"failure::flaky-test": This label can refine an issue that has the ~bug label. -- ~"technical debt": This label can refine an issue that has the ~backstage label. -- ~"static analysis": This label can refine an issue that has the ~backstage label. -- ~"ci-build": This label can refine an issue that has the ~backstage label. +- ~"technical debt": This label can refine an issue that has the ~tooling label. +- ~"static analysis": This label can refine an issue that has the ~tooling label. +- ~"ci-build": This label can refine an issue that has the ~tooling label. - ~performance: A performance issue could describe a ~bug or a ~feature. - ~security: A security issue could describe a ~bug or a ~feature. - ~database: A database issue could describe a ~bug or a ~feature. diff --git a/doc/development/internal_api.md b/doc/development/internal_api.md index c51bf66be46..569128c9612 100644 --- a/doc/development/internal_api.md +++ b/doc/development/internal_api.md @@ -26,8 +26,8 @@ file, and include the token Base64 encoded in a `secret_token` parameter or in the `Gitlab-Shared-Secret` header. NOTE: **Note:** -The internal API used by GitLab Pages uses a different kind of -authentication. +The internal API used by GitLab Pages, and GitLab Kubernetes Agent Server (kas) uses JSON Web Token (JWT) +authentication, which is different from GitLab Shell. ## Git Authentication diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 54fcfeb2ee4..4d9f09bb8b0 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -11,7 +11,7 @@ as the hardware requirements that are needed to install and use GitLab. ### Supported Linux distributions -- Ubuntu (16.04/18.04) +- Ubuntu (16.04/18.04/20.04) - Debian (8/9/10) - CentOS (6/7/8) - openSUSE (Leap 15.1/Enterprise Server 12.2) diff --git a/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_2.png b/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_2.png Binary files differdeleted file mode 100644 index 591a08f4d7a..00000000000 --- a/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_2.png +++ /dev/null diff --git a/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_3.png b/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_3.png Binary files differnew file mode 100644 index 00000000000..c89179e739d --- /dev/null +++ b/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_3.png diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md index b8fcc513cb1..2ef6dd2e8f4 100644 --- a/doc/user/application_security/security_dashboard/index.md +++ b/doc/user/application_security/security_dashboard/index.md @@ -43,7 +43,7 @@ To use the instance, group, project, or pipeline security dashboard: At the pipeline level, the Security section displays the vulnerabilities present in the branch of the project the pipeline was run against. -![Pipeline Security Dashboard](img/pipeline_security_dashboard_v13_2.png) +![Pipeline Security Dashboard](img/pipeline_security_dashboard_v13_3.png) Visit the page for any pipeline that ran any of the [supported reports](#supported-reports). To view the pipeline's security findings, select the **Security** tab when viewing the pipeline. diff --git a/doc/user/packages/composer_repository/index.md b/doc/user/packages/composer_repository/index.md index 9b1f23f6d59..cc3c1c48a62 100644 --- a/doc/user/packages/composer_repository/index.md +++ b/doc/user/packages/composer_repository/index.md @@ -130,11 +130,8 @@ You also need to create a `auth.json` file with your GitLab credentials: ```json { - "http-basic": { - "gitlab.com": { - "username": "___token___", - "password": "<personal_access_token>" - } + "gitlab-token": { + "gitlab.com": "<personal_access_token>" } } ``` diff --git a/doc/user/project/import/gemnasium.md b/doc/user/project/import/gemnasium.md index 3838289aec4..8957960d098 100644 --- a/doc/user/project/import/gemnasium.md +++ b/doc/user/project/import/gemnasium.md @@ -105,7 +105,7 @@ back to both GitLab and GitHub when completed. 1. The result of the job will be visible directly from the pipeline view: - ![Security Dashboard](../../application_security/security_dashboard/img/pipeline_security_dashboard_v13_2.png) + ![Security Dashboard](../../application_security/security_dashboard/img/pipeline_security_dashboard_v13_3.png) NOTE: **Note:** If you don't commit very often to your project, you may want to use diff --git a/lib/api/internal/kubernetes.rb b/lib/api/internal/kubernetes.rb index 7f64fd7efe3..49ace13f322 100644 --- a/lib/api/internal/kubernetes.rb +++ b/lib/api/internal/kubernetes.rb @@ -4,7 +4,15 @@ module API # Kubernetes Internal API module Internal class Kubernetes < Grape::API::Instance + before do + authenticate_gitlab_kas_request! + end + helpers do + def authenticate_gitlab_kas_request! + unauthorized! unless Gitlab::Kas.verify_api_request(headers) + end + def agent_token @agent_token ||= cluster_agent_token_from_authorization_token end diff --git a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb index b64990d6a7a..72ef0a8d067 100644 --- a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb +++ b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb @@ -33,7 +33,7 @@ module Gitlab def kubernetes_namespace strong_memoize(:kubernetes_namespace) do - Clusters::KubernetesNamespaceFinder.new( + ::Clusters::KubernetesNamespaceFinder.new( deployment_cluster, project: environment.project, environment_name: environment.name, @@ -47,7 +47,7 @@ module Gitlab return if conflicting_ci_namespace_requested?(namespace) - Clusters::Kubernetes::CreateOrUpdateNamespaceService.new( + ::Clusters::Kubernetes::CreateOrUpdateNamespaceService.new( cluster: deployment_cluster, kubernetes_namespace: namespace ).execute @@ -71,7 +71,7 @@ module Gitlab end def build_namespace_record - Clusters::BuildKubernetesNamespaceService.new( + ::Clusters::BuildKubernetesNamespaceService.new( deployment_cluster, environment: environment ).execute diff --git a/lib/gitlab/json.rb b/lib/gitlab/json.rb index d6681347f42..29cfec443e8 100644 --- a/lib/gitlab/json.rb +++ b/lib/gitlab/json.rb @@ -22,7 +22,7 @@ module Gitlab string = string.to_s unless string.is_a?(String) legacy_mode = legacy_mode_enabled?(opts.delete(:legacy_mode)) - data = adapter_load(string, opts) + data = adapter_load(string, **opts) handle_legacy_mode!(data) if legacy_mode diff --git a/lib/gitlab/kas.rb b/lib/gitlab/kas.rb new file mode 100644 index 00000000000..08dde98e965 --- /dev/null +++ b/lib/gitlab/kas.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Gitlab + module Kas + INTERNAL_API_REQUEST_HEADER = 'Gitlab-Kas-Api-Request' + JWT_ISSUER = 'gitlab-kas' + + include JwtAuthenticatable + + class << self + def verify_api_request(request_headers) + decode_jwt_for_issuer(JWT_ISSUER, request_headers[INTERNAL_API_REQUEST_HEADER]) + rescue JWT::DecodeError + nil + end + + def secret_path + Gitlab.config.gitlab_kas.secret_file + end + + def ensure_secret! + return if File.exist?(secret_path) + + write_secret + end + end + end +end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 19faaaacf9a..42be49bdbd6 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -409,7 +409,7 @@ module Gitlab def successful_deployments_with_cluster(scope) scope .joins(cluster: :deployments) - .merge(Clusters::Cluster.enabled) + .merge(::Clusters::Cluster.enabled) .merge(Deployment.success) end # rubocop: enable UsageData/LargeTable diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6e7c11c29d3..80d6d166da5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5114,6 +5114,9 @@ msgstr "" msgid "ClusterAgent|You have insufficient permissions to create a cluster agent for this project" msgstr "" +msgid "ClusterAgent|You have insufficient permissions to delete this cluster agent" +msgstr "" + msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster" msgstr "" diff --git a/spec/frontend/monitoring/fixture_data.js b/spec/frontend/monitoring/fixture_data.js index 30040d3f89f..18ec74550b4 100644 --- a/spec/frontend/monitoring/fixture_data.js +++ b/spec/frontend/monitoring/fixture_data.js @@ -1,8 +1,7 @@ import { stateAndPropsFromDataset } from '~/monitoring/utils'; import { mapToDashboardViewModel } from '~/monitoring/stores/utils'; import { metricStates } from '~/monitoring/constants'; -import { convertObjectProps } from '~/lib/utils/common_utils'; -import { convertToCamelCase } from '~/lib/utils/text_utility'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { metricsResult } from './mock_data'; @@ -14,13 +13,7 @@ export const metricsDashboardResponse = getJSONFixture( export const metricsDashboardPayload = metricsDashboardResponse.dashboard; const datasetState = stateAndPropsFromDataset( - // It's preferable to have props in snake_case, this will be addressed at: - // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33574 - convertObjectProps( - // Some props use kebab-case, convert to snake_case first - key => convertToCamelCase(key.replace(/-/g, '_')), - metricsDashboardResponse.metrics_data, - ), + convertObjectPropsToCamelCase(metricsDashboardResponse.metrics_data), ); // new properties like addDashboardDocumentationPath prop and alertsEndpoint diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js index 10669330b61..6f3e12a0789 100644 --- a/spec/frontend/repository/components/table/index_spec.js +++ b/spec/frontend/repository/components/table/index_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlSkeletonLoading, GlButton } from '@gitlab/ui'; import Table from '~/repository/components/table/index.vue'; import TableRow from '~/repository/components/table/row.vue'; @@ -34,12 +34,13 @@ const MOCK_BLOBS = [ }, ]; -function factory({ path, isLoading = false, entries = {} }) { +function factory({ path, isLoading = false, hasMore = true, entries = {} }) { vm = shallowMount(Table, { propsData: { path, isLoading, entries, + hasMore, }, mocks: { $apollo, @@ -88,4 +89,27 @@ describe('Repository table component', () => { expect(rows.length).toEqual(3); expect(rows.at(2).attributes().mode).toEqual('120000'); }); + + describe('Show more button', () => { + const showMoreButton = () => vm.find(GlButton); + + it.each` + hasMore | expectButtonToExist + ${true} | ${true} + ${false} | ${false} + `('renders correctly', ({ hasMore, expectButtonToExist }) => { + factory({ path: '/', hasMore }); + expect(showMoreButton().exists()).toBe(expectButtonToExist); + }); + + it('when is clicked, emits showMore event', async () => { + factory({ path: '/' }); + + showMoreButton().vm.$emit('click'); + + await vm.vm.$nextTick(); + + expect(vm.emitted('showMore')).toHaveLength(1); + }); + }); }); diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js index ea85cd34743..70dbfaea551 100644 --- a/spec/frontend/repository/components/tree_content_spec.js +++ b/spec/frontend/repository/components/tree_content_spec.js @@ -1,7 +1,7 @@ import { shallowMount } from '@vue/test-utils'; -import { GlButton } from '@gitlab/ui'; import TreeContent, { INITIAL_FETCH_COUNT } from '~/repository/components/tree_content.vue'; import FilePreview from '~/repository/components/preview/index.vue'; +import FileTable from '~/repository/components/table/index.vue'; let vm; let $apollo; @@ -82,41 +82,36 @@ describe('Repository table component', () => { }); }); - describe('Show more button', () => { - const showMoreButton = () => vm.find(GlButton); - + describe('FileTable showMore', () => { describe('when is present', () => { + const fileTable = () => vm.find(FileTable); + beforeEach(async () => { factory('/'); - - vm.setData({ fetchCounter: 10, clickedShowMore: false }); - - await vm.vm.$nextTick(); }); - it('is not rendered once it is clicked', async () => { - showMoreButton().vm.$emit('click'); + it('is changes hasShowMore to false when "showMore" event is emitted', async () => { + fileTable().vm.$emit('showMore'); + await vm.vm.$nextTick(); - expect(showMoreButton().exists()).toBe(false); + expect(vm.vm.hasShowMore).toBe(false); }); - it('is rendered', async () => { - expect(showMoreButton().exists()).toBe(true); - }); + it('changes clickedShowMore when "showMore" event is emitted', async () => { + fileTable().vm.$emit('showMore'); - it('changes clickedShowMore when show more button is clicked', async () => { - showMoreButton().vm.$emit('click'); + await vm.vm.$nextTick(); expect(vm.vm.clickedShowMore).toBe(true); }); - it('triggers fetchFiles when show more button is clicked', async () => { + it('triggers fetchFiles when "showMore" event is emitted', () => { jest.spyOn(vm.vm, 'fetchFiles'); - showMoreButton().vm.$emit('click'); + fileTable().vm.$emit('showMore'); - expect(vm.vm.fetchFiles).toBeCalled(); + expect(vm.vm.fetchFiles).toHaveBeenCalled(); }); }); @@ -127,7 +122,7 @@ describe('Repository table component', () => { await vm.vm.$nextTick(); - expect(showMoreButton().exists()).toBe(false); + expect(vm.vm.hasShowMore).toBe(false); }); it('has limit of 1000 files on initial load', () => { diff --git a/spec/graphql/resolvers/project_members_resolver_spec.rb b/spec/graphql/resolvers/project_members_resolver_spec.rb index 602225cf632..91406d5c749 100644 --- a/spec/graphql/resolvers/project_members_resolver_spec.rb +++ b/spec/graphql/resolvers/project_members_resolver_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Resolvers::ProjectMembersResolver do let_it_be(:root_group) { create(:group) } let_it_be(:group_1) { create(:group, parent: root_group) } let_it_be(:group_2) { create(:group, parent: root_group) } - let_it_be(:project) { create(:project, :public, group: group_1) } + let_it_be(:project) { create(:project, group: group_1) } let_it_be(:user_1) { create(:user, name: 'test user') } let_it_be(:user_2) { create(:user, name: 'test user 2') } @@ -24,7 +24,7 @@ RSpec.describe Resolvers::ProjectMembersResolver do let(:args) { {} } subject do - resolve(described_class, obj: project, args: args, ctx: { context: user_4 }) + resolve(described_class, obj: project, args: args, ctx: { current_user: user_4 }) end describe '#resolve' do @@ -50,11 +50,15 @@ RSpec.describe Resolvers::ProjectMembersResolver do end end - context 'when project is nil' do - let(:project) { nil } + context 'when user can not see project members' do + let_it_be(:other_user) { create(:user) } - it 'returns nil' do - expect(subject).to be_empty + subject do + resolve(described_class, obj: project, args: args, ctx: { current_user: other_user }) + end + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) end end end diff --git a/spec/graphql/types/member_interface_spec.rb b/spec/graphql/types/member_interface_spec.rb new file mode 100644 index 00000000000..11fd09eb335 --- /dev/null +++ b/spec/graphql/types/member_interface_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::MemberInterface do + it 'exposes the expected fields' do + expected_fields = %i[ + id + access_level + created_by + created_at + updated_at + expires_at + user + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end + + describe '.resolve_type' do + subject { described_class.resolve_type(object, {}) } + + context 'for project member' do + let(:object) { build(:project_member) } + + it { is_expected.to be Types::ProjectMemberType } + end + + context 'for group member' do + let(:object) { build(:group_member) } + + it { is_expected.to be Types::GroupMemberType } + end + + context 'for an unkown type' do + let(:object) { build(:user) } + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::BaseError) + end + end + end +end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 8a5d0cdf12d..243aa1e68cc 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -108,7 +108,7 @@ RSpec.describe GitlabSchema.types['Project'] do describe 'members field' do subject { described_class.fields['projectMembers'] } - it { is_expected.to have_graphql_type(Types::ProjectMemberType.connection_type) } + it { is_expected.to have_graphql_type(Types::MemberInterface.connection_type) } it { is_expected.to have_graphql_resolver(Resolvers::ProjectMembersResolver) } end diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb index cb7d12b331a..d316f2b0a0a 100644 --- a/spec/helpers/environments_helper_spec.rb +++ b/spec/helpers/environments_helper_spec.rb @@ -18,34 +18,34 @@ RSpec.describe EnvironmentsHelper do it 'returns data' do expect(metrics_data).to include( - 'settings-path' => edit_project_service_path(project, 'prometheus'), - 'clusters-path' => project_clusters_path(project), - 'metrics-dashboard-base-path' => environment_metrics_path(environment), - 'current-environment-name' => environment.name, - 'documentation-path' => help_page_path('administration/monitoring/prometheus/index.md'), - 'add-dashboard-documentation-path' => help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'), - 'empty-getting-started-svg-path' => match_asset_path('/assets/illustrations/monitoring/getting_started.svg'), - 'empty-loading-svg-path' => match_asset_path('/assets/illustrations/monitoring/loading.svg'), - 'empty-no-data-svg-path' => match_asset_path('/assets/illustrations/monitoring/no_data.svg'), - 'empty-unable-to-connect-svg-path' => match_asset_path('/assets/illustrations/monitoring/unable_to_connect.svg'), - 'metrics-endpoint' => additional_metrics_project_environment_path(project, environment, format: :json), - 'deployments-endpoint' => project_environment_deployments_path(project, environment, format: :json), - 'default-branch' => 'master', - 'project-path' => project_path(project), - 'tags-path' => project_tags_path(project), - 'has-metrics' => "#{environment.has_metrics?}", - 'prometheus-status' => "#{environment.prometheus_status}", - 'external-dashboard-url' => nil, - 'environment-state' => environment.state, - 'custom-metrics-path' => project_prometheus_metrics_path(project), - 'validate-query-path' => validate_query_project_prometheus_metrics_path(project), - 'custom-metrics-available' => 'true', - 'alerts-endpoint' => project_prometheus_alerts_path(project, environment_id: environment.id, format: :json), - 'prometheus-alerts-available' => 'true', - 'custom-dashboard-base-path' => Gitlab::Metrics::Dashboard::RepoDashboardFinder::DASHBOARD_ROOT, - 'operations-settings-path' => project_settings_operations_path(project), - 'can-access-operations-settings' => 'true', - 'panel-preview-endpoint' => project_metrics_dashboards_builder_path(project, format: :json) + 'settings_path' => edit_project_service_path(project, 'prometheus'), + 'clusters_path' => project_clusters_path(project), + 'metrics_dashboard_base_path' => environment_metrics_path(environment), + 'current_environment_name' => environment.name, + 'documentation_path' => help_page_path('administration/monitoring/prometheus/index.md'), + 'add_dashboard_documentation_path' => help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'), + 'empty_getting_started_svg_path' => match_asset_path('/assets/illustrations/monitoring/getting_started.svg'), + 'empty_loading_svg_path' => match_asset_path('/assets/illustrations/monitoring/loading.svg'), + 'empty_no_data_svg_path' => match_asset_path('/assets/illustrations/monitoring/no_data.svg'), + 'empty_unable_to_connect_svg_path' => match_asset_path('/assets/illustrations/monitoring/unable_to_connect.svg'), + 'metrics_endpoint' => additional_metrics_project_environment_path(project, environment, format: :json), + 'deployments_endpoint' => project_environment_deployments_path(project, environment, format: :json), + 'default_branch' => 'master', + 'project_path' => project_path(project), + 'tags_path' => project_tags_path(project), + 'has_metrics' => "#{environment.has_metrics?}", + 'prometheus_status' => "#{environment.prometheus_status}", + 'external_dashboard_url' => nil, + 'environment_state' => environment.state, + 'custom_metrics_path' => project_prometheus_metrics_path(project), + 'validate_query_path' => validate_query_project_prometheus_metrics_path(project), + 'custom_metrics_available' => 'true', + 'alerts_endpoint' => project_prometheus_alerts_path(project, environment_id: environment.id, format: :json), + 'prometheus_alerts_available' => 'true', + 'custom_dashboard_base_path' => Gitlab::Metrics::Dashboard::RepoDashboardFinder::DASHBOARD_ROOT, + 'operations_settings_path' => project_settings_operations_path(project), + 'can_access_operations_settings' => 'true', + 'panel_preview_endpoint' => project_metrics_dashboards_builder_path(project, format: :json) ) end @@ -58,7 +58,7 @@ RSpec.describe EnvironmentsHelper do specify do expect(metrics_data).to include( - 'can-access-operations-settings' => 'false' + 'can_access_operations_settings' => 'false' ) end end @@ -72,7 +72,7 @@ RSpec.describe EnvironmentsHelper do it 'returns false' do expect(metrics_data).to include( - 'prometheus-alerts-available' => 'false' + 'prometheus_alerts_available' => 'false' ) end end @@ -83,7 +83,7 @@ RSpec.describe EnvironmentsHelper do end it 'adds external_dashboard_url' do - expect(metrics_data['external-dashboard-url']).to eq('http://gitlab.com') + expect(metrics_data['external_dashboard_url']).to eq('http://gitlab.com') end end @@ -94,7 +94,7 @@ RSpec.describe EnvironmentsHelper do subject { metrics_data } - it { is_expected.to include('environment-state' => 'stopped') } + it { is_expected.to include('environment_state' => 'stopped') } end context 'when request is from project scoped metrics path' do @@ -107,16 +107,16 @@ RSpec.describe EnvironmentsHelper do context '/:namespace/:project/-/metrics' do let(:path) { project_metrics_dashboard_path(project) } - it 'uses correct path for metrics-dashboard-base-path' do - expect(metrics_data['metrics-dashboard-base-path']).to eq(project_metrics_dashboard_path(project)) + it 'uses correct path for metrics_dashboard_base_path' do + expect(metrics_data['metrics_dashboard_base_path']).to eq(project_metrics_dashboard_path(project)) end end context '/:namespace/:project/-/metrics/some_custom_dashboard.yml' do let(:path) { "#{project_metrics_dashboard_path(project)}/some_custom_dashboard.yml" } - it 'uses correct path for metrics-dashboard-base-path' do - expect(metrics_data['metrics-dashboard-base-path']).to eq(project_metrics_dashboard_path(project)) + it 'uses correct path for metrics_dashboard_base_path' do + expect(metrics_data['metrics_dashboard_base_path']).to eq(project_metrics_dashboard_path(project)) end end end @@ -143,11 +143,11 @@ RSpec.describe EnvironmentsHelper do describe '#environment_logs_data' do it 'returns logs data' do expected_data = { - "environment-name": environment.name, - "environments-path": project_environments_path(project, format: :json), - "environment-id": environment.id, - "cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack'), - "clusters-path": project_clusters_path(project, format: :json) + "environment_name": environment.name, + "environments_path": project_environments_path(project, format: :json), + "environment_id": environment.id, + "cluster_applications_documentation_path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack'), + "clusters_path": project_clusters_path(project, format: :json) } expect(helper.environment_logs_data(project, environment)).to eq(expected_data) diff --git a/spec/lib/gitlab/kas_spec.rb b/spec/lib/gitlab/kas_spec.rb new file mode 100644 index 00000000000..ce22f36e9fd --- /dev/null +++ b/spec/lib/gitlab/kas_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Kas do + let(:jwt_secret) { SecureRandom.random_bytes(described_class::SECRET_LENGTH) } + + before do + allow(described_class).to receive(:secret).and_return(jwt_secret) + end + + describe '.verify_api_request' do + let(:payload) { { 'iss' => described_class::JWT_ISSUER } } + + it 'returns nil if fails to validate the JWT' do + encoded_token = JWT.encode(payload, 'wrongsecret', 'HS256') + headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token } + + expect(described_class.verify_api_request(headers)).to be_nil + end + + it 'returns the decoded JWT' do + encoded_token = JWT.encode(payload, described_class.secret, 'HS256') + headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token } + + expect(described_class.verify_api_request(headers)).to eq([{ "iss" => described_class::JWT_ISSUER }, { "alg" => "HS256" }]) + end + end + + describe '.secret_path' do + it 'returns default gitlab config' do + expect(described_class.secret_path).to eq(Gitlab.config.gitlab_kas.secret_file) + end + end + + describe '.ensure_secret!' do + context 'secret file exists' do + before do + allow(File).to receive(:exist?).with(Gitlab.config.gitlab_kas.secret_file).and_return(true) + end + + it 'does not call write_secret' do + expect(described_class).not_to receive(:write_secret) + + described_class.ensure_secret! + end + end + + context 'secret file does not exist' do + before do + allow(File).to receive(:exist?).with(Gitlab.config.gitlab_kas.secret_file).and_return(false) + end + + it 'calls write_secret' do + expect(described_class).to receive(:write_secret) + + described_class.ensure_secret! + end + end + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 656a93cee62..da84b2b6a8f 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1257,10 +1257,8 @@ RSpec.describe MergeRequest do let(:repository) { merge_request.source_project.repository } context 'when the source project is set' do - it 'memoizes the value and returns the result' do - expect(repository).to receive(:branch_exists?).once.with(merge_request.source_branch).and_return(true) - - 2.times { expect(merge_request.source_branch_exists?).to eq(true) } + it 'returns true when the branch exists' do + expect(merge_request.source_branch_exists?).to eq(true) end end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index c4a9c0329c7..d001b6ec0f1 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -138,16 +138,16 @@ RSpec.describe Service do describe '#can_test?' do subject { service.can_test? } - let(:service) { create(:service, project: project) } + let(:service) { build(:service, project: project) } context 'when repository is not empty' do - let(:project) { create(:project, :repository) } + let(:project) { build(:project, :repository) } it { is_expected.to be true } end context 'when repository is empty' do - let(:project) { create(:project) } + let(:project) { build(:project) } it { is_expected.to be true } end @@ -165,10 +165,10 @@ RSpec.describe Service do describe '#test' do let(:data) { 'test' } - let(:service) { create(:service, project: project) } + let(:service) { build(:service, project: project) } context 'when repository is not empty' do - let(:project) { create(:project, :repository) } + let(:project) { build(:project, :repository) } it 'test runs execute' do expect(service).to receive(:execute).with(data) @@ -178,7 +178,7 @@ RSpec.describe Service do end context 'when repository is empty' do - let(:project) { create(:project) } + let(:project) { build(:project) } it 'test runs execute' do expect(service).to receive(:execute).with(data) @@ -622,8 +622,8 @@ RSpec.describe Service do end context 'logging' do - let(:project) { create(:project) } - let(:service) { create(:service, project: project) } + let(:project) { build(:project) } + let(:service) { build(:service, project: project) } let(:test_message) { "test message" } let(:arguments) do { diff --git a/spec/requests/api/composer_packages_spec.rb b/spec/requests/api/composer_packages_spec.rb index f5b8ebb545b..f5279af0483 100644 --- a/spec/requests/api/composer_packages_spec.rb +++ b/spec/requests/api/composer_packages_spec.rb @@ -26,30 +26,61 @@ RSpec.describe API::ComposerPackages do group.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) end - where(:project_visibility_level, :user_role, :member, :user_token, :include_package) do - 'PUBLIC' | :developer | true | true | :include_package - 'PUBLIC' | :developer | true | false | :include_package - 'PUBLIC' | :developer | false | false | :include_package - 'PUBLIC' | :developer | false | true | :include_package - 'PUBLIC' | :guest | true | true | :include_package - 'PUBLIC' | :guest | true | false | :include_package - 'PUBLIC' | :guest | false | true | :include_package - 'PUBLIC' | :guest | false | false | :include_package - 'PUBLIC' | :anonymous | false | true | :include_package - 'PRIVATE' | :developer | true | true | :include_package - 'PRIVATE' | :developer | true | false | :does_not_include_package - 'PRIVATE' | :developer | false | true | :does_not_include_package - 'PRIVATE' | :developer | false | false | :does_not_include_package - 'PRIVATE' | :guest | true | true | :does_not_include_package - 'PRIVATE' | :guest | true | false | :does_not_include_package - 'PRIVATE' | :guest | false | true | :does_not_include_package - 'PRIVATE' | :guest | false | false | :does_not_include_package - 'PRIVATE' | :anonymous | false | true | :does_not_include_package + context 'with basic auth' do + where(:project_visibility_level, :user_role, :member, :user_token, :include_package) do + 'PUBLIC' | :developer | true | true | :include_package + 'PUBLIC' | :developer | false | true | :include_package + 'PUBLIC' | :guest | true | true | :include_package + 'PUBLIC' | :guest | false | true | :include_package + 'PUBLIC' | :anonymous | false | true | :include_package + 'PRIVATE' | :developer | true | true | :include_package + 'PRIVATE' | :developer | false | true | :does_not_include_package + 'PRIVATE' | :guest | true | true | :does_not_include_package + 'PRIVATE' | :guest | false | true | :does_not_include_package + 'PRIVATE' | :anonymous | false | true | :does_not_include_package + 'PRIVATE' | :guest | false | false | :does_not_include_package + 'PRIVATE' | :guest | true | false | :does_not_include_package + 'PRIVATE' | :developer | false | false | :does_not_include_package + 'PRIVATE' | :developer | true | false | :does_not_include_package + 'PUBLIC' | :developer | true | false | :include_package + 'PUBLIC' | :guest | true | false | :include_package + 'PUBLIC' | :developer | false | false | :include_package + 'PUBLIC' | :guest | false | false | :include_package + end + + with_them do + include_context 'Composer api project access', params[:project_visibility_level], params[:user_role], params[:user_token], :basic do + it_behaves_like 'Composer package index', params[:user_role], :success, params[:member], params[:include_package] + end + end end - with_them do - include_context 'Composer api project access', params[:project_visibility_level], params[:user_role], params[:user_token] do - it_behaves_like 'Composer package index', params[:user_role], :success, params[:member], params[:include_package] + context 'with private token header auth' do + where(:project_visibility_level, :user_role, :member, :user_token, :expected_status, :include_package) do + 'PUBLIC' | :developer | true | true | :success | :include_package + 'PUBLIC' | :developer | false | true | :success | :include_package + 'PUBLIC' | :guest | true | true | :success | :include_package + 'PUBLIC' | :guest | false | true | :success | :include_package + 'PUBLIC' | :anonymous | false | true | :success | :include_package + 'PRIVATE' | :developer | true | true | :success | :include_package + 'PRIVATE' | :developer | false | true | :success | :does_not_include_package + 'PRIVATE' | :guest | true | true | :success | :does_not_include_package + 'PRIVATE' | :guest | false | true | :success | :does_not_include_package + 'PRIVATE' | :anonymous | false | true | :success | :does_not_include_package + 'PRIVATE' | :guest | false | false | :unauthorized | nil + 'PRIVATE' | :guest | true | false | :unauthorized | nil + 'PRIVATE' | :developer | false | false | :unauthorized | nil + 'PRIVATE' | :developer | true | false | :unauthorized | nil + 'PUBLIC' | :developer | true | false | :unauthorized | nil + 'PUBLIC' | :guest | true | false | :unauthorized | nil + 'PUBLIC' | :developer | false | false | :unauthorized | nil + 'PUBLIC' | :guest | false | false | :unauthorized | nil + end + + with_them do + include_context 'Composer api project access', params[:project_visibility_level], params[:user_role], params[:user_token], :token do + it_behaves_like 'Composer package index', params[:user_role], params[:expected_status], params[:member], params[:include_package] + end end end end @@ -105,22 +136,22 @@ RSpec.describe API::ComposerPackages do context 'with valid project' do where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do 'PUBLIC' | :developer | true | true | 'Composer provider index' | :success - 'PUBLIC' | :developer | true | false | 'Composer provider index' | :success + 'PUBLIC' | :developer | true | false | 'process Composer api request' | :unauthorized 'PUBLIC' | :developer | false | true | 'Composer provider index' | :success - 'PUBLIC' | :developer | false | false | 'Composer provider index' | :success + 'PUBLIC' | :developer | false | false | 'process Composer api request' | :unauthorized 'PUBLIC' | :guest | true | true | 'Composer provider index' | :success - 'PUBLIC' | :guest | true | false | 'Composer provider index' | :success + 'PUBLIC' | :guest | true | false | 'process Composer api request' | :unauthorized 'PUBLIC' | :guest | false | true | 'Composer provider index' | :success - 'PUBLIC' | :guest | false | false | 'Composer provider index' | :success + 'PUBLIC' | :guest | false | false | 'process Composer api request' | :unauthorized 'PUBLIC' | :anonymous | false | true | 'Composer provider index' | :success 'PRIVATE' | :developer | true | true | 'Composer provider index' | :success - 'PRIVATE' | :developer | true | false | 'process Composer api request' | :not_found + 'PRIVATE' | :developer | true | false | 'process Composer api request' | :unauthorized 'PRIVATE' | :developer | false | true | 'process Composer api request' | :not_found - 'PRIVATE' | :developer | false | false | 'process Composer api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process Composer api request' | :unauthorized 'PRIVATE' | :guest | true | true | 'Composer empty provider index' | :success - 'PRIVATE' | :guest | true | false | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | true | false | 'process Composer api request' | :unauthorized 'PRIVATE' | :guest | false | true | 'process Composer api request' | :not_found - 'PRIVATE' | :guest | false | false | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | false | false | 'process Composer api request' | :unauthorized 'PRIVATE' | :anonymous | false | true | 'process Composer api request' | :not_found end @@ -151,22 +182,22 @@ RSpec.describe API::ComposerPackages do where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do 'PUBLIC' | :developer | true | true | 'Composer package api request' | :success - 'PUBLIC' | :developer | true | false | 'Composer package api request' | :success + 'PUBLIC' | :developer | true | false | 'process Composer api request' | :unauthorized 'PUBLIC' | :developer | false | true | 'Composer package api request' | :success - 'PUBLIC' | :developer | false | false | 'Composer package api request' | :success + 'PUBLIC' | :developer | false | false | 'process Composer api request' | :unauthorized 'PUBLIC' | :guest | true | true | 'Composer package api request' | :success - 'PUBLIC' | :guest | true | false | 'Composer package api request' | :success + 'PUBLIC' | :guest | true | false | 'process Composer api request' | :unauthorized 'PUBLIC' | :guest | false | true | 'Composer package api request' | :success - 'PUBLIC' | :guest | false | false | 'Composer package api request' | :success + 'PUBLIC' | :guest | false | false | 'process Composer api request' | :unauthorized 'PUBLIC' | :anonymous | false | true | 'Composer package api request' | :success 'PRIVATE' | :developer | true | true | 'Composer package api request' | :success - 'PRIVATE' | :developer | true | false | 'process Composer api request' | :not_found + 'PRIVATE' | :developer | true | false | 'process Composer api request' | :unauthorized 'PRIVATE' | :developer | false | true | 'process Composer api request' | :not_found - 'PRIVATE' | :developer | false | false | 'process Composer api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process Composer api request' | :unauthorized 'PRIVATE' | :guest | true | true | 'process Composer api request' | :not_found - 'PRIVATE' | :guest | true | false | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | true | false | 'process Composer api request' | :unauthorized 'PRIVATE' | :guest | false | true | 'process Composer api request' | :not_found - 'PRIVATE' | :guest | false | false | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | false | false | 'process Composer api request' | :unauthorized 'PRIVATE' | :anonymous | false | true | 'process Composer api request' | :not_found end diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb index c6049e098be..4b8ffb0675c 100644 --- a/spec/requests/api/graphql/project_query_spec.rb +++ b/spec/requests/api/graphql/project_query_spec.rb @@ -5,7 +5,8 @@ require 'spec_helper' RSpec.describe 'getting project information' do include GraphqlHelpers - let(:project) { create(:project, :repository) } + let(:group) { create(:group) } + let(:project) { create(:project, :repository, group: group) } let(:current_user) { create(:user) } let(:query) do @@ -60,6 +61,51 @@ RSpec.describe 'getting project information' do expect(graphql_data['project']['pipelines']['edges'].size).to eq(1) end end + + it 'includes inherited members in project_members' do + group_member = create(:group_member, group: group) + project_member = create(:project_member, project: project) + member_query = <<~GQL + query { + project(fullPath: "#{project.full_path}") { + projectMembers { + nodes { + id + user { + username + } + ... on ProjectMember { + project { + id + } + } + ... on GroupMember { + group { + id + } + } + } + } + } + } + GQL + + post_graphql(member_query, current_user: current_user) + + member_ids = graphql_data.dig('project', 'projectMembers', 'nodes') + expect(member_ids).to include( + a_hash_including( + 'id' => group_member.to_global_id.to_s, + 'group' => { 'id' => group.to_global_id.to_s } + ) + ) + expect(member_ids).to include( + a_hash_including( + 'id' => project_member.to_global_id.to_s, + 'project' => { 'id' => project.to_global_id.to_s } + ) + ) + end end describe 'performance' do diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb index 555ca441fe7..a802e3a858a 100644 --- a/spec/requests/api/internal/kubernetes_spec.rb +++ b/spec/requests/api/internal/kubernetes_spec.rb @@ -3,21 +3,45 @@ require 'spec_helper' RSpec.describe API::Internal::Kubernetes do + let(:jwt_auth_headers) do + jwt_token = JWT.encode({ 'iss' => Gitlab::Kas::JWT_ISSUER }, Gitlab::Kas.secret, 'HS256') + + { Gitlab::Kas::INTERNAL_API_REQUEST_HEADER => jwt_token } + end + + let(:jwt_secret) { SecureRandom.random_bytes(Gitlab::Kas::SECRET_LENGTH) } + + before do + allow(Gitlab::Kas).to receive(:secret).and_return(jwt_secret) + end + describe "GET /internal/kubernetes/agent_info" do + def send_request(headers: {}, params: {}) + get api('/internal/kubernetes/agent_info'), params: params, headers: headers.reverse_merge(jwt_auth_headers) + end + + context 'not authenticated' do + it 'returns 401' do + send_request(headers: { Gitlab::Kas::INTERNAL_API_REQUEST_HEADER => '' }) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + context 'kubernetes_agent_internal_api feature flag disabled' do before do stub_feature_flags(kubernetes_agent_internal_api: false) end it 'returns 404' do - get api('/internal/kubernetes/agent_info') + send_request expect(response).to have_gitlab_http_status(:not_found) end end it 'returns 403 if Authorization header not sent' do - get api('/internal/kubernetes/agent_info') + send_request expect(response).to have_gitlab_http_status(:forbidden) end @@ -29,7 +53,7 @@ RSpec.describe API::Internal::Kubernetes do let(:project) { agent.project } it 'returns expected data', :aggregate_failures do - get api('/internal/kubernetes/agent_info'), headers: { 'Authorization' => "Bearer #{agent_token.token}" } + send_request(headers: { 'Authorization' => "Bearer #{agent_token.token}" }) expect(response).to have_gitlab_http_status(:success) @@ -56,7 +80,7 @@ RSpec.describe API::Internal::Kubernetes do context 'no such agent exists' do it 'returns 404' do - get api('/internal/kubernetes/agent_info'), headers: { 'Authorization' => 'Bearer ABCD' } + send_request(headers: { 'Authorization' => 'Bearer ABCD' }) expect(response).to have_gitlab_http_status(:forbidden) end @@ -64,27 +88,39 @@ RSpec.describe API::Internal::Kubernetes do end describe 'GET /internal/kubernetes/project_info' do + def send_request(headers: {}, params: {}) + get api('/internal/kubernetes/project_info'), params: params, headers: headers.reverse_merge(jwt_auth_headers) + end + + context 'not authenticated' do + it 'returns 401' do + send_request(headers: { Gitlab::Kas::INTERNAL_API_REQUEST_HEADER => '' }) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + context 'kubernetes_agent_internal_api feature flag disabled' do before do stub_feature_flags(kubernetes_agent_internal_api: false) end it 'returns 404' do - get api('/internal/kubernetes/project_info') + send_request expect(response).to have_gitlab_http_status(:not_found) end end it 'returns 403 if Authorization header not sent' do - get api('/internal/kubernetes/project_info') + send_request expect(response).to have_gitlab_http_status(:forbidden) end context 'no such agent exists' do it 'returns 404' do - get api('/internal/kubernetes/project_info'), headers: { 'Authorization' => 'Bearer ABCD' } + send_request(headers: { 'Authorization' => 'Bearer ABCD' }) expect(response).to have_gitlab_http_status(:forbidden) end @@ -99,7 +135,7 @@ RSpec.describe API::Internal::Kubernetes do let(:project) { create(:project, :public) } it 'returns expected data', :aggregate_failures do - get api('/internal/kubernetes/project_info'), params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" } + send_request(params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }) expect(response).to have_gitlab_http_status(:success) @@ -126,7 +162,7 @@ RSpec.describe API::Internal::Kubernetes do let(:project) { create(:project, :private) } it 'returns 404' do - get api('/internal/kubernetes/project_info'), params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" } + send_request(params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }) expect(response).to have_gitlab_http_status(:not_found) end @@ -136,7 +172,7 @@ RSpec.describe API::Internal::Kubernetes do let(:project) { create(:project, :internal) } it 'returns 404' do - get api('/internal/kubernetes/project_info'), params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" } + send_request(params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }) expect(response).to have_gitlab_http_status(:not_found) end @@ -144,7 +180,7 @@ RSpec.describe API::Internal::Kubernetes do context 'project does not exist' do it 'returns 404' do - get api('/internal/kubernetes/project_info'), params: { id: 0 }, headers: { 'Authorization' => "Bearer #{agent_token.token}" } + send_request(params: { id: 0 }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }) expect(response).to have_gitlab_http_status(:not_found) end diff --git a/spec/services/merge_requests/conflicts/list_service_spec.rb b/spec/services/merge_requests/conflicts/list_service_spec.rb index 14133731e37..cd9eae2f2ee 100644 --- a/spec/services/merge_requests/conflicts/list_service_spec.rb +++ b/spec/services/merge_requests/conflicts/list_service_spec.rb @@ -30,7 +30,6 @@ RSpec.describe MergeRequests::Conflicts::ListService do it 'returns a falsey value when one of the MR branches is missing' do merge_request = create_merge_request('conflict-resolvable') merge_request.project.repository.rm_branch(merge_request.author, 'conflict-resolvable') - merge_request.clear_memoized_source_branch_exists expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey end diff --git a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb index 09743c20fba..5c122b4b5d6 100644 --- a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb @@ -16,8 +16,11 @@ RSpec.shared_examples 'Composer package index' do |user_type, status, add_member subject expect(response).to have_gitlab_http_status(status) - expect(response).to match_response_schema('public_api/v4/packages/composer/index') - expect(json_response).to eq presenter.root + + if status == :success + expect(response).to match_response_schema('public_api/v4/packages/composer/index') + expect(json_response).to eq presenter.root + end end end end @@ -87,13 +90,22 @@ RSpec.shared_examples 'process Composer api request' do |user_type, status, add_ end end -RSpec.shared_context 'Composer auth headers' do |user_role, user_token| +RSpec.shared_context 'Composer auth headers' do |user_role, user_token, auth_method = :token| let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + + let(:headers) do + if user_role == :anonymous + {} + elsif auth_method == :token + { 'Private-Token' => token } + else + basic_auth_header(user.username, token) + end + end end -RSpec.shared_context 'Composer api project access' do |project_visibility_level, user_role, user_token| - include_context 'Composer auth headers', user_role, user_token do +RSpec.shared_context 'Composer api project access' do |project_visibility_level, user_role, user_token, auth_method| + include_context 'Composer auth headers', user_role, user_token, auth_method do before do project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) end |