From a0393f766f5b2d9956bc03457c0f27f27e67c8f8 Mon Sep 17 00:00:00 2001 From: Mireya Andres Date: Mon, 9 Jan 2023 23:02:23 +0800 Subject: [PATCH 1/3] Toggle JWT access from CI/CD settings Adds a new setting that limits JWT access for pipeline jobs. When enabled, JWT must be manually declared in each job that needs it. Changelog: added --- .../token_access/components/opt_in_jwt.vue | 122 +++++++++++++++++ .../token_access/components/token_access.vue | 3 + .../update_opt_in_jwt.mutation.graphql | 8 ++ .../get_opt_in_jwt_setting.query.graphql | 8 ++ locale/gitlab.pot | 12 ++ spec/frontend/token_access/mock_data.js | 26 ++++ spec/frontend/token_access/opt_in_jwt_spec.js | 128 ++++++++++++++++++ .../token_access/token_access_spec.js | 17 +++ 8 files changed, 324 insertions(+) create mode 100644 app/assets/javascripts/token_access/components/opt_in_jwt.vue create mode 100644 app/assets/javascripts/token_access/graphql/mutations/update_opt_in_jwt.mutation.graphql create mode 100644 app/assets/javascripts/token_access/graphql/queries/get_opt_in_jwt_setting.query.graphql create mode 100644 spec/frontend/token_access/opt_in_jwt_spec.js diff --git a/app/assets/javascripts/token_access/components/opt_in_jwt.vue b/app/assets/javascripts/token_access/components/opt_in_jwt.vue new file mode 100644 index 00000000000000..a8244685edd592 --- /dev/null +++ b/app/assets/javascripts/token_access/components/opt_in_jwt.vue @@ -0,0 +1,122 @@ + + diff --git a/app/assets/javascripts/token_access/components/token_access.vue b/app/assets/javascripts/token_access/components/token_access.vue index fe99f3e1fdd365..527f01f0a6fdc8 100644 --- a/app/assets/javascripts/token_access/components/token_access.vue +++ b/app/assets/javascripts/token_access/components/token_access.vue @@ -17,6 +17,7 @@ import removeProjectCIJobTokenScopeMutation from '../graphql/mutations/remove_pr import updateCIJobTokenScopeMutation from '../graphql/mutations/update_ci_job_token_scope.mutation.graphql'; import getCIJobTokenScopeQuery from '../graphql/queries/get_ci_job_token_scope.query.graphql'; import getProjectsWithCIJobTokenScopeQuery from '../graphql/queries/get_projects_with_ci_job_token_scope.query.graphql'; +import OptInJwt from './opt_in_jwt.vue'; import TokenProjectsTable from './token_projects_table.vue'; export default { @@ -44,6 +45,7 @@ export default { GlLoadingIcon, GlSprintf, GlToggle, + OptInJwt, TokenProjectsTable, }, inject: { @@ -230,6 +232,7 @@ export default { + diff --git a/app/assets/javascripts/token_access/graphql/mutations/update_opt_in_jwt.mutation.graphql b/app/assets/javascripts/token_access/graphql/mutations/update_opt_in_jwt.mutation.graphql new file mode 100644 index 00000000000000..c12b5646423e54 --- /dev/null +++ b/app/assets/javascripts/token_access/graphql/mutations/update_opt_in_jwt.mutation.graphql @@ -0,0 +1,8 @@ +mutation updateOptInJwt($input: CiCdSettingsUpdateInput!) { + ciCdSettingsUpdate(input: $input) { + ciCdSettings { + optInJwt + } + errors + } +} diff --git a/app/assets/javascripts/token_access/graphql/queries/get_opt_in_jwt_setting.query.graphql b/app/assets/javascripts/token_access/graphql/queries/get_opt_in_jwt_setting.query.graphql new file mode 100644 index 00000000000000..a1a216b7dc3501 --- /dev/null +++ b/app/assets/javascripts/token_access/graphql/queries/get_opt_in_jwt_setting.query.graphql @@ -0,0 +1,8 @@ +query getOptInJwtSetting($fullPath: ID!) { + project(fullPath: $fullPath) { + id + ciCdSettings { + optInJwt + } + } +} diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 5a60905422da70..f1761a7628e41c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7721,6 +7721,9 @@ msgstr "" msgid "CICD|Limit CI_JOB_TOKEN access" msgstr "" +msgid "CICD|Limit JSON Web Token (JWT) access" +msgstr "" + msgid "CICD|Select the projects that can be accessed by API requests authenticated with this project's CI_JOB_TOKEN CI/CD variable. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API. %{linkStart}Learn more.%{linkEnd}" msgstr "" @@ -7730,6 +7733,9 @@ msgstr "" msgid "CICD|The Auto DevOps pipeline runs if no alternative CI configuration file is found." msgstr "" +msgid "CICD|The JWT must be manually declared in each job that needs it. When disabled, the token is always available in all jobs in the pipeline." +msgstr "" + msgid "CICD|There are several CI/CD limits in place." msgstr "" @@ -7739,6 +7745,9 @@ msgstr "" msgid "CICD|Use separate caches for protected branches" msgstr "" +msgid "CICD|Use the %{codeStart}secrets%{codeEnd} keyword to configure a job with a JWT." +msgstr "" + msgid "CICD|group enabled" msgstr "" @@ -42623,6 +42632,9 @@ msgstr "" msgid "There was a problem fetching the projects" msgstr "" +msgid "There was a problem fetching the token access settings." +msgstr "" + msgid "There was a problem fetching users." msgstr "" diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js index 0c8ba266201bec..fff5a0ad4d0dac 100644 --- a/spec/frontend/token_access/mock_data.js +++ b/spec/frontend/token_access/mock_data.js @@ -105,3 +105,29 @@ export const mockProjects = [ __typename: 'Project', }, ]; + +export const optInJwtQueryResponse = (optInJwt) => ({ + data: { + project: { + id: '1', + ciCdSettings: { + optInJwt, + __typename: 'ProjectCiCdSetting', + }, + __typename: 'Project', + }, + }, +}); + +export const optInJwtMutationResponse = (optInJwt) => ({ + data: { + ciCdSettingsUpdate: { + ciCdSettings: { + optInJwt, + __typename: 'ProjectCiCdSetting', + }, + errors: [], + __typename: 'CiCdSettingsUpdatePayload', + }, + }, +}); diff --git a/spec/frontend/token_access/opt_in_jwt_spec.js b/spec/frontend/token_access/opt_in_jwt_spec.js new file mode 100644 index 00000000000000..a6d0a848a6f1a1 --- /dev/null +++ b/spec/frontend/token_access/opt_in_jwt_spec.js @@ -0,0 +1,128 @@ +import { GlLoadingIcon, GlToggle } from '@gitlab/ui'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createAlert } from '~/flash'; +import OptInJwt from '~/token_access/components/opt_in_jwt.vue'; +import getOptInJwtSettingQuery from '~/token_access/graphql/queries/get_opt_in_jwt_setting.query.graphql'; +import updateOptInJwtMutation from '~/token_access/graphql/mutations/update_opt_in_jwt.mutation.graphql'; +import { optInJwtMutationResponse, optInJwtQueryResponse } from './mock_data'; + +const errorMessage = 'An error occurred'; +const error = new Error(errorMessage); + +Vue.use(VueApollo); + +jest.mock('~/flash'); + +describe('OptInJwt component', () => { + let wrapper; + + const failureHandler = jest.fn().mockRejectedValue(error); + const enabledOptInJwtHandler = jest.fn().mockResolvedValue(optInJwtQueryResponse(true)); + const disabledOptInJwtHandler = jest.fn().mockResolvedValue(optInJwtQueryResponse(false)); + const updateOptInJwtHandler = jest.fn().mockResolvedValue(optInJwtMutationResponse(true)); + + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findToggle = () => wrapper.findComponent(GlToggle); + const findOptInJwtExpandedSection = () => wrapper.findByTestId('opt-in-jwt-expanded-section'); + + const createMockApolloProvider = (requestHandlers) => { + return createMockApollo(requestHandlers); + }; + + const createComponent = (requestHandlers, mountFn = shallowMountExtended) => { + wrapper = mountFn(OptInJwt, { + provide: { + fullPath: 'root/my-repo', + }, + apolloProvider: createMockApolloProvider(requestHandlers), + data() { + return { + targetProjectPath: 'root/test', + }; + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('loading state', () => { + it('shows loading state and hide toggle while waiting on query to resolve', async () => { + createComponent([[getOptInJwtSettingQuery, enabledOptInJwtHandler]]); + + expect(findLoadingIcon().exists()).toBe(true); + expect(findToggle().exists()).toBe(false); + + await waitForPromises(); + + expect(findLoadingIcon().exists()).toBe(false); + expect(findToggle().exists()).toBe(true); + }); + }); + + describe('toggle JWT token access', () => { + it('code instruction is visible when toggle is enabled', async () => { + createComponent([[getOptInJwtSettingQuery, enabledOptInJwtHandler]]); + + await waitForPromises(); + + expect(findToggle().props('value')).toBe(true); + expect(findOptInJwtExpandedSection().exists()).toBe(true); + }); + + it('code instruction is hidden when toggle is disabled', async () => { + createComponent([[getOptInJwtSettingQuery, disabledOptInJwtHandler]]); + + await waitForPromises(); + + expect(findToggle().props('value')).toBe(false); + expect(findOptInJwtExpandedSection().exists()).toBe(false); + }); + + describe('update JWT token access', () => { + it('calls updateOptInJwtMutation with correct arguments', async () => { + createComponent( + [ + [getOptInJwtSettingQuery, disabledOptInJwtHandler], + [updateOptInJwtMutation, updateOptInJwtHandler], + ], + mountExtended, + ); + + await waitForPromises(); + + findToggle().vm.$emit('change', true); + + expect(updateOptInJwtHandler).toHaveBeenCalledWith({ + input: { + fullPath: 'root/my-repo', + optInJwt: true, + }, + }); + }); + + it('handles update error', async () => { + createComponent( + [ + [getOptInJwtSettingQuery, enabledOptInJwtHandler], + [updateOptInJwtMutation, failureHandler], + ], + mountExtended, + ); + + await waitForPromises(); + + findToggle().vm.$emit('change', false); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ message: errorMessage }); + }); + }); + }); +}); diff --git a/spec/frontend/token_access/token_access_spec.js b/spec/frontend/token_access/token_access_spec.js index 6fe94e285480b2..62f546463a1f56 100644 --- a/spec/frontend/token_access/token_access_spec.js +++ b/spec/frontend/token_access/token_access_spec.js @@ -5,6 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { createAlert } from '~/flash'; +import OptInJwt from '~/token_access/components/opt_in_jwt.vue'; import TokenAccess from '~/token_access/components/token_access.vue'; import addProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql'; import removeProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql'; @@ -40,6 +41,7 @@ describe('TokenAccess component', () => { const failureHandler = jest.fn().mockRejectedValue(error); const findToggle = () => wrapper.findComponent(GlToggle); + const findOptInJwt = () => wrapper.findComponent(OptInJwt); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findAddProjectBtn = () => wrapper.findByRole('button', { name: 'Add project' }); const findRemoveProjectBtn = () => wrapper.findByRole('button', { name: 'Remove access' }); @@ -82,6 +84,21 @@ describe('TokenAccess component', () => { }); }); + describe('template', () => { + beforeEach(async () => { + createComponent([ + [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler], + [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler], + ]); + + await waitForPromises(); + }); + + it('renders the opt in jwt component', () => { + expect(findOptInJwt().exists()).toBe(true); + }); + }); + describe('fetching projects and scope', () => { it('fetches projects and scope correctly', () => { const expectedVariables = { -- GitLab From 222f097baef2041b54021c0ced170b9af95354b5 Mon Sep 17 00:00:00 2001 From: Mireya Andres Date: Thu, 19 Jan 2023 18:19:31 +0800 Subject: [PATCH 2/3] Apply frontend review comments --- .../token_access/components/opt_in_jwt.vue | 52 ++++++++++--------- locale/gitlab.pot | 6 +-- spec/frontend/token_access/opt_in_jwt_spec.js | 6 +-- 3 files changed, 31 insertions(+), 33 deletions(-) diff --git a/app/assets/javascripts/token_access/components/opt_in_jwt.vue b/app/assets/javascripts/token_access/components/opt_in_jwt.vue index a8244685edd592..caa5b84ce861f9 100644 --- a/app/assets/javascripts/token_access/components/opt_in_jwt.vue +++ b/app/assets/javascripts/token_access/components/opt_in_jwt.vue @@ -25,7 +25,7 @@ export default { 'CICD|Use the %{codeStart}secrets%{codeEnd} keyword to configure a job with a JWT.', ), copyToClipboard: __('Copy to clipboard'), - fetchError: __('There was a problem fetching the token access settings.'), + fetchError: __('CICD|There was a problem fetching the token access settings.'), }, components: { CodeInstruction, @@ -93,30 +93,32 @@ export default { };