diff options
47 files changed, 491 insertions, 66 deletions
diff --git a/app/assets/javascripts/static_site_editor/components/static_site_editor.vue b/app/assets/javascripts/static_site_editor/components/static_site_editor.vue index 4a0e153eb33..f06d48ee4f5 100644 --- a/app/assets/javascripts/static_site_editor/components/static_site_editor.vue +++ b/app/assets/javascripts/static_site_editor/components/static_site_editor.vue @@ -1,5 +1,5 @@ <script> -import { mapState, mapActions } from 'vuex'; +import { mapState, mapGetters, mapActions } from 'vuex'; import { GlSkeletonLoader } from '@gitlab/ui'; import EditArea from './edit_area.vue'; @@ -10,7 +10,8 @@ export default { GlSkeletonLoader, }, computed: { - ...mapState(['content', 'isContentLoaded', 'isLoadingContent']), + ...mapState(['content', 'isLoadingContent']), + ...mapGetters(['isContentLoaded']), }, mounted() { this.loadContent(); diff --git a/app/assets/javascripts/static_site_editor/index.js b/app/assets/javascripts/static_site_editor/index.js index 4290cb9a9ba..22f96a60df0 100644 --- a/app/assets/javascripts/static_site_editor/index.js +++ b/app/assets/javascripts/static_site_editor/index.js @@ -3,7 +3,11 @@ import StaticSiteEditor from './components/static_site_editor.vue'; import createStore from './store'; const initStaticSiteEditor = el => { - const store = createStore(); + const { projectId, path: sourcePath } = el.dataset; + + const store = createStore({ + initialState: { projectId, sourcePath }, + }); return new Vue({ el, diff --git a/app/assets/javascripts/static_site_editor/store/actions.js b/app/assets/javascripts/static_site_editor/store/actions.js new file mode 100644 index 00000000000..192345f3749 --- /dev/null +++ b/app/assets/javascripts/static_site_editor/store/actions.js @@ -0,0 +1,18 @@ +import createFlash from '~/flash'; +import { __ } from '~/locale'; + +import * as mutationTypes from './mutation_types'; +import loadSourceContent from '~/static_site_editor/services/load_source_content'; + +export const loadContent = ({ commit, state: { sourcePath, projectId } }) => { + commit(mutationTypes.LOAD_CONTENT); + + return loadSourceContent({ sourcePath, projectId }) + .then(data => commit(mutationTypes.RECEIVE_CONTENT_SUCCESS, data)) + .catch(() => { + commit(mutationTypes.RECEIVE_CONTENT_ERROR); + createFlash(__('An error ocurred while loading your content. Please try again.')); + }); +}; + +export default () => {}; diff --git a/app/assets/javascripts/static_site_editor/store/getters.js b/app/assets/javascripts/static_site_editor/store/getters.js new file mode 100644 index 00000000000..8baa2941594 --- /dev/null +++ b/app/assets/javascripts/static_site_editor/store/getters.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export const isContentLoaded = ({ content }) => Boolean(content); diff --git a/app/assets/javascripts/static_site_editor/store/index.js b/app/assets/javascripts/static_site_editor/store/index.js index 653c2532ee6..43256979ddd 100644 --- a/app/assets/javascripts/static_site_editor/store/index.js +++ b/app/assets/javascripts/static_site_editor/store/index.js @@ -1,12 +1,18 @@ import Vuex from 'vuex'; import Vue from 'vue'; import createState from './state'; +import * as getters from './getters'; +import * as actions from './actions'; +import mutations from './mutations'; Vue.use(Vuex); const createStore = ({ initialState } = {}) => { return new Vuex.Store({ state: createState(initialState), + getters, + actions, + mutations, }); }; diff --git a/app/assets/javascripts/static_site_editor/store/mutation_types.js b/app/assets/javascripts/static_site_editor/store/mutation_types.js new file mode 100644 index 00000000000..cbe51180541 --- /dev/null +++ b/app/assets/javascripts/static_site_editor/store/mutation_types.js @@ -0,0 +1,3 @@ +export const LOAD_CONTENT = 'loadContent'; +export const RECEIVE_CONTENT_SUCCESS = 'receiveContentSuccess'; +export const RECEIVE_CONTENT_ERROR = 'receiveContentError'; diff --git a/app/assets/javascripts/static_site_editor/store/mutations.js b/app/assets/javascripts/static_site_editor/store/mutations.js new file mode 100644 index 00000000000..88cb74d2b11 --- /dev/null +++ b/app/assets/javascripts/static_site_editor/store/mutations.js @@ -0,0 +1,15 @@ +import * as types from './mutation_types'; + +export default { + [types.LOAD_CONTENT](state) { + state.isLoadingContent = true; + }, + [types.RECEIVE_CONTENT_SUCCESS](state, { title, content }) { + state.isLoadingContent = false; + state.title = title; + state.content = content; + }, + [types.RECEIVE_CONTENT_ERROR](state) { + state.isLoadingContent = false; + }, +}; diff --git a/app/assets/javascripts/static_site_editor/store/state.js b/app/assets/javascripts/static_site_editor/store/state.js index 68a7f95760c..b68e73f06f5 100644 --- a/app/assets/javascripts/static_site_editor/store/state.js +++ b/app/assets/javascripts/static_site_editor/store/state.js @@ -1,8 +1,12 @@ const createState = (initialState = {}) => ({ + projectId: null, + sourcePath: null, + isLoadingContent: false, - isContentLoaded: false, content: '', + title: '', + ...initialState, }); diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb index 0e05318b253..15dc1ca8954 100644 --- a/app/models/ci/group.rb +++ b/app/models/ci/group.rb @@ -11,11 +11,12 @@ module Ci include StaticModel include Gitlab::Utils::StrongMemoize - attr_reader :stage, :name, :jobs + attr_reader :project, :stage, :name, :jobs delegate :size, to: :jobs - def initialize(stage, name:, jobs:) + def initialize(project, stage, name:, jobs:) + @project = project @stage = stage @name = name @jobs = jobs @@ -23,7 +24,7 @@ module Ci def status strong_memoize(:status) do - if Feature.enabled?(:ci_composite_status, default_enabled: false) + if Feature.enabled?(:ci_composite_status, project, default_enabled: false) Gitlab::Ci::Status::Composite .new(@jobs) .status @@ -44,11 +45,11 @@ module Ci end end - def self.fabricate(stage) + def self.fabricate(project, stage) stage.statuses.ordered.latest .sort_by(&:sortable_name).group_by(&:group_name) .map do |group_name, grouped_statuses| - self.new(stage, name: group_name, jobs: grouped_statuses) + self.new(project, stage, name: group_name, jobs: grouped_statuses) end end end diff --git a/app/models/ci/legacy_stage.rb b/app/models/ci/legacy_stage.rb index 9ca5cf13907..f156219ea81 100644 --- a/app/models/ci/legacy_stage.rb +++ b/app/models/ci/legacy_stage.rb @@ -20,7 +20,7 @@ module Ci end def groups - @groups ||= Ci::Group.fabricate(self) + @groups ||= Ci::Group.fabricate(project, self) end def to_param diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index e6c34f3df03..93bd42f8734 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -109,7 +109,7 @@ module Ci end def groups - @groups ||= Ci::Group.fabricate(self) + @groups ||= Ci::Group.fabricate(project, self) end def has_warnings? diff --git a/app/models/concerns/ci/maskable.rb b/app/models/concerns/ci/maskable.rb index 15bc48bf964..4e0ee72f18f 100644 --- a/app/models/concerns/ci/maskable.rb +++ b/app/models/concerns/ci/maskable.rb @@ -9,9 +9,9 @@ module Ci # * No variables # * No spaces # * Minimal length of 8 characters - # * Characters must be from the Base64 alphabet (RFC4648) with the addition of @ and : + # * Characters must be from the Base64 alphabet (RFC4648) with the addition of '@', ':' and '.' # * Absolutely no fun is allowed - REGEX = /\A[a-zA-Z0-9_+=\/@:-]{8,}\z/.freeze + REGEX = /\A[a-zA-Z0-9_+=\/@:.-]{8,}\z/.freeze included do validates :masked, inclusion: { in: [true, false] } diff --git a/app/serializers/analytics_summary_entity.rb b/app/serializers/analytics_summary_entity.rb index 39c6b4b06b2..b9797bfb021 100644 --- a/app/serializers/analytics_summary_entity.rb +++ b/app/serializers/analytics_summary_entity.rb @@ -3,4 +3,5 @@ class AnalyticsSummaryEntity < Grape::Entity expose :value, safe: true expose :title + expose :unit, if: { with_unit: true } end diff --git a/app/services/ci/external_pull_requests/create_pipeline_service.rb b/app/services/ci/external_pull_requests/create_pipeline_service.rb new file mode 100644 index 00000000000..78be94bfb41 --- /dev/null +++ b/app/services/ci/external_pull_requests/create_pipeline_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# This service is responsible for creating a pipeline for a given +# ExternalPullRequest coming from other providers such as GitHub. + +module Ci + module ExternalPullRequests + class CreatePipelineService < BaseService + def execute(pull_request) + return unless pull_request.open? && pull_request.actual_branch_head? + + create_pipeline_for(pull_request) + end + + private + + def create_pipeline_for(pull_request) + Ci::CreatePipelineService.new(project, current_user, create_params(pull_request)) + .execute(:external_pull_request_event, external_pull_request: pull_request) + end + + def create_params(pull_request) + { + ref: pull_request.source_ref, + source_sha: pull_request.source_sha, + target_sha: pull_request.target_sha + } + end + end + end +end diff --git a/app/services/external_pull_requests/create_pipeline_service.rb b/app/services/external_pull_requests/create_pipeline_service.rb deleted file mode 100644 index 36411465ff1..00000000000 --- a/app/services/external_pull_requests/create_pipeline_service.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -# This service is responsible for creating a pipeline for a given -# ExternalPullRequest coming from other providers such as GitHub. - -module ExternalPullRequests - class CreatePipelineService < BaseService - def execute(pull_request) - return unless pull_request.open? && pull_request.actual_branch_head? - - create_pipeline_for(pull_request) - end - - private - - def create_pipeline_for(pull_request) - Ci::CreatePipelineService.new(project, current_user, create_params(pull_request)) - .execute(:external_pull_request_event, external_pull_request: pull_request) - end - - def create_params(pull_request) - { - ref: pull_request.source_ref, - source_sha: pull_request.source_sha, - target_sha: pull_request.target_sha - } - end - end -end diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml index 44554cab1e9..47e7e27de48 100644 --- a/app/views/groups/_activities.html.haml +++ b/app/views/groups/_activities.html.haml @@ -5,4 +5,6 @@ %i.fa.fa-rss .content_list -= spinner +.loading + .spinner.spinner-md + diff --git a/app/views/projects/settings/_general.html.haml b/app/views/projects/settings/_general.html.haml index 520f342f567..0f60fc18026 100644 --- a/app/views/projects/settings/_general.html.haml +++ b/app/views/projects/settings/_general.html.haml @@ -20,6 +20,8 @@ = f.text_field :tag_list, value: @project.tag_list.join(', '), maxlength: 2000, class: "form-control" %p.form-text.text-muted= _('Separate topics with commas.') + = render_if_exists 'compliance_management/compliance_framework/project_settings', f: f + .row .form-group.col-md-9 = f.label :description, _('Project description (optional)'), class: 'label-bold' diff --git a/app/workers/update_external_pull_requests_worker.rb b/app/workers/update_external_pull_requests_worker.rb index b459d26e487..0d48877e1b0 100644 --- a/app/workers/update_external_pull_requests_worker.rb +++ b/app/workers/update_external_pull_requests_worker.rb @@ -21,7 +21,7 @@ class UpdateExternalPullRequestsWorker # rubocop:disable Scalability/IdempotentW .by_source_branch(branch) external_pull_requests.find_each do |pull_request| - ExternalPullRequests::CreatePipelineService.new(project, user) + Ci::ExternalPullRequests::CreatePipelineService.new(project, user) .execute(pull_request) end end diff --git a/changelogs/unreleased/20440-limit-the-api-scope-of-personal-access-tokens.yml b/changelogs/unreleased/20440-limit-the-api-scope-of-personal-access-tokens.yml new file mode 100644 index 00000000000..8756237cc40 --- /dev/null +++ b/changelogs/unreleased/20440-limit-the-api-scope-of-personal-access-tokens.yml @@ -0,0 +1,5 @@ +--- +title: Add read_api scope to personal access tokens for granting read only API access +merge_request: 28944 +author: +type: added diff --git a/changelogs/unreleased/Resolve-Migrate--fa-spinner-app-views-groups.yml b/changelogs/unreleased/Resolve-Migrate--fa-spinner-app-views-groups.yml new file mode 100644 index 00000000000..555e02d880c --- /dev/null +++ b/changelogs/unreleased/Resolve-Migrate--fa-spinner-app-views-groups.yml @@ -0,0 +1,5 @@ +--- +title: Migrate .fa-spinner to .spinner for app/views/groups +merge_request: 25053 +author: nuwe1 +type: other diff --git a/changelogs/unreleased/add-operations-ff-lists-table.yml b/changelogs/unreleased/add-operations-ff-lists-table.yml new file mode 100644 index 00000000000..7696797c9f5 --- /dev/null +++ b/changelogs/unreleased/add-operations-ff-lists-table.yml @@ -0,0 +1,5 @@ +--- +title: Create operations_user_lists table +merge_request: 28822 +author: +type: added diff --git a/changelogs/unreleased/support-dot-in-variables-masking.yml b/changelogs/unreleased/support-dot-in-variables-masking.yml new file mode 100644 index 00000000000..24970e5c671 --- /dev/null +++ b/changelogs/unreleased/support-dot-in-variables-masking.yml @@ -0,0 +1,5 @@ +--- +title: Add support for dot (.) in variables masking +merge_request: 29022 +author: +type: changed diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 258d8a99986..c9dbde23d35 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -70,6 +70,8 @@ en: scope_desc: api: Grants complete read/write access to the API, including all groups and projects, the container registry, and the package registry. + read_api: + Grants read access to the API, including all groups and projects, the container registry, and the package registry. read_user: Grants read-only access to the authenticated user's profile through the /user API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under /users. read_repository: diff --git a/db/migrate/20200331132103_add_project_compliance_framework_settings_table.rb b/db/migrate/20200331132103_add_project_compliance_framework_settings_table.rb new file mode 100644 index 00000000000..6af8c6db939 --- /dev/null +++ b/db/migrate/20200331132103_add_project_compliance_framework_settings_table.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class AddProjectComplianceFrameworkSettingsTable < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + create_table :project_compliance_framework_settings, id: false do |t| + t.references :project, primary_key: true, null: false, index: true, foreign_key: { on_delete: :cascade } + t.integer :framework, null: false, limit: 2 + end + end + end + + def down + with_lock_retries do + drop_table :project_compliance_framework_settings + end + end +end diff --git a/db/migrate/20200401211005_create_operations_user_lists.rb b/db/migrate/20200401211005_create_operations_user_lists.rb new file mode 100644 index 00000000000..c5b928b8350 --- /dev/null +++ b/db/migrate/20200401211005_create_operations_user_lists.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreateOperationsUserLists < ActiveRecord::Migration[6.0] + DOWNTIME = false + + def change + create_table :operations_user_lists do |t| + t.references :project, index: false, foreign_key: { on_delete: :cascade }, null: false + t.timestamps_with_timezone + t.integer :iid, null: false + t.string :name, null: false, limit: 255 + t.text :user_xids, null: false, default: '' + + t.index [:project_id, :iid], unique: true + t.index [:project_id, :name], unique: true + end + end +end diff --git a/db/structure.sql b/db/structure.sql index a284d001ee5..c2a2e4b361c 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -4323,6 +4323,25 @@ CREATE SEQUENCE public.operations_strategies_id_seq ALTER SEQUENCE public.operations_strategies_id_seq OWNED BY public.operations_strategies.id; +CREATE TABLE public.operations_user_lists ( + id bigint NOT NULL, + project_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + iid integer NOT NULL, + name character varying(255) NOT NULL, + user_xids text DEFAULT ''::text NOT NULL +); + +CREATE SEQUENCE public.operations_user_lists_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE public.operations_user_lists_id_seq OWNED BY public.operations_user_lists.id; + CREATE TABLE public.packages_build_infos ( id bigint NOT NULL, package_id integer NOT NULL, @@ -4722,6 +4741,20 @@ CREATE SEQUENCE public.project_ci_cd_settings_id_seq ALTER SEQUENCE public.project_ci_cd_settings_id_seq OWNED BY public.project_ci_cd_settings.id; +CREATE TABLE public.project_compliance_framework_settings ( + project_id bigint NOT NULL, + framework smallint NOT NULL +); + +CREATE SEQUENCE public.project_compliance_framework_settings_project_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE public.project_compliance_framework_settings_project_id_seq OWNED BY public.project_compliance_framework_settings.project_id; + CREATE TABLE public.project_custom_attributes ( id integer NOT NULL, created_at timestamp with time zone NOT NULL, @@ -7275,6 +7308,8 @@ ALTER TABLE ONLY public.operations_scopes ALTER COLUMN id SET DEFAULT nextval('p ALTER TABLE ONLY public.operations_strategies ALTER COLUMN id SET DEFAULT nextval('public.operations_strategies_id_seq'::regclass); +ALTER TABLE ONLY public.operations_user_lists ALTER COLUMN id SET DEFAULT nextval('public.operations_user_lists_id_seq'::regclass); + ALTER TABLE ONLY public.packages_build_infos ALTER COLUMN id SET DEFAULT nextval('public.packages_build_infos_id_seq'::regclass); ALTER TABLE ONLY public.packages_conan_file_metadata ALTER COLUMN id SET DEFAULT nextval('public.packages_conan_file_metadata_id_seq'::regclass); @@ -7315,6 +7350,8 @@ ALTER TABLE ONLY public.project_auto_devops ALTER COLUMN id SET DEFAULT nextval( ALTER TABLE ONLY public.project_ci_cd_settings ALTER COLUMN id SET DEFAULT nextval('public.project_ci_cd_settings_id_seq'::regclass); +ALTER TABLE ONLY public.project_compliance_framework_settings ALTER COLUMN project_id SET DEFAULT nextval('public.project_compliance_framework_settings_project_id_seq'::regclass); + ALTER TABLE ONLY public.project_custom_attributes ALTER COLUMN id SET DEFAULT nextval('public.project_custom_attributes_id_seq'::regclass); ALTER TABLE ONLY public.project_daily_statistics ALTER COLUMN id SET DEFAULT nextval('public.project_daily_statistics_id_seq'::regclass); @@ -8081,6 +8118,9 @@ ALTER TABLE ONLY public.operations_scopes ALTER TABLE ONLY public.operations_strategies ADD CONSTRAINT operations_strategies_pkey PRIMARY KEY (id); +ALTER TABLE ONLY public.operations_user_lists + ADD CONSTRAINT operations_user_lists_pkey PRIMARY KEY (id); + ALTER TABLE ONLY public.packages_build_infos ADD CONSTRAINT packages_build_infos_pkey PRIMARY KEY (id); @@ -8144,6 +8184,9 @@ ALTER TABLE ONLY public.project_auto_devops ALTER TABLE ONLY public.project_ci_cd_settings ADD CONSTRAINT project_ci_cd_settings_pkey PRIMARY KEY (id); +ALTER TABLE ONLY public.project_compliance_framework_settings + ADD CONSTRAINT project_compliance_framework_settings_pkey PRIMARY KEY (project_id); + ALTER TABLE ONLY public.project_custom_attributes ADD CONSTRAINT project_custom_attributes_pkey PRIMARY KEY (id); @@ -9644,6 +9687,10 @@ CREATE UNIQUE INDEX index_operations_scopes_on_strategy_id_and_environment_scope CREATE INDEX index_operations_strategies_on_feature_flag_id ON public.operations_strategies USING btree (feature_flag_id); +CREATE UNIQUE INDEX index_operations_user_lists_on_project_id_and_iid ON public.operations_user_lists USING btree (project_id, iid); + +CREATE UNIQUE INDEX index_operations_user_lists_on_project_id_and_name ON public.operations_user_lists USING btree (project_id, name); + CREATE UNIQUE INDEX index_packages_build_infos_on_package_id ON public.packages_build_infos USING btree (package_id); CREATE INDEX index_packages_build_infos_on_pipeline_id ON public.packages_build_infos USING btree (pipeline_id); @@ -9736,6 +9783,8 @@ CREATE UNIQUE INDEX index_project_auto_devops_on_project_id ON public.project_au CREATE UNIQUE INDEX index_project_ci_cd_settings_on_project_id ON public.project_ci_cd_settings USING btree (project_id); +CREATE INDEX index_project_compliance_framework_settings_on_project_id ON public.project_compliance_framework_settings USING btree (project_id); + CREATE INDEX index_project_custom_attributes_on_key_and_value ON public.project_custom_attributes USING btree (key, value); CREATE UNIQUE INDEX index_project_custom_attributes_on_project_id_and_key ON public.project_custom_attributes USING btree (project_id, key); @@ -10956,6 +11005,9 @@ ALTER TABLE ONLY public.project_deploy_tokens ALTER TABLE ONLY public.packages_conan_file_metadata ADD CONSTRAINT fk_rails_0afabd9328 FOREIGN KEY (package_file_id) REFERENCES public.packages_package_files(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.operations_user_lists + ADD CONSTRAINT fk_rails_0c716e079b FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; + ALTER TABLE ONLY public.geo_node_statuses ADD CONSTRAINT fk_rails_0ecc699c2a FOREIGN KEY (geo_node_id) REFERENCES public.geo_nodes(id) ON DELETE CASCADE; @@ -11391,6 +11443,9 @@ ALTER TABLE ONLY public.prometheus_alerts ALTER TABLE ONLY public.term_agreements ADD CONSTRAINT fk_rails_6ea6520e4a FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.project_compliance_framework_settings + ADD CONSTRAINT fk_rails_6f5294f16c FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; + ALTER TABLE ONLY public.users_security_dashboard_projects ADD CONSTRAINT fk_rails_6f6cf8e66e FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; @@ -13002,8 +13057,10 @@ COPY "schema_migrations" (version) FROM STDIN; 20200330121000 20200330123739 20200330132913 +20200331132103 20200331195952 20200331220930 +20200401211005 20200402123926 20200402135250 20200402185044 diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index b667df24d32..204230c4ca3 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -43,6 +43,7 @@ the following table. | ------------------ | ------------- | ----------- | | `read_user` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5951) | Allows access to the read-only endpoints under `/users`. Essentially, any of the `GET` requests in the [Users API][users] are allowed. | | `api` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5951) | Grants complete read/write access to the API, including all groups and projects, the container registry, and the package registry. | +| `read_api` | [GitLab 12.10](https://https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28944) | Grants read access to the API, including all groups and projects, the container registry, and the package registry. | | `read_registry` | [GitLab 9.3](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/11845) | Allows to read (pull) [container registry] images if a project is private and authorization is required. | | `sudo` | [GitLab 10.2](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14838) | Allows performing API actions as any user in the system (if the authenticated user is an admin). | | `read_repository` | [GitLab 10.7](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17894) | Allows read-only access (pull) to the repository through `git clone`. | diff --git a/lib/api/api.rb b/lib/api/api.rb index cf6e7f18a5f..eb7f47de9e2 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -28,6 +28,7 @@ module API ] allow_access_with_scope :api + allow_access_with_scope :read_api, if: -> (request) { request.get? } prefix :api version 'v3', using: :path do diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index cedf50aaff6..8ad682fc961 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -129,6 +129,7 @@ module API :avatar, :suggestion_commit_message, :repository_storage, + :compliance_framework_setting, # TODO: remove in API v5, replaced by *_access_level :issues_enabled, diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 8a68808d9fd..c489c835d9d 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -6,7 +6,7 @@ module Gitlab IpBlacklisted = Class.new(StandardError) # Scopes used for GitLab API access - API_SCOPES = [:api, :read_user].freeze + API_SCOPES = [:api, :read_user, :read_api].freeze # Scopes used for GitLab Repository access REPOSITORY_SCOPES = [:read_repository, :write_repository].freeze @@ -198,6 +198,7 @@ module Gitlab def abilities_for_scopes(scopes) abilities_by_scope = { api: full_authentication_abilities, + read_api: read_only_authentication_abilities, read_registry: [:read_container_image], read_repository: [:download_code], write_repository: [:download_code, :push_code] diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml index 06d7dd6f897..3cbd0d144e6 100644 --- a/lib/gitlab/import_export/project/import_export.yml +++ b/lib/gitlab/import_export/project/import_export.yml @@ -159,6 +159,7 @@ excluded_attributes: - :max_artifacts_size - :marked_for_deletion_at - :marked_for_deletion_by_user_id + - :compliance_framework_setting namespaces: - :runners_token - :runners_token_encrypted diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 71b228c276a..971a77e55be 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2090,6 +2090,9 @@ msgstr "" msgid "An error occurred. Please try again." msgstr "" +msgid "An error ocurred while loading your content. Please try again." +msgstr "" + msgid "An instance-level serverless domain already exists." msgstr "" @@ -3830,6 +3833,9 @@ msgstr "" msgid "Choose which status most accurately reflects the current state of this issue:" msgstr "" +msgid "Choose your framework" +msgstr "" + msgid "CiStatusLabel|canceled" msgstr "" @@ -5236,6 +5242,24 @@ msgstr "" msgid "Compliance Dashboard" msgstr "" +msgid "Compliance framework (optional)" +msgstr "" + +msgid "ComplianceFramework|GDPR - General Data Protection Regulation" +msgstr "" + +msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act" +msgstr "" + +msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard" +msgstr "" + +msgid "ComplianceFramework|SOC 2 - Service Organization Control 2" +msgstr "" + +msgid "ComplianceFramework|SOX - Sarbanes-Oxley" +msgstr "" + msgid "Confidence: %{confidence}" msgstr "" @@ -17967,6 +17991,9 @@ msgstr "" msgid "Select projects you want to import." msgstr "" +msgid "Select required regulatory standard" +msgstr "" + msgid "Select shards to replicate" msgstr "" diff --git a/spec/frontend/static_site_editor/components/static_site_editor_spec.js b/spec/frontend/static_site_editor/components/static_site_editor_spec.js index 919763ce9fe..cde95d0a21e 100644 --- a/spec/frontend/static_site_editor/components/static_site_editor_spec.js +++ b/spec/frontend/static_site_editor/components/static_site_editor_spec.js @@ -17,11 +17,15 @@ describe('StaticSiteEditor', () => { let store; let loadContentActionMock; - const buildStore = (initialState = {}) => { + const buildStore = ({ initialState, getters } = {}) => { loadContentActionMock = jest.fn(); store = new Vuex.Store({ state: createState(initialState), + getters: { + isContentLoaded: () => false, + ...getters, + }, actions: { loadContent: loadContentActionMock, }, @@ -56,7 +60,7 @@ describe('StaticSiteEditor', () => { const content = 'edit area content'; beforeEach(() => { - buildStore({ content, isContentLoaded: true }); + buildStore({ initialState: { content }, getters: { isContentLoaded: () => true } }); buildWrapper(); }); @@ -70,7 +74,7 @@ describe('StaticSiteEditor', () => { }); it('displays skeleton loader while loading content', () => { - buildStore({ isLoadingContent: true }); + buildStore({ initialState: { isLoadingContent: true } }); buildWrapper(); expect(wrapper.find(GlSkeletonLoader).exists()).toBe(true); diff --git a/spec/frontend/static_site_editor/store/actions_spec.js b/spec/frontend/static_site_editor/store/actions_spec.js new file mode 100644 index 00000000000..98d7d0d2c2d --- /dev/null +++ b/spec/frontend/static_site_editor/store/actions_spec.js @@ -0,0 +1,76 @@ +import testAction from 'helpers/vuex_action_helper'; +import createState from '~/static_site_editor/store/state'; +import * as actions from '~/static_site_editor/store/actions'; +import * as mutationTypes from '~/static_site_editor/store/mutation_types'; +import loadSourceContent from '~/static_site_editor/services/load_source_content'; + +import createFlash from '~/flash'; + +import { + projectId, + sourcePath, + sourceContentTitle as title, + sourceContent as content, +} from '../mock_data'; + +jest.mock('~/flash'); +jest.mock('~/static_site_editor/services/load_source_content', () => jest.fn()); + +describe('Static Site Editor Store actions', () => { + let state; + + beforeEach(() => { + state = createState({ + projectId, + sourcePath, + }); + }); + + describe('loadContent', () => { + describe('on success', () => { + const payload = { title, content }; + + beforeEach(() => { + loadSourceContent.mockResolvedValueOnce(payload); + }); + + it('commits receiveContentSuccess', () => { + testAction( + actions.loadContent, + null, + state, + [ + { type: mutationTypes.LOAD_CONTENT }, + { type: mutationTypes.RECEIVE_CONTENT_SUCCESS, payload }, + ], + [], + ); + + expect(loadSourceContent).toHaveBeenCalledWith({ projectId, sourcePath }); + }); + }); + + describe('on error', () => { + const expectedMutations = [ + { type: mutationTypes.LOAD_CONTENT }, + { type: mutationTypes.RECEIVE_CONTENT_ERROR }, + ]; + + beforeEach(() => { + loadSourceContent.mockRejectedValueOnce(); + }); + + it('commits receiveContentError', () => { + testAction(actions.loadContent, null, state, expectedMutations); + }); + + it('displays flash communicating error', () => { + return testAction(actions.loadContent, null, state, expectedMutations).then(() => { + expect(createFlash).toHaveBeenCalledWith( + 'An error ocurred while loading your content. Please try again.', + ); + }); + }); + }); + }); +}); diff --git a/spec/frontend/static_site_editor/store/getters_spec.js b/spec/frontend/static_site_editor/store/getters_spec.js new file mode 100644 index 00000000000..8800216f3b0 --- /dev/null +++ b/spec/frontend/static_site_editor/store/getters_spec.js @@ -0,0 +1,15 @@ +import createState from '~/static_site_editor/store/state'; +import { isContentLoaded } from '~/static_site_editor/store/getters'; +import { sourceContent as content } from '../mock_data'; + +describe('Static Site Editor Store getters', () => { + describe('isContentLoaded', () => { + it('returns true when content is not empty', () => { + expect(isContentLoaded(createState({ content }))).toBe(true); + }); + + it('returns false when content is empty', () => { + expect(isContentLoaded(createState({ content: '' }))).toBe(false); + }); + }); +}); diff --git a/spec/frontend/static_site_editor/store/mutations_spec.js b/spec/frontend/static_site_editor/store/mutations_spec.js new file mode 100644 index 00000000000..c7055fbb2f0 --- /dev/null +++ b/spec/frontend/static_site_editor/store/mutations_spec.js @@ -0,0 +1,52 @@ +import createState from '~/static_site_editor/store/state'; +import mutations from '~/static_site_editor/store/mutations'; +import * as types from '~/static_site_editor/store/mutation_types'; +import { sourceContentTitle as title, sourceContent as content } from '../mock_data'; + +describe('Static Site Editor Store mutations', () => { + let state; + + beforeEach(() => { + state = createState(); + }); + + describe('loadContent', () => { + beforeEach(() => { + mutations[types.LOAD_CONTENT](state); + }); + + it('sets isLoadingContent to true', () => { + expect(state.isLoadingContent).toBe(true); + }); + }); + + describe('receiveContentSuccess', () => { + const payload = { title, content }; + + beforeEach(() => { + mutations[types.RECEIVE_CONTENT_SUCCESS](state, payload); + }); + + it('sets current state to LOADING', () => { + expect(state.isLoadingContent).toBe(false); + }); + + it('sets title', () => { + expect(state.title).toBe(payload.title); + }); + + it('sets content', () => { + expect(state.content).toBe(payload.content); + }); + }); + + describe('receiveContentError', () => { + beforeEach(() => { + mutations[types.RECEIVE_CONTENT_ERROR](state); + }); + + it('sets current state to LOADING_ERROR', () => { + expect(state.isLoadingContent).toBe(false); + }); + }); +}); diff --git a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js index b53e30b6896..a1377564073 100644 --- a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js @@ -224,7 +224,7 @@ describe('AjaxFormVariableList', () => { describe('maskableRegex', () => { it('takes in the regex provided by the data attribute', () => { - expect(container.dataset.maskableRegex).toBe('^[a-zA-Z0-9_+=/@:-]{8,}$'); + expect(container.dataset.maskableRegex).toBe('^[a-zA-Z0-9_+=/@:.-]{8,}$'); expect(ajaxVariableList.maskableRegex).toBe(container.dataset.maskableRegex); }); }); diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js index 180ba979325..c0c3a83a44b 100644 --- a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js @@ -162,7 +162,7 @@ describe('VariableList', () => { }); it('has a regex provided via a data attribute', () => { - expect($wrapper.attr('data-maskable-regex')).toBe('^[a-zA-Z0-9_+=/@:-]{8,}$'); + expect($wrapper.attr('data-maskable-regex')).toBe('^[a-zA-Z0-9_+=/@:.-]{8,}$'); }); it('allows values that are 8 characters long', done => { diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index dcc4d277f5c..ce60a19a7b3 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -8,7 +8,7 @@ describe Gitlab::Auth, :use_clean_rails_memory_store_caching do describe 'constants' do it 'API_SCOPES contains all scopes for API access' do - expect(subject::API_SCOPES).to eq %i[api read_user] + expect(subject::API_SCOPES).to eq %i[api read_user read_api] end it 'ADMIN_SCOPES contains all scopes for ADMIN access' do @@ -30,7 +30,7 @@ describe Gitlab::Auth, :use_clean_rails_memory_store_caching do it 'optional_scopes contains all non-default scopes' do stub_container_registry_config(enabled: true) - expect(subject.optional_scopes).to eq %i[read_user read_repository write_repository read_registry sudo openid profile email] + expect(subject.optional_scopes).to eq %i[read_user read_api read_repository write_repository read_registry sudo openid profile email] end end @@ -38,21 +38,21 @@ describe Gitlab::Auth, :use_clean_rails_memory_store_caching do it 'contains all non-default scopes' do stub_container_registry_config(enabled: true) - expect(subject.all_available_scopes).to eq %i[api read_user read_repository write_repository read_registry sudo] + expect(subject.all_available_scopes).to eq %i[api read_user read_api read_repository write_repository read_registry sudo] end it 'contains for non-admin user all non-default scopes without ADMIN access' do stub_container_registry_config(enabled: true) user = create(:user, admin: false) - expect(subject.available_scopes_for(user)).to eq %i[api read_user read_repository write_repository read_registry] + expect(subject.available_scopes_for(user)).to eq %i[api read_user read_api read_repository write_repository read_registry] end it 'contains for admin user all non-default scopes with ADMIN access' do stub_container_registry_config(enabled: true) user = create(:user, admin: true) - expect(subject.available_scopes_for(user)).to eq %i[api read_user read_repository write_repository read_registry sudo] + expect(subject.available_scopes_for(user)).to eq %i[api read_user read_api read_repository write_repository read_registry sudo] end context 'registry_scopes' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index aaaf6458ee7..515d72add92 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -477,6 +477,7 @@ project: - export_jobs - daily_report_results - jira_imports +- compliance_framework_setting award_emoji: - awardable - user diff --git a/spec/models/ci/group_spec.rb b/spec/models/ci/group_spec.rb index 5516a1a9c61..868382e3756 100644 --- a/spec/models/ci/group_spec.rb +++ b/spec/models/ci/group_spec.rb @@ -3,12 +3,14 @@ require 'spec_helper' describe Ci::Group do + let_it_be(:project) { create(:project) } + + let!(:jobs) { build_list(:ci_build, 1, :success, project: project) } + subject do - described_class.new('test', name: 'rspec', jobs: jobs) + described_class.new(project, 'test', name: 'rspec', jobs: jobs) end - let!(:jobs) { build_list(:ci_build, 1, :success) } - it { is_expected.to include_module(StaticModel) } it { is_expected.to respond_to(:stage) } diff --git a/spec/models/concerns/ci/maskable_spec.rb b/spec/models/concerns/ci/maskable_spec.rb index 22ffb294819..01861b39165 100644 --- a/spec/models/concerns/ci/maskable_spec.rb +++ b/spec/models/concerns/ci/maskable_spec.rb @@ -61,8 +61,12 @@ describe Ci::Maskable do expect(subject.match?(string)).to eq(false) end + it 'does not match strings using unsupported characters' do + expect(subject.match?('HelloWorld%#^')).to eq(false) + end + it 'matches valid strings' do - expect(subject.match?('helloworld')).to eq(true) + expect(subject.match?('Hello+World_123/@:-.')).to eq(true) end end diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb index baebbbce631..201c0d1796c 100644 --- a/spec/requests/api/api_spec.rb +++ b/spec/requests/api/api_spec.rb @@ -3,15 +3,60 @@ require 'spec_helper' describe API::API do - let(:user) { create(:user, last_activity_on: Date.yesterday) } + include GroupAPIHelpers describe 'Record user last activity in after hook' do # It does not matter which endpoint is used because last_activity_on should # be updated on every request. `/groups` is used as an example # to represent any API endpoint + let(:user) { create(:user, last_activity_on: Date.yesterday) } - it 'updates the users last_activity_on date' do + it 'updates the users last_activity_on to the current date' do expect { get api('/groups', user) }.to change { user.reload.last_activity_on }.to(Date.today) end end + + describe 'User with only read_api scope personal access token' do + # It does not matter which endpoint is used because this should behave + # in the same way for every request. `/groups` is used as an example + # to represent any API endpoint + + context 'when personal access token has only read_api scope' do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:token) { create(:personal_access_token, user: user, scopes: [:read_api]) } + + before_all do + group.add_owner(user) + end + + it 'does authorize user for get request' do + get api('/groups', personal_access_token: token) + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'does not authorize user for post request' do + params = attributes_for_group_api + + post api("/groups", personal_access_token: token), params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'does not authorize user for put request' do + group_param = { name: 'Test' } + + put api("/groups/#{group.id}", personal_access_token: token), params: group_param + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'does not authorize user for delete request' do + delete api("/groups/#{group.id}", personal_access_token: token) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end end diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb index d7c08484dc4..bd270679acd 100644 --- a/spec/requests/openid_connect_spec.rb +++ b/spec/requests/openid_connect_spec.rb @@ -180,7 +180,7 @@ describe 'OpenID Connect requests' do expect(response).to have_gitlab_http_status(:ok) expect(json_response['issuer']).to eq('http://localhost') expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys') - expect(json_response['scopes_supported']).to eq(%w[api read_user read_repository write_repository sudo openid profile email]) + expect(json_response['scopes_supported']).to eq(%w[api read_user read_api read_repository write_repository sudo openid profile email]) end end diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb index 06f2c0ca68b..7950f89bcc7 100644 --- a/spec/serializers/analytics_summary_serializer_spec.rb +++ b/spec/serializers/analytics_summary_serializer_spec.rb @@ -28,4 +28,18 @@ describe AnalyticsSummarySerializer do it 'contains important elements of AnalyticsStage' do expect(subject).to include(:title, :value) end + + it 'does not include unit' do + expect(subject).not_to include(:unit) + end + + context 'when representing with unit' do + let(:resource) { { title: 'frequency', value: 1.12, unit: 'per day' } } + + subject { described_class.new.represent(resource, with_unit: true) } + + it 'contains unit' do + expect(subject).to include(:unit) + end + end end diff --git a/spec/services/external_pull_requests/create_pipeline_service_spec.rb b/spec/services/ci/external_pull_requests/create_pipeline_service_spec.rb index d1893960960..5048f2b71b3 100644 --- a/spec/services/external_pull_requests/create_pipeline_service_spec.rb +++ b/spec/services/ci/external_pull_requests/create_pipeline_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe ExternalPullRequests::CreatePipelineService do +describe Ci::ExternalPullRequests::CreatePipelineService do describe '#execute' do let_it_be(:project) { create(:project, :auto_devops, :repository) } let_it_be(:user) { create(:user) } diff --git a/spec/workers/update_external_pull_requests_worker_spec.rb b/spec/workers/update_external_pull_requests_worker_spec.rb index 8930a36ceb8..afac0357b2d 100644 --- a/spec/workers/update_external_pull_requests_worker_spec.rb +++ b/spec/workers/update_external_pull_requests_worker_spec.rb @@ -28,10 +28,10 @@ describe UpdateExternalPullRequestsWorker do context 'when ref is a branch' do let(:ref) { 'refs/heads/feature-1' } - let(:create_pipeline_service) { instance_double(ExternalPullRequests::CreatePipelineService) } + let(:create_pipeline_service) { instance_double(Ci::ExternalPullRequests::CreatePipelineService) } it 'runs CreatePipelineService for each pull request matching the source branch and repository' do - expect(ExternalPullRequests::CreatePipelineService) + expect(Ci::ExternalPullRequests::CreatePipelineService) .to receive(:new) .and_return(create_pipeline_service) .twice @@ -45,7 +45,7 @@ describe UpdateExternalPullRequestsWorker do let(:ref) { 'refs/tags/v1.2.3' } it 'does nothing' do - expect(ExternalPullRequests::CreatePipelineService).not_to receive(:new) + expect(Ci::ExternalPullRequests::CreatePipelineService).not_to receive(:new) subject end |