diff --git a/app/assets/javascripts/token_access/graphql/mutations/autopopulate_allowlist.mutation.graphql b/app/assets/javascripts/token_access/graphql/mutations/autopopulate_allowlist.mutation.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..b808810a8cb13054938590ecf027e3ac911090c8
--- /dev/null
+++ b/app/assets/javascripts/token_access/graphql/mutations/autopopulate_allowlist.mutation.graphql
@@ -0,0 +1,6 @@
+mutation CiJobTokenScopeAutopopulateAllowlist($projectPath: ID!) {
+ ciJobTokenScopeAutopopulateAllowlist(input: { projectPath: $projectPath }) {
+ status
+ errors
+ }
+}
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c85a2c611175a7cba9a9e811722d7257f2cd947b..f0c182663cb24e968942d7dc91136d64b4b93a9d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3369,6 +3369,9 @@ msgstr ""
msgid "Add email participants that don't have a GitLab account."
msgstr ""
+msgid "Add entries"
+msgstr ""
+
msgid "Add environment"
msgstr ""
@@ -11060,6 +11063,9 @@ msgstr ""
msgid "CICD|Add a %{kubernetes_cluster_link_start}Kubernetes cluster integration%{link_end} with a domain, or create an AUTO_DEVOPS_PLATFORM_TARGET CI variable."
msgstr ""
+msgid "CICD|Add all authentication log entries to the allowlist"
+msgstr ""
+
msgid "CICD|Add an existing project to the scope"
msgstr ""
@@ -11075,6 +11081,9 @@ msgstr ""
msgid "CICD|Allow Git push requests to the repository"
msgstr ""
+msgid "CICD|An error occurred while adding the authentication log entries. Please try again."
+msgstr ""
+
msgid "CICD|Are you sure you want to remove %{namespace} from the job token allowlist?"
msgstr ""
@@ -11087,6 +11096,9 @@ msgstr ""
msgid "CICD|Authentication log"
msgstr ""
+msgid "CICD|Authentication log entries were successfully added to the allowlist."
+msgstr ""
+
msgid "CICD|Authorized groups and projects"
msgstr ""
@@ -11147,6 +11159,9 @@ msgstr ""
msgid "CICD|Group or project"
msgstr ""
+msgid "CICD|Groups and projects on the allowlist are authorized to use a CI/CD job token to authenticate requests to this project. Entries added from the authentication log can be removed later if needed."
+msgstr ""
+
msgid "CICD|Job token permissions"
msgstr ""
@@ -11198,6 +11213,9 @@ msgstr ""
msgid "CICD|The Auto DevOps pipeline runs if no alternative CI configuration file is found."
msgstr ""
+msgid "CICD|The process to add entries could take a moment to complete with large logs or allowlists."
+msgstr ""
+
msgid "CICD|There are several CI/CD limits in place."
msgstr ""
@@ -11216,6 +11234,9 @@ msgstr ""
msgid "CICD|When enabled, all projects must use their allowlist to control CI/CD job token access between projects. The option to allow access from all groups and projects is hidden. %{link_start}Learn More.%{link_end}"
msgstr ""
+msgid "CICD|You're about to add all entries from the authentication log to the allowlist for %{projectName}. Duplicate entries will be ignored."
+msgstr ""
+
msgid "CICD|group enabled"
msgstr ""
diff --git a/spec/frontend/token_access/autopopulate_allowlist_modal_spec.js b/spec/frontend/token_access/autopopulate_allowlist_modal_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..ed3b405335190d6f82de9c5a8a5ad7761224e9f8
--- /dev/null
+++ b/spec/frontend/token_access/autopopulate_allowlist_modal_spec.js
@@ -0,0 +1,125 @@
+import { GlAlert, GlModal } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { createMockDirective } from 'helpers/vue_mock_directive';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import AutopopulateAllowlistMutation from '~/token_access/graphql/mutations/autopopulate_allowlist.mutation.graphql';
+import AutopopulateAllowlistModal from '~/token_access/components/autopopulate_allowlist_modal.vue';
+import { mockAutopopulateAllowlistResponse, mockAutopopulateAllowlistError } from './mock_data';
+
+const projectName = 'My project';
+const fullPath = 'root/my-repo';
+
+Vue.use(VueApollo);
+const mockToastShow = jest.fn();
+
+describe('AutopopulateAllowlistModal component', () => {
+ let wrapper;
+ let mockApollo;
+ let mockAutopopulateMutation;
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ const createComponent = ({ props } = {}) => {
+ const handlers = [[AutopopulateAllowlistMutation, mockAutopopulateMutation]];
+ mockApollo = createMockApollo(handlers);
+
+ wrapper = shallowMountExtended(AutopopulateAllowlistModal, {
+ apolloProvider: mockApollo,
+ provide: {
+ fullPath,
+ },
+ mocks: {
+ $toast: { show: mockToastShow },
+ },
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ propsData: {
+ projectName,
+ showModal: true,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mockAutopopulateMutation = jest.fn();
+ });
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not render alert by default', () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+ });
+
+ describe('when mutation is running', () => {
+ beforeEach(() => {
+ mockAutopopulateMutation.mockResolvedValue(mockAutopopulateAllowlistResponse);
+ createComponent();
+ });
+
+ it('shows loading state for confirm button and disables cancel button', async () => {
+ expect(findModal().props('actionPrimary').attributes).toMatchObject({ loading: false });
+ expect(findModal().props('actionSecondary').attributes).toMatchObject({ disabled: false });
+
+ findModal().vm.$emit('primary', { preventDefault: jest.fn() });
+ await nextTick();
+
+ expect(findModal().props('actionPrimary').attributes).toMatchObject({ loading: true });
+ expect(findModal().props('actionSecondary').attributes).toMatchObject({ disabled: true });
+ });
+ });
+
+ describe('when mutation is successful', () => {
+ beforeEach(async () => {
+ mockAutopopulateMutation.mockResolvedValue(mockAutopopulateAllowlistResponse);
+
+ createComponent();
+ findModal().vm.$emit('primary', { preventDefault: jest.fn() });
+ await waitForPromises();
+ });
+
+ it('calls the mutation', () => {
+ expect(mockAutopopulateMutation).toHaveBeenCalledWith({ projectPath: fullPath });
+ });
+
+ it('shows toast message', () => {
+ expect(mockToastShow).toHaveBeenCalledWith(
+ 'Authentication log entries were successfully added to the allowlist.',
+ );
+ });
+
+ it('emits events for refetching data and hiding modal', () => {
+ expect(wrapper.emitted('refetch-allowlist')).toHaveLength(1);
+ expect(wrapper.emitted('hide')).toHaveLength(1);
+ });
+ });
+
+ describe('when mutation fails', () => {
+ beforeEach(async () => {
+ createComponent();
+ findModal().vm.$emit('primary', { preventDefault: jest.fn() });
+ await waitForPromises();
+
+ mockAutopopulateMutation.mockResolvedValue(mockAutopopulateAllowlistError);
+ });
+
+ it('renders alert', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('does not render toast message or emit events', () => {
+ expect(mockToastShow).not.toHaveBeenCalledWith();
+ expect(wrapper.emitted('refetch-allowlist')).toBeUndefined();
+ expect(wrapper.emitted('hide')).toBeUndefined();
+ });
+ });
+});
diff --git a/spec/frontend/token_access/inbound_token_access_spec.js b/spec/frontend/token_access/inbound_token_access_spec.js
index 0401416a9795ec37d50be05fd8cefb7c1f8242c5..556e650a867e9b653a7eb614f194b790debd3e18 100644
--- a/spec/frontend/token_access/inbound_token_access_spec.js
+++ b/spec/frontend/token_access/inbound_token_access_spec.js
@@ -6,7 +6,11 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import InboundTokenAccess from '~/token_access/components/inbound_token_access.vue';
-import { JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT } from '~/token_access/constants';
+import {
+ JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT,
+ JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG,
+} from '~/token_access/constants';
+import AutopopulateAllowlistModal from '~/token_access/components/autopopulate_allowlist_modal.vue';
import NamespaceForm from '~/token_access/components/namespace_form.vue';
import inboundRemoveGroupCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_remove_group_ci_job_token_scope.mutation.graphql';
import inboundRemoveProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_remove_project_ci_job_token_scope.mutation.graphql';
@@ -59,6 +63,7 @@ describe('TokenAccess component', () => {
const failureHandler = jest.fn().mockRejectedValue(error);
const mockToastShow = jest.fn();
+ const findAutopopulateAllowlistModal = () => wrapper.findComponent(AutopopulateAllowlistModal);
const findFormSelector = () => wrapper.findByTestId('form-selector');
const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
@@ -379,6 +384,7 @@ describe('TokenAccess component', () => {
beforeEach(() =>
createComponent(
[
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
@@ -388,18 +394,48 @@ describe('TokenAccess component', () => {
),
);
- it('toggle form button is replaced by actions dropdown', () => {
- expect(findToggleFormBtn().exists()).toBe(false);
- expect(findFormSelector().exists()).toBe(true);
- });
+ describe('autopopulate entries', () => {
+ it('replaces toggle form button with actions dropdown', () => {
+ expect(findToggleFormBtn().exists()).toBe(false);
+ expect(findFormSelector().exists()).toBe(true);
+ });
- it('Add group or project option renders the namespace form', async () => {
- expect(findNamespaceForm().exists()).toBe(false);
+ it('renders the namespace form when clicking "Add group or project option"', async () => {
+ expect(findNamespaceForm().exists()).toBe(false);
+
+ findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT);
+ await nextTick();
+
+ expect(findNamespaceForm().exists()).toBe(true);
+ });
+
+ it('renders the autopopulate allowlist modal when clicking "All projects in authentication log"', async () => {
+ expect(findAutopopulateAllowlistModal().props('showModal')).toBe(false);
- findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT);
- await nextTick();
+ findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
+ await nextTick();
+
+ expect(findAutopopulateAllowlistModal().props('showModal')).toBe(true);
+ });
+
+ it('unselects dropdown option when autopopulate allowlist modal is hidden', async () => {
+ findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
+ findAutopopulateAllowlistModal().vm.$emit('hide');
+ await nextTick();
+
+ expect(findFormSelector().props('selected')).toBe(null);
+ });
+
+ it('refetches allowlist when autopopulate mutation is successful', async () => {
+ expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(1);
+
+ findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
+ findAutopopulateAllowlistModal().vm.$emit('refetch-allowlist');
+ await nextTick();
- expect(findNamespaceForm().exists()).toBe(true);
+ expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(2);
+ expect(findFormSelector().props('selected')).toBe(null);
+ });
});
});
diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js
index 8cb2e1f79f28d0a0fe9d23bf67414ea6210aefe4..4fe4b8e2fa6065237a2ae040d5a83b6745c37b6a 100644
--- a/spec/frontend/token_access/mock_data.js
+++ b/spec/frontend/token_access/mock_data.js
@@ -287,3 +287,26 @@ export const mockAuthLogsResponse = (hasNextPage = false) => ({
},
},
});
+
+export const mockAutopopulateAllowlistResponse = {
+ data: {
+ ciJobTokenScopeAutopopulateAllowlist: {
+ status: 'complete',
+ errors: [],
+ __typename: 'CiJobTokenScopeAutopopulateAllowlistPayload',
+ },
+ },
+};
+
+export const mockAutopopulateAllowlistError = {
+ data: {
+ ciJobTokenScopeAutopopulateAllowlist: {
+ errors: [
+ {
+ message: 'An error occurred',
+ },
+ ],
+ __typename: 'CiJobTokenScopeAutopopulateAllowlistPayload',
+ },
+ },
+};