From 3bca3b62c1946d823b6032c08cf925e48f266a42 Mon Sep 17 00:00:00 2001
From: Mireya Andres
Date: Mon, 20 Jan 2025 16:33:04 +0800
Subject: [PATCH] Add feature flag
`authentication_logs_migration_for_allowlist`
This FF is for allowing the user to autopopulate the job token
allowlist using the authentication logs. This MR is just for the
creation of the feature flag. The feature will be further developed
in the following MRs.
---
.../components/inbound_token_access.vue | 48 +++++++++++++--
.../javascripts/token_access/constants.js | 3 +
.../vue_shared/components/crud_component.vue | 2 +-
.../projects/settings/ci_cd_controller.rb | 1 +
...ntication_logs_migration_for_allowlist.yml | 9 +++
locale/gitlab.pot | 6 ++
.../token_access/inbound_token_access_spec.js | 58 ++++++++++++++++++-
.../components/crud_component_spec.js | 11 ++++
8 files changed, 131 insertions(+), 7 deletions(-)
create mode 100644 config/feature_flags/gitlab_com_derisk/authentication_logs_migration_for_allowlist.yml
diff --git a/app/assets/javascripts/token_access/components/inbound_token_access.vue b/app/assets/javascripts/token_access/components/inbound_token_access.vue
index 6ad02fedefa489..b927483ade5c1f 100644
--- a/app/assets/javascripts/token_access/components/inbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/inbound_token_access.vue
@@ -2,8 +2,9 @@
import {
GlAlert,
GlButton,
- GlLink,
+ GlCollapsibleListbox,
GlIcon,
+ GlLink,
GlLoadingIcon,
GlSprintf,
GlTooltipDirective,
@@ -22,6 +23,10 @@ import inboundUpdateCIJobTokenScopeMutation from '../graphql/mutations/inbound_u
import inboundGetCIJobTokenScopeQuery from '../graphql/queries/inbound_get_ci_job_token_scope.query.graphql';
import inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery from '../graphql/queries/inbound_get_groups_and_projects_with_ci_job_token_scope.query.graphql';
import getCiJobTokenScopeAllowlistQuery from '../graphql/queries/get_ci_job_token_scope_allowlist.query.graphql';
+import {
+ JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT,
+ JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG,
+} from '../constants';
import TokenAccessTable from './token_access_table.vue';
import NamespaceForm from './namespace_form.vue';
@@ -38,6 +43,7 @@ export default {
settingDisabledMessage: s__(
'CICD|Access unrestricted, so users with sufficient permissions in this project can authenticate with a job token generated in any other project.',
),
+ add: __('Add'),
addGroupOrProject: __('Add group or project'),
projectsFetchError: __('There was a problem fetching the projects'),
scopeFetchError: __('There was a problem fetching the job token scope value'),
@@ -58,11 +64,22 @@ export default {
text: s__('CICD|Only this project and any groups and projects in the allowlist'),
},
],
+ crudFormActions: [
+ {
+ text: __('Group or project'),
+ value: JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT,
+ },
+ {
+ text: __('All projects in authentication log'),
+ value: JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG,
+ },
+ ],
components: {
GlAlert,
GlButton,
- GlLink,
+ GlCollapsibleListbox,
GlIcon,
+ GlLink,
GlLoadingIcon,
GlSprintf,
CrudComponent,
@@ -132,6 +149,7 @@ export default {
projectName: '',
namespaceToEdit: null,
namespaceToRemove: null,
+ selectedAction: null,
};
},
computed: {
@@ -147,6 +165,9 @@ export default {
const { groups, projects } = this.groupsAndProjectsWithAccess;
return [...groups, ...projects];
},
+ canAutopopulateAuthLog() {
+ return this.glFeatures.authenticationLogsMigrationForAllowlist;
+ },
groupCount() {
return this.groupsAndProjectsWithAccess.groups.length;
},
@@ -169,6 +190,10 @@ export default {
},
},
methods: {
+ hideSelectedAction() {
+ this.namespaceToEdit = null;
+ this.selectedAction = null;
+ },
mapAllowlistNodes(list) {
// The defaultPermissions and jobTokenPolicies are separate fields from the target (the group or project in the
// allowlist). Combine them into a single object.
@@ -235,6 +260,11 @@ export default {
refetchGroupsAndProjects() {
this.$apollo.queries.groupsAndProjectsWithAccess.refetch();
},
+ selectAction(action, showFormFn) {
+ // TODO: render autopopulate modal when selected
+ this.selectedAction = action;
+ showFormFn();
+ },
showNamespaceForm(namespace, showFormFn) {
this.namespaceToEdit = namespace;
showFormFn();
@@ -289,10 +319,20 @@ export default {
+
+
+
diff --git a/app/assets/javascripts/token_access/constants.js b/app/assets/javascripts/token_access/constants.js
index 46f0e443ac43d0..80c013b2ed4c08 100644
--- a/app/assets/javascripts/token_access/constants.js
+++ b/app/assets/javascripts/token_access/constants.js
@@ -145,3 +145,6 @@ export const JOB_TOKEN_POLICIES = keyBy(
POLICIES_BY_RESOURCE.flatMap(({ policies }) => policies),
({ value }) => value,
);
+
+export const JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT = 'JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT';
+export const JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG = 'JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG';
diff --git a/app/assets/javascripts/vue_shared/components/crud_component.vue b/app/assets/javascripts/vue_shared/components/crud_component.vue
index aa97eca6141837..671f4f253623a9 100644
--- a/app/assets/javascripts/vue_shared/components/crud_component.vue
+++ b/app/assets/javascripts/vue_shared/components/crud_component.vue
@@ -217,7 +217,7 @@ export default {
-
+
{
const failureHandler = jest.fn().mockRejectedValue(error);
const mockToastShow = jest.fn();
+ const findFormSelector = () => wrapper.findByTestId('form-selector');
const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findToggleFormBtn = () => wrapper.findByTestId('crud-form-toggle');
@@ -72,13 +74,18 @@ describe('TokenAccess component', () => {
const createComponent = (
requestHandlers,
- { addPoliciesToCiJobToken = false, enforceAllowlist = false, stubs = {} } = {},
+ {
+ addPoliciesToCiJobToken = false,
+ authenticationLogsMigrationForAllowlist = false,
+ enforceAllowlist = false,
+ stubs = {},
+ } = {},
) => {
wrapper = shallowMountExtended(InboundTokenAccess, {
provide: {
fullPath: projectPath,
enforceAllowlist,
- glFeatures: { addPoliciesToCiJobToken },
+ glFeatures: { addPoliciesToCiJobToken, authenticationLogsMigrationForAllowlist },
},
apolloProvider: createMockApollo(requestHandlers),
mocks: {
@@ -349,6 +356,53 @@ describe('TokenAccess component', () => {
});
});
+ describe('when authenticationLogsMigrationForAllowlist feature flag is disabled', () => {
+ beforeEach(() =>
+ createComponent(
+ [
+ [
+ inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
+ inboundGroupsAndProjectsWithScopeResponseHandler,
+ ],
+ ],
+ { authenticationLogsMigrationForAllowlist: false, stubs: { CrudComponent } },
+ ),
+ );
+
+ it('renders toggle form button and hides actions dropdown', () => {
+ expect(findToggleFormBtn().exists()).toBe(true);
+ expect(findFormSelector().exists()).toBe(false);
+ });
+ });
+
+ describe('when authenticationLogsMigrationForAllowlist feature flag is enabled', () => {
+ beforeEach(() =>
+ createComponent(
+ [
+ [
+ inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
+ inboundGroupsAndProjectsWithScopeResponseHandler,
+ ],
+ ],
+ { authenticationLogsMigrationForAllowlist: true, stubs: { CrudComponent } },
+ ),
+ );
+
+ it('toggle form button is replaced by 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);
+
+ findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT);
+ await nextTick();
+
+ expect(findNamespaceForm().exists()).toBe(true);
+ });
+ });
+
describe.each`
type | mutation | handler
${'Group'} | ${inboundRemoveGroupCIJobTokenScopeMutation} | ${inboundRemoveGroupSuccessHandler}
diff --git a/spec/frontend/vue_shared/components/crud_component_spec.js b/spec/frontend/vue_shared/components/crud_component_spec.js
index d7b17bcc77a57b..01bc9914852aab 100644
--- a/spec/frontend/vue_shared/components/crud_component_spec.js
+++ b/spec/frontend/vue_shared/components/crud_component_spec.js
@@ -241,4 +241,15 @@ describe('CRUD Component', () => {
);
});
});
+
+ describe('actions slot', () => {
+ it('passes the showForm function to the actions slot', () => {
+ const actionsSlot = jest.fn();
+ createComponent({}, { actions: actionsSlot });
+
+ expect(actionsSlot).toHaveBeenCalledWith(
+ expect.objectContaining({ showForm: wrapper.vm.showForm }),
+ );
+ });
+ });
});
--
GitLab