summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-02-25 03:10:50 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-02-25 03:10:50 +0000
commite66e16c73cda415ccd03ac0a1818a58ddc4429d7 (patch)
tree72fa2f7ece17e8c494b1c5aef6909f3f05a7a37e
parentcffcf0772c5354d0d55fd4e32f724108a9582f15 (diff)
downloadgitlab-ce-e66e16c73cda415ccd03ac0a1818a58ddc4429d7.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/access_tokens/components/projects_field.vue38
-rw-r--r--app/assets/javascripts/access_tokens/index.js30
-rw-r--r--app/assets/javascripts/pages/admin/impersonation_tokens/index.js4
-rw-r--r--app/assets/javascripts/pages/profiles/personal_access_tokens/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/settings/access_tokens/index.js2
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb4
-rw-r--r--app/views/shared/access_tokens/_form.html.haml4
-rw-r--r--config/feature_flags/development/personal_access_tokens_scoped_to_projects.yml8
-rw-r--r--lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml2
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb6
-rw-r--r--spec/frontend/access_tokens/components/projects_field_spec.js58
-rw-r--r--spec/frontend/access_tokens/index_spec.js62
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);
+ });
+ });
+ });
+});