diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js
index 5e3f142543f6c6939d390de73f06ac7925124ef6..26de2ec1973a3e6c7324cd4c632a0fb092f08598 100644
--- a/app/assets/javascripts/access_tokens/index.js
+++ b/app/assets/javascripts/access_tokens/index.js
@@ -7,6 +7,7 @@ import { parseRailsFormFields } from '~/lib/utils/forms';
import { __, sprintf } from '~/locale';
import Translate from '~/vue_shared/translate';
import AccessTokens from '~/vue_shared/access_tokens/components/access_tokens.vue';
+import GenerateFineGrainedTokenApp from '~/vue_shared/access_tokens/components/fine_grained_tokens/generate_token_app.vue';
import AccessTokenTableApp from './components/access_token_table_app.vue';
import InactiveAccessTokenTableApp from './components/inactive_access_token_table_app.vue';
import ExpiresAtField from './components/expires_at_field.vue';
@@ -204,3 +205,16 @@ export const initTokensApp = () => {
},
});
};
+
+export const initGenerateFineGrainedTokenApp = () => {
+ const el = document.getElementById('js-generate-fine-grained-token-app');
+
+ if (!el) return null;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(GenerateFineGrainedTokenApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/pages/user_settings/personal_access_tokens/new/index.js b/app/assets/javascripts/pages/user_settings/personal_access_tokens/new/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..1a20ad878a1961af24b4979550c6a2369b72bf24
--- /dev/null
+++ b/app/assets/javascripts/pages/user_settings/personal_access_tokens/new/index.js
@@ -0,0 +1,3 @@
+import { initGenerateFineGrainedTokenApp } from '~/access_tokens';
+
+initGenerateFineGrainedTokenApp();
diff --git a/app/assets/javascripts/vue_shared/access_tokens/components/fine_grained_tokens/generate_token_app.vue b/app/assets/javascripts/vue_shared/access_tokens/components/fine_grained_tokens/generate_token_app.vue
new file mode 100644
index 0000000000000000000000000000000000000000..fff395309a578b2f3314c674874fb738e9d393d2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/access_tokens/components/fine_grained_tokens/generate_token_app.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+ {{
+ s__(
+ 'AccessTokens|Fine-grained personal access tokens give you granular control over the specific resources and actions available to the token.',
+ )
+ }}
+
+
+
diff --git a/app/controllers/user_settings/personal_access_tokens_controller.rb b/app/controllers/user_settings/personal_access_tokens_controller.rb
index 6bfd86361a040fcbada53e82f6bc5bf60efc48ed..7fffaa8fa256117a9d03f93cf4e7ea3c5b6aabe0 100644
--- a/app/controllers/user_settings/personal_access_tokens_controller.rb
+++ b/app/controllers/user_settings/personal_access_tokens_controller.rb
@@ -43,6 +43,10 @@ def index
end
end
+ def new
+ render_404 unless Feature.enabled?(:fine_grained_personal_access_tokens, :instance)
+ end
+
def create
result = ::PersonalAccessTokens::CreateService.new(
current_user: current_user,
diff --git a/app/views/user_settings/personal_access_tokens/new.html.haml b/app/views/user_settings/personal_access_tokens/new.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..d8d35af8d7063e08d564335df04c5de829caaec8
--- /dev/null
+++ b/app/views/user_settings/personal_access_tokens/new.html.haml
@@ -0,0 +1,4 @@
+- page_title s_('AccessTokens|Generate fine-grained token')
+- @hide_search_settings = true
+
+#js-generate-fine-grained-token-app
diff --git a/config/feature_flags/wip/fine_grained_personal_access_tokens.yml b/config/feature_flags/wip/fine_grained_personal_access_tokens.yml
new file mode 100644
index 0000000000000000000000000000000000000000..14321a087fdbed49754fbf0059e230225e03fd09
--- /dev/null
+++ b/config/feature_flags/wip/fine_grained_personal_access_tokens.yml
@@ -0,0 +1,10 @@
+---
+name: fine_grained_personal_access_tokens
+description: Support for fine-grained personal access tokens
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/570705
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209386
+rollout_issue_url:
+milestone: '18.6'
+group: group::authorization
+type: wip
+default_enabled: false
diff --git a/config/routes/user_settings.rb b/config/routes/user_settings.rb
index 39b05bf8a27fccbeb4b780988792cf1cecfc056d..9cf6160d38c00deaf4320da97d6753b824cf8020 100644
--- a/config/routes/user_settings.rb
+++ b/config/routes/user_settings.rb
@@ -13,7 +13,7 @@
put :reset
end
end
- resources :personal_access_tokens, only: [:index, :create] do
+ resources :personal_access_tokens, only: [:index, :create, :new] do
put :toggle_dpop, on: :collection
end
resources :gpg_keys, only: [:index, :create, :destroy] do
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 788302886352f89214c26b69cb90dfe7a88ba9d2..f7d03eef9b7239c05b671183a7dd69e5bc54bb85 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3691,6 +3691,9 @@ msgstr ""
msgid "AccessTokens|Filter list"
msgstr ""
+msgid "AccessTokens|Fine-grained personal access tokens give you granular control over the specific resources and actions available to the token."
+msgstr ""
+
msgid "AccessTokens|For example, the application using the token or the purpose of the token."
msgstr ""
@@ -3703,6 +3706,9 @@ msgstr ""
msgid "AccessTokens|Forces new personal, group, project and impersonation tokens to require an expiration date. Does not affect existing token expiration dates or personal access tokens for service accounts. This setting overrides the group-level %{link_start}service account token expiration%{link_end} setting."
msgstr ""
+msgid "AccessTokens|Generate fine-grained token"
+msgstr ""
+
msgid "AccessTokens|How do I use DPoP?"
msgstr ""
diff --git a/spec/controllers/user_settings/personal_access_tokens_controller_spec.rb b/spec/controllers/user_settings/personal_access_tokens_controller_spec.rb
index ad37df13c7e6696fc43927cbd053e9d3dd10c24c..3cb5cd4ed9df416a76cc82f130b8e43cf94dac4b 100644
--- a/spec/controllers/user_settings/personal_access_tokens_controller_spec.rb
+++ b/spec/controllers/user_settings/personal_access_tokens_controller_spec.rb
@@ -162,4 +162,30 @@ def created_token
expect(response.body).to include("SUMMARY:#{format(_("Token '%{name}' expires today"), name: token.name)}")
end
end
+
+ describe '#new' do
+ context 'when fine_grained_personal_access_tokens feature flag is disabled' do
+ before do
+ stub_feature_flags(fine_grained_personal_access_tokens: false)
+ end
+
+ it 'returns 404' do
+ get :new
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when feature flag is enabled' do
+ before do
+ stub_feature_flags(fine_grained_personal_access_tokens: true)
+ end
+
+ it 'renders the new template' do
+ get :new
+
+ expect(response).to render_template(:new)
+ end
+ end
+ end
end
diff --git a/spec/frontend/vue_shared/access_tokens/components/fine_grained_tokens/generate_token_app_spec.js b/spec/frontend/vue_shared/access_tokens/components/fine_grained_tokens/generate_token_app_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..a6e41ca72d0861fe968ef33fbeb27c9a60ab6531
--- /dev/null
+++ b/spec/frontend/vue_shared/access_tokens/components/fine_grained_tokens/generate_token_app_spec.js
@@ -0,0 +1,25 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import GenerateTokenApp from '~/vue_shared/access_tokens/components/fine_grained_tokens/generate_token_app.vue';
+import PageHeading from '~/vue_shared/components/page_heading.vue';
+
+describe('Generate Token app', () => {
+ let wrapper;
+
+ const createWrapper = () => {
+ wrapper = shallowMountExtended(GenerateTokenApp);
+ };
+
+ const findPageHeading = () => wrapper.findComponent(PageHeading);
+
+ beforeEach(() => createWrapper());
+
+ it('shows page header', () => {
+ expect(findPageHeading().props('heading')).toBe('Generate fine-grained token');
+ });
+
+ it('shows page header description', () => {
+ expect(findPageHeading().text()).toBe(
+ 'Fine-grained personal access tokens give you granular control over the specific resources and actions available to the token.',
+ );
+ });
+});