diff options
| author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-25 03:10:50 +0000 |
|---|---|---|
| committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-25 03:10:50 +0000 |
| commit | e66e16c73cda415ccd03ac0a1818a58ddc4429d7 (patch) | |
| tree | 72fa2f7ece17e8c494b1c5aef6909f3f05a7a37e | |
| parent | cffcf0772c5354d0d55fd4e32f724108a9582f15 (diff) | |
| download | gitlab-ce-e66e16c73cda415ccd03ac0a1818a58ddc4429d7.tar.gz | |
Add latest changes from gitlab-org/gitlab@master
13 files changed, 221 insertions, 8 deletions
diff --git a/app/assets/javascripts/access_tokens/components/projects_field.vue b/app/assets/javascripts/access_tokens/components/projects_field.vue new file mode 100644 index 00000000000..e58f74b6ad4 --- /dev/null +++ b/app/assets/javascripts/access_tokens/components/projects_field.vue @@ -0,0 +1,38 @@ +<script> +import { GlFormGroup, GlFormRadio, GlFormText } from '@gitlab/ui'; + +export default { + name: 'ProjectsField', + ALL_PROJECTS: 'ALL_PROJECTS', + SELECTED_PROJECTS: 'SELECTED_PROJECTS', + components: { GlFormGroup, GlFormRadio, GlFormText }, + props: { + inputAttrs: { + type: Object, + required: true, + }, + }, + data() { + return { + selectedRadio: this.$options.ALL_PROJECTS, + }; + }, +}; +</script> + +<template> + <div> + <gl-form-group :label="__('Projects')" label-class="gl-pb-0!"> + <gl-form-text class="gl-pb-3">{{ + __('Set access permissions for this token.') + }}</gl-form-text> + <gl-form-radio v-model="selectedRadio" :value="$options.ALL_PROJECTS">{{ + __('All projects') + }}</gl-form-radio> + <gl-form-radio v-model="selectedRadio" :value="$options.SELECTED_PROJECTS">{{ + __('Selected projects') + }}</gl-form-radio> + <input :id="inputAttrs.id" type="hidden" :name="inputAttrs.name" /> + </gl-form-group> + </div> +</template> diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js index b4353af30d5..e29ec5adb42 100644 --- a/app/assets/javascripts/access_tokens/index.js +++ b/app/assets/javascripts/access_tokens/index.js @@ -1,4 +1,5 @@ import Vue from 'vue'; + import ExpiresAtField from './components/expires_at_field.vue'; const getInputAttrs = (el) => { @@ -11,7 +12,7 @@ const getInputAttrs = (el) => { }; }; -const initExpiresAtField = () => { +export const initExpiresAtField = () => { const el = document.querySelector('.js-access-tokens-expires-at'); if (!el) { @@ -32,4 +33,29 @@ const initExpiresAtField = () => { }); }; -export default initExpiresAtField; +export const initProjectsField = () => { + const el = document.querySelector('.js-access-tokens-projects'); + + if (!el) { + return null; + } + + const inputAttrs = getInputAttrs(el); + + if (window.gon.features.personalAccessTokensScopedToProjects) { + const ProjectsField = () => import('./components/projects_field.vue'); + + return new Vue({ + el, + render(h) { + return h(ProjectsField, { + props: { + inputAttrs, + }, + }); + }, + }); + } + + return null; +}; diff --git a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js index ae2209b0292..dc1bb88bf4b 100644 --- a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js +++ b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js @@ -1,3 +1,3 @@ -import initExpiresAtField from '~/access_tokens'; +import { initExpiresAtField } from '~/access_tokens'; -document.addEventListener('DOMContentLoaded', initExpiresAtField); +initExpiresAtField(); diff --git a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js index ae2209b0292..fdbfc35456f 100644 --- a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js +++ b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js @@ -1,3 +1,4 @@ -import initExpiresAtField from '~/access_tokens'; +import { initExpiresAtField, initProjectsField } from '~/access_tokens'; -document.addEventListener('DOMContentLoaded', initExpiresAtField); +initExpiresAtField(); +initProjectsField(); diff --git a/app/assets/javascripts/pages/projects/settings/access_tokens/index.js b/app/assets/javascripts/pages/projects/settings/access_tokens/index.js index 22dddb72f98..dc1bb88bf4b 100644 --- a/app/assets/javascripts/pages/projects/settings/access_tokens/index.js +++ b/app/assets/javascripts/pages/projects/settings/access_tokens/index.js @@ -1,3 +1,3 @@ -import initExpiresAtField from '~/access_tokens'; +import { initExpiresAtField } from '~/access_tokens'; initExpiresAtField(); diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index a45205c5da7..251967a7dff 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -3,6 +3,10 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController feature_category :authentication_and_authorization + before_action do + push_frontend_feature_flag(:personal_access_tokens_scoped_to_projects, current_user) + end + def index set_index_vars @personal_access_token = finder.build diff --git a/app/views/shared/access_tokens/_form.html.haml b/app/views/shared/access_tokens/_form.html.haml index 089643f4748..4063674b2fc 100644 --- a/app/views/shared/access_tokens/_form.html.haml +++ b/app/views/shared/access_tokens/_form.html.haml @@ -29,5 +29,9 @@ = f.label :scopes, _('Scopes'), class: 'label-bold' = render 'shared/tokens/scopes_form', prefix: prefix, token: token, scopes: scopes + - if prefix == :personal_access_token && Feature.enabled?(:personal_access_tokens_scoped_to_projects, current_user) + .js-access-tokens-projects + %input{ type: 'hidden', name: 'temporary-name', id: 'temporary-id' } + .gl-mt-3 = f.submit _('Create %{type}') % { type: type }, class: 'gl-button btn btn-success', data: { qa_selector: 'create_token_button' } diff --git a/config/feature_flags/development/personal_access_tokens_scoped_to_projects.yml b/config/feature_flags/development/personal_access_tokens_scoped_to_projects.yml new file mode 100644 index 00000000000..9188b0dbab4 --- /dev/null +++ b/config/feature_flags/development/personal_access_tokens_scoped_to_projects.yml @@ -0,0 +1,8 @@ +--- +name: personal_access_tokens_scoped_to_projects +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54617 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/322187 +milestone: '13.10' +type: development +group: group::access +default_enabled: false diff --git a/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml index d2a6fa06dd8..c255fb4707a 100644 --- a/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml @@ -1,7 +1,7 @@ # Read more about this feature here: https://docs.gitlab.com/ee/user/application_security/secret_detection # # Configure the scanning tool through the environment variables. -# List of the variables: https://gitlab.com/gitlab-org/security-products/secret_detection#available-variables +# List of the variables: https://docs.gitlab.com/ee/user/application_security/secret_detection/#available-variables # How to set: https://docs.gitlab.com/ee/ci/yaml/#variables variables: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e0a624b2113..e73a8f3ed0c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -26812,6 +26812,9 @@ msgstr "" msgid "Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users." msgstr "" +msgid "Selected projects" +msgstr "" + msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By %{link_open}@johnsmith%{link_close}\"). It will also associate and/or assign these issues and comments with the selected user." msgstr "" @@ -27031,6 +27034,9 @@ msgstr "" msgid "Set a template repository for projects in this group" msgstr "" +msgid "Set access permissions for this token." +msgstr "" + msgid "Set an instance-wide domain that will be available to all clusters when installing Knative." msgstr "" diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index 88bfc71cfbe..9e56ef087ae 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -138,4 +138,10 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do end end end + + it 'pushes `personal_access_tokens_scoped_to_projects` feature flag to the frontend' do + visit profile_personal_access_tokens_path + + expect(page).to have_pushed_frontend_feature_flags(personalAccessTokensScopedToProjects: true) + end end diff --git a/spec/frontend/access_tokens/components/projects_field_spec.js b/spec/frontend/access_tokens/components/projects_field_spec.js new file mode 100644 index 00000000000..7e9f06b9022 --- /dev/null +++ b/spec/frontend/access_tokens/components/projects_field_spec.js @@ -0,0 +1,58 @@ +import { within } from '@testing-library/dom'; +import { mount } from '@vue/test-utils'; +import ProjectsField from '~/access_tokens/components/projects_field.vue'; + +describe('ProjectsField', () => { + let wrapper; + + const createComponent = () => { + wrapper = mount(ProjectsField, { + propsData: { + inputAttrs: { + id: 'projects', + name: 'projects', + }, + }, + }); + }; + + const queryByLabelText = (text) => within(wrapper.element).queryByLabelText(text); + const queryByText = (text) => within(wrapper.element).queryByText(text); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('renders label and sub-label', () => { + expect(queryByText('Projects')).not.toBe(null); + expect(queryByText('Set access permissions for this token.')).not.toBe(null); + }); + + it('renders "All projects" radio selected by default', () => { + const allProjectsRadio = queryByLabelText('All projects'); + + expect(allProjectsRadio).not.toBe(null); + expect(allProjectsRadio.checked).toBe(true); + }); + + it('renders "Selected projects" radio unchecked by default', () => { + const selectedProjectsRadio = queryByLabelText('Selected projects'); + + expect(selectedProjectsRadio).not.toBe(null); + expect(selectedProjectsRadio.checked).toBe(false); + }); + + it('renders hidden input with correct `name` and `id` attributes', () => { + expect(wrapper.find('input[type="hidden"]').attributes()).toEqual( + expect.objectContaining({ + id: 'projects', + name: 'projects', + }), + ); + }); +}); diff --git a/spec/frontend/access_tokens/index_spec.js b/spec/frontend/access_tokens/index_spec.js new file mode 100644 index 00000000000..2225e23e09c --- /dev/null +++ b/spec/frontend/access_tokens/index_spec.js @@ -0,0 +1,62 @@ +import { createWrapper } from '@vue/test-utils'; + +import waitForPromises from 'helpers/wait_for_promises'; + +import { initExpiresAtField, initProjectsField } from '~/access_tokens'; +import ExpiresAtField from '~/access_tokens/components/expires_at_field.vue'; +import ProjectsField from '~/access_tokens/components/projects_field.vue'; + +describe('access tokens', () => { + beforeEach(() => { + window.gon = { features: { personalAccessTokensScopedToProjects: true } }; + }); + + afterEach(() => { + document.body.innerHTML = ''; + window.gon = {}; + }); + + describe.each` + initFunction | mountSelector | expectedComponent + ${initExpiresAtField} | ${'js-access-tokens-expires-at'} | ${ExpiresAtField} + ${initProjectsField} | ${'js-access-tokens-projects'} | ${ProjectsField} + `('$initFunction', ({ initFunction, mountSelector, expectedComponent }) => { + describe('when mount element exists', () => { + beforeEach(() => { + const mountEl = document.createElement('div'); + mountEl.classList.add(mountSelector); + + const input = document.createElement('input'); + input.setAttribute('name', 'foo-bar'); + input.setAttribute('id', 'foo-bar'); + input.setAttribute('placeholder', 'Foo bar'); + + mountEl.appendChild(input); + + document.body.appendChild(mountEl); + }); + + it(`mounts component and sets \`inputAttrs\` prop`, async () => { + const wrapper = createWrapper(initFunction()); + + // Wait for dynamic imports to resolve + await waitForPromises(); + + const component = wrapper.findComponent(expectedComponent); + + expect(component.exists()).toBe(true); + expect(component.props('inputAttrs')).toEqual({ + name: 'foo-bar', + id: 'foo-bar', + placeholder: 'Foo bar', + }); + }); + }); + + describe('when mount element does not exist', () => { + it('returns `null`', () => { + expect(initFunction()).toBe(null); + }); + }); + }); +}); |
