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 02605dfb4fb3af6a382b3595f0fb81ed67e42c21..31575cd36b71842bc6ccc65d8c978d5ac56db7af 100644
--- a/app/assets/javascripts/token_access/components/inbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/inbound_token_access.vue
@@ -13,42 +13,43 @@ import {
import { createAlert } from '~/alert';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
-import inboundAddProjectCIJobTokenScopeMutation from '../graphql/mutations/inbound_add_project_ci_job_token_scope.mutation.graphql';
+import { TYPENAME_GROUP } from '~/graphql_shared/constants';
+import inboundAddGroupOrProjectCIJobTokenScope from '../graphql/mutations/inbound_add_group_or_project_ci_job_token_scope.mutation.graphql';
import inboundRemoveProjectCIJobTokenScopeMutation from '../graphql/mutations/inbound_remove_project_ci_job_token_scope.mutation.graphql';
+import inboundRemoveGroupCIJobTokenScopeMutation from '../graphql/mutations/inbound_remove_group_ci_job_token_scope.mutation.graphql';
import inboundUpdateCIJobTokenScopeMutation from '../graphql/mutations/inbound_update_ci_job_token_scope.mutation.graphql';
import inboundGetCIJobTokenScopeQuery from '../graphql/queries/inbound_get_ci_job_token_scope.query.graphql';
-import inboundGetProjectsWithCIJobTokenScopeQuery from '../graphql/queries/inbound_get_projects_with_ci_job_token_scope.query.graphql';
-import TokenProjectsTable from './token_projects_table.vue';
+import inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery from '../graphql/queries/inbound_get_groups_and_projects_with_ci_job_token_scope.query.graphql';
+import TokenAccessTable from './token_access_table.vue';
export default {
i18n: {
toggleLabelTitle: s__('CICD|Limit access %{italicStart}to%{italicEnd} this project'),
- toggleHelpText: s__(
- `CICD|Prevent access to this project from other project CI/CD job tokens, unless the other project is added to the allowlist. 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}.`,
- ),
- cardHeaderTitle: s__(
- 'CICD|Allow CI job tokens from the following projects to access this project',
+ toggleDescription: s__(
+ `CICD|Allow access to this project from authorized groups or projects by adding them to the allowlist. 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}.`,
),
+ cardHeaderTitle: s__('CICD|Groups and projects with access'),
settingDisabledMessage: s__(
- 'CICD|Enable feature to limit job token access, so only the projects in this list can access this project with a CI/CD job token.',
+ 'CICD|No access is currently allowed to this project. Enable feature to authorize access from groups or projects in the allowlist below.',
),
- addProject: __('Add project'),
+ addGroupOrProject: __('Add group or project'),
+ add: __('Add'),
cancel: __('Cancel'),
- addProjectPlaceholder: __('Paste project path (i.e. gitlab-org/gitlab)'),
+ addProjectPlaceholder: __(
+ 'Paste group path (i.e. gitlab-org) or project path (i.e. gitlab-org/gitlab)',
+ ),
projectsFetchError: __('There was a problem fetching the projects'),
scopeFetchError: __('There was a problem fetching the job token scope value'),
},
fields: [
{
- key: 'project',
- label: __('Project with access'),
- thClass: 'gl-border-t-none!',
+ key: 'fullPath',
+ label: '',
},
{
key: 'actions',
label: '',
tdClass: 'gl-text-right',
- thClass: 'gl-border-t-none!',
},
],
components: {
@@ -61,7 +62,7 @@ export default {
GlLoadingIcon,
GlSprintf,
GlToggle,
- TokenProjectsTable,
+ TokenAccessTable,
},
inject: {
fullPath: {
@@ -83,15 +84,16 @@ export default {
createAlert({ message: this.$options.i18n.scopeFetchError });
},
},
- projects: {
- query: inboundGetProjectsWithCIJobTokenScopeQuery,
+ groupsAndProjectsWithAccess: {
+ query: inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
variables() {
return {
fullPath: this.fullPath,
};
},
update({ project }) {
- return project?.ciJobTokenScope?.inboundAllowlist?.nodes ?? [];
+ this.projects = project?.ciJobTokenScope?.inboundAllowlist?.nodes ?? [];
+ this.groups = project?.ciJobTokenScope?.groupsAllowlist?.nodes ?? [];
},
error() {
createAlert({ message: this.$options.i18n.projectsFetchError });
@@ -101,14 +103,15 @@ export default {
data() {
return {
inboundJobTokenScopeEnabled: null,
- targetProjectPath: '',
+ targetPath: '',
projects: [],
+ groups: [],
isAddFormVisible: false,
};
},
computed: {
- isProjectPathEmpty() {
- return this.targetProjectPath === '';
+ isTargetPathEmpty() {
+ return this.targetPath === '';
},
ciJobTokenHelpPage() {
return helpPagePath('ci/jobs/ci_job_token#allow-access-to-your-project-with-a-job-token');
@@ -139,17 +142,17 @@ export default {
createAlert({ message: error.message });
}
},
- async addProject() {
+ async addGroupOrProject() {
try {
const {
data: {
- ciJobTokenScopeAddProject: { errors },
+ ciJobTokenScopeAddGroupOrProject: { errors },
},
} = await this.$apollo.mutate({
- mutation: inboundAddProjectCIJobTokenScopeMutation,
+ mutation: inboundAddGroupOrProjectCIJobTokenScope,
variables: {
projectPath: this.fullPath,
- targetProjectPath: this.targetProjectPath,
+ targetPath: this.targetPath,
},
});
@@ -159,23 +162,38 @@ export default {
} catch (error) {
createAlert({ message: error.message });
} finally {
- this.clearTargetProjectPath();
- this.getProjects();
+ this.clearTargetPath();
+ this.getGroupsAndProjects();
}
},
- async removeProject(removeTargetPath) {
+ async removeItem(item) {
try {
- const {
- data: {
- ciJobTokenScopeRemoveProject: { errors },
- },
- } = await this.$apollo.mutate({
- mutation: inboundRemoveProjectCIJobTokenScopeMutation,
- variables: {
- projectPath: this.fullPath,
- targetProjectPath: removeTargetPath,
- },
- });
+ let errors;
+
+ // eslint-disable-next-line no-underscore-dangle
+ if (item.__typename === TYPENAME_GROUP) {
+ const {
+ data: { ciJobTokenScopeRemoveGroup },
+ } = await this.$apollo.mutate({
+ mutation: inboundRemoveGroupCIJobTokenScopeMutation,
+ variables: {
+ projectPath: this.fullPath,
+ targetGroupPath: item.fullPath,
+ },
+ });
+ errors = ciJobTokenScopeRemoveGroup.errors;
+ } else {
+ const {
+ data: { ciJobTokenScopeRemoveProject },
+ } = await this.$apollo.mutate({
+ mutation: inboundRemoveProjectCIJobTokenScopeMutation,
+ variables: {
+ projectPath: this.fullPath,
+ targetProjectPath: item.fullPath,
+ },
+ });
+ errors = ciJobTokenScopeRemoveProject.errors;
+ }
if (errors.length) {
throw new Error(errors[0]);
@@ -183,15 +201,15 @@ export default {
} catch (error) {
createAlert({ message: error.message });
} finally {
- this.getProjects();
+ this.getGroupsAndProjects();
}
},
- clearTargetProjectPath() {
- this.targetProjectPath = '';
+ clearTargetPath() {
+ this.targetPath = '';
this.isAddFormVisible = false;
},
- getProjects() {
- this.$apollo.queries.projects.refetch();
+ getGroupsAndProjects() {
+ this.$apollo.queries.groupsAndProjectsWithAccess.refetch();
},
showAddForm() {
this.isAddFormVisible = true;
@@ -206,6 +224,7 @@ export default {
@@ -215,8 +234,8 @@ export default {
-
-
+
+
{{
content
@@ -226,15 +245,29 @@ export default {
+
+ {{ $options.i18n.settingDisabledMessage }}
+
+
{{ $options.i18n.cardHeaderTitle }}
+
+
+ {{ groups.length }}
+
{{ projects.length }}
@@ -246,46 +279,44 @@ export default {
size="small"
data-testid="toggle-form-btn"
@click="showAddForm"
- >{{ $options.i18n.addProject }}{{ $options.i18n.addGroupOrProject }}
-
+
+
-
- {{ $options.i18n.settingDisabledMessage }}
-
diff --git a/app/assets/javascripts/token_access/components/outbound_token_access.vue b/app/assets/javascripts/token_access/components/outbound_token_access.vue
index a819ab45f55f170e84a8d29721010cf702fa354c..aad46e730d83890b53214c8b82f8f43208c53f75 100644
--- a/app/assets/javascripts/token_access/components/outbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/outbound_token_access.vue
@@ -18,7 +18,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 TokenProjectsTable from './token_projects_table.vue';
+import TokenAccessTable from './token_access_table.vue';
// Note: This component will be removed in 17.0, as the outbound access token is getting deprecated
export default {
@@ -48,15 +48,13 @@ export default {
}),
fields: [
{
- key: 'project',
+ key: 'fullPath',
label: __('Project that can be accessed'),
- thClass: 'gl-border-t-none!',
},
{
key: 'actions',
label: '',
tdClass: 'gl-text-right',
- thClass: 'gl-border-t-none!',
},
],
components: {
@@ -68,7 +66,7 @@ export default {
GlLoadingIcon,
GlSprintf,
GlToggle,
- TokenProjectsTable,
+ TokenAccessTable,
},
mixins: [glFeatureFlagMixin()],
inject: {
@@ -174,7 +172,7 @@ export default {
this.getProjects();
}
},
- async removeProject(removeTargetPath) {
+ async removeProject(project) {
try {
const {
data: {
@@ -185,7 +183,7 @@ export default {
variables: {
input: {
projectPath: this.fullPath,
- targetProjectPath: removeTargetPath,
+ targetProjectPath: project.fullPath,
},
},
});
@@ -282,10 +280,10 @@ export default {
{{ $options.i18n.addProject }}
-
diff --git a/app/assets/javascripts/token_access/components/token_access_table.vue b/app/assets/javascripts/token_access/components/token_access_table.vue
new file mode 100644
index 0000000000000000000000000000000000000000..700c6b9ef3958131e70b36bd74b17fea167d04c5
--- /dev/null
+++ b/app/assets/javascripts/token_access/components/token_access_table.vue
@@ -0,0 +1,67 @@
+
+
+
+
+ {{ item.fullPath }}
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/token_access/components/token_projects_table.vue b/app/assets/javascripts/token_access/components/token_projects_table.vue
deleted file mode 100644
index 4245b39dec1b08031ff7ef19a65039e8be6a7590..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/token_access/components/token_projects_table.vue
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
-
-
-
-
-
-
- {{ item.fullPath }}
-
-
-
-
-
-
-
diff --git a/app/assets/javascripts/token_access/graphql/mutations/inbound_add_group_or_project_ci_job_token_scope.mutation.graphql b/app/assets/javascripts/token_access/graphql/mutations/inbound_add_group_or_project_ci_job_token_scope.mutation.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..94b34a6c1eb9feb42c1b0a1f4fa0739d49594a3f
--- /dev/null
+++ b/app/assets/javascripts/token_access/graphql/mutations/inbound_add_group_or_project_ci_job_token_scope.mutation.graphql
@@ -0,0 +1,5 @@
+mutation inboundAddGroupOrProjectCIJobTokenScope($projectPath: ID!, $targetPath: ID!) {
+ ciJobTokenScopeAddGroupOrProject(input: { projectPath: $projectPath, targetPath: $targetPath }) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/token_access/graphql/mutations/inbound_add_project_ci_job_token_scope.mutation.graphql b/app/assets/javascripts/token_access/graphql/mutations/inbound_add_project_ci_job_token_scope.mutation.graphql
deleted file mode 100644
index f030a892af2ca2b27cbbaaaeb444e534ad11e817..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/token_access/graphql/mutations/inbound_add_project_ci_job_token_scope.mutation.graphql
+++ /dev/null
@@ -1,7 +0,0 @@
-mutation inboundAddProjectCIJobTokenScope($projectPath: ID!, $targetProjectPath: ID!) {
- ciJobTokenScopeAddProject(
- input: { projectPath: $projectPath, targetProjectPath: $targetProjectPath, direction: INBOUND }
- ) {
- errors
- }
-}
diff --git a/app/assets/javascripts/token_access/graphql/mutations/inbound_remove_group_ci_job_token_scope.mutation.graphql b/app/assets/javascripts/token_access/graphql/mutations/inbound_remove_group_ci_job_token_scope.mutation.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..5681c5967adf8e2f24fa7dadde58dc9efc3b3c93
--- /dev/null
+++ b/app/assets/javascripts/token_access/graphql/mutations/inbound_remove_group_ci_job_token_scope.mutation.graphql
@@ -0,0 +1,7 @@
+mutation inboundRemoveGroupCIJobTokenScope($projectPath: ID!, $targetGroupPath: ID!) {
+ ciJobTokenScopeRemoveGroup(
+ input: { projectPath: $projectPath, targetGroupPath: $targetGroupPath }
+ ) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/token_access/graphql/queries/inbound_get_projects_with_ci_job_token_scope.query.graphql b/app/assets/javascripts/token_access/graphql/queries/inbound_get_groups_and_projects_with_ci_job_token_scope.query.graphql
similarity index 50%
rename from app/assets/javascripts/token_access/graphql/queries/inbound_get_projects_with_ci_job_token_scope.query.graphql
rename to app/assets/javascripts/token_access/graphql/queries/inbound_get_groups_and_projects_with_ci_job_token_scope.query.graphql
index c51bdcbf7d2ece38c34804704a018375e91f541e..641642ffc7677b2f1b3c5f17810a48c9a1610b2d 100644
--- a/app/assets/javascripts/token_access/graphql/queries/inbound_get_projects_with_ci_job_token_scope.query.graphql
+++ b/app/assets/javascripts/token_access/graphql/queries/inbound_get_groups_and_projects_with_ci_job_token_scope.query.graphql
@@ -1,15 +1,18 @@
-query inboundGetProjectsWithCIJobTokenScope($fullPath: ID!) {
+query inboundGetGroupsAndProjectsWithCIJobTokenScope($fullPath: ID!) {
project(fullPath: $fullPath) {
id
ciJobTokenScope {
+ groupsAllowlist {
+ nodes {
+ id
+ name
+ fullPath
+ }
+ }
inboundAllowlist {
nodes {
id
name
- namespace {
- id
- fullPath
- }
fullPath
}
}
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 67402b1f8ab545e79123f89575d541a1e501f5c0..22a1672df44bc4cdeae562b6e3f65a7a0a82f119 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3101,6 +3101,9 @@ msgstr ""
msgid "Add existing issue"
msgstr ""
+msgid "Add group or project"
+msgstr ""
+
msgid "Add header and footer to emails. Please note that color settings will only be applied within the application interface"
msgstr ""
@@ -9716,7 +9719,7 @@ msgstr ""
msgid "CI/CD limits"
msgstr ""
-msgid "CI/CD|No projects have been added to the scope"
+msgid "CI/CD|No %{itemType}s have been added to the scope"
msgstr ""
msgid "CICDAnalytics|%{percent}%{percentSymbol}"
@@ -9772,7 +9775,7 @@ msgstr ""
msgid "CICD|Add an existing project to the scope"
msgstr ""
-msgid "CICD|Allow CI job tokens from the following projects to access this project"
+msgid "CICD|Allow access to this project from authorized groups or projects by adding them to the allowlist. 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 ""
msgid "CICD|Auto DevOps"
@@ -9805,7 +9808,7 @@ msgstr ""
msgid "CICD|Enable feature to limit job token access to the following projects."
msgstr ""
-msgid "CICD|Enable feature to limit job token access, so only the projects in this list can access this project with a CI/CD job token."
+msgid "CICD|Groups and projects with access"
msgstr ""
msgid "CICD|Jobs"
@@ -9823,13 +9826,13 @@ msgstr ""
msgid "CICD|Maintainer"
msgstr ""
-msgid "CICD|Pipelines and jobs cannot be cancelled"
+msgid "CICD|No access is currently allowed to this project. Enable feature to authorize access from groups or projects in the allowlist below."
msgstr ""
-msgid "CICD|Prevent CI/CD job tokens from this project from being used to access other projects unless the other project is added to the allowlist. 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}."
+msgid "CICD|Pipelines and jobs cannot be cancelled"
msgstr ""
-msgid "CICD|Prevent access to this project from other project CI/CD job tokens, unless the other project is added to the allowlist. 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}."
+msgid "CICD|Prevent CI/CD job tokens from this project from being used to access other projects unless the other project is added to the allowlist. 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 ""
msgid "CICD|The %{boldStart}Limit access %{boldEnd}%{italicAndBoldStart}from%{italicAndBoldEnd}%{boldStart} this project%{boldEnd} setting is deprecated and will be removed in the 18.0 milestone. Use the %{boldStart}Limit access %{boldEnd}%{italicAndBoldStart}to%{italicAndBoldEnd}%{boldStart} this project%{boldEnd} setting and allowlist instead. %{linkStart}How do I do this?%{linkEnd}"
@@ -37079,6 +37082,9 @@ msgstr ""
msgid "Paste epic link"
msgstr ""
+msgid "Paste group path (i.e. gitlab-org) or project path (i.e. gitlab-org/gitlab)"
+msgstr ""
+
msgid "Paste issue link"
msgstr ""
@@ -40046,9 +40052,6 @@ msgstr ""
msgid "Project was not found or you do not have permission to add this project to Security Dashboards."
msgstr ""
-msgid "Project with access"
-msgstr ""
-
msgid "Project: %{name}"
msgstr ""
diff --git a/spec/frontend/token_access/inbound_token_access_spec.js b/spec/frontend/token_access/inbound_token_access_spec.js
index d82d65e35495d5affb9019a6a7ea614534648782..a7b0a9820b502fa97333639f8166528dc8fb5f52 100644
--- a/spec/frontend/token_access/inbound_token_access_spec.js
+++ b/spec/frontend/token_access/inbound_token_access_spec.js
@@ -6,21 +6,24 @@ import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_help
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import InboundTokenAccess from '~/token_access/components/inbound_token_access.vue';
-import inboundAddProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_add_project_ci_job_token_scope.mutation.graphql';
+import inboundAddGroupOrProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_add_group_or_project_ci_job_token_scope.mutation.graphql';
+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';
import inboundUpdateCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_update_ci_job_token_scope.mutation.graphql';
import inboundGetCIJobTokenScopeQuery from '~/token_access/graphql/queries/inbound_get_ci_job_token_scope.query.graphql';
-import inboundGetProjectsWithCIJobTokenScopeQuery from '~/token_access/graphql/queries/inbound_get_projects_with_ci_job_token_scope.query.graphql';
+import inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery from '~/token_access/graphql/queries/inbound_get_groups_and_projects_with_ci_job_token_scope.query.graphql';
import {
inboundJobTokenScopeEnabledResponse,
inboundJobTokenScopeDisabledResponse,
- inboundProjectsWithScopeResponse,
- inboundAddProjectSuccessResponse,
+ inboundGroupsAndProjectsWithScopeResponse,
+ inboundAddGroupOrProjectSuccessResponse,
+ inboundRemoveGroupSuccess,
inboundRemoveProjectSuccess,
inboundUpdateScopeSuccessResponse,
} from './mock_data';
const projectPath = 'root/my-repo';
+const testGroupPath = 'gitlab-org';
const testProjectPath = 'root/test';
const message = 'An error occurred';
const error = new Error(message);
@@ -38,12 +41,13 @@ describe('TokenAccess component', () => {
const inboundJobTokenScopeDisabledResponseHandler = jest
.fn()
.mockResolvedValue(inboundJobTokenScopeDisabledResponse);
- const inboundProjectsWithScopeResponseHandler = jest
+ const inboundGroupsAndProjectsWithScopeResponseHandler = jest
.fn()
- .mockResolvedValue(inboundProjectsWithScopeResponse);
- const inboundAddProjectSuccessResponseHandler = jest
+ .mockResolvedValue(inboundGroupsAndProjectsWithScopeResponse);
+ const inboundAddGroupOrProjectSuccessResponseHandler = jest
.fn()
- .mockResolvedValue(inboundAddProjectSuccessResponse);
+ .mockResolvedValue(inboundAddGroupOrProjectSuccessResponse);
+ const inboundRemoveGroupSuccessHandler = jest.fn().mockResolvedValue(inboundRemoveGroupSuccess);
const inboundRemoveProjectSuccessHandler = jest
.fn()
.mockResolvedValue(inboundRemoveProjectSuccess);
@@ -57,7 +61,8 @@ describe('TokenAccess component', () => {
const findAddProjectBtn = () => wrapper.findByTestId('add-project-btn');
const findCancelBtn = () => wrapper.findByRole('button', { name: 'Cancel' });
const findProjectInput = () => wrapper.findComponent(GlFormInput);
- const findRemoveProjectBtn = () => wrapper.findByRole('button', { name: 'Remove access' });
+ const findRemoveProjectBtnAt = (i) =>
+ wrapper.findAllByRole('button', { name: 'Remove access' }).at(i);
const findToggleFormBtn = () => wrapper.findByTestId('toggle-form-btn');
const findTokenDisabledAlert = () => wrapper.findComponent(GlAlert);
@@ -78,7 +83,10 @@ describe('TokenAccess component', () => {
it('shows loading state while waiting on query to resolve', async () => {
createComponent([
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
- [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ [
+ inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
+ inboundGroupsAndProjectsWithScopeResponseHandler,
+ ],
]);
expect(findLoadingIcon().exists()).toBe(true);
@@ -89,25 +97,30 @@ describe('TokenAccess component', () => {
});
});
- describe('fetching projects and scope', () => {
- it('fetches projects and scope correctly', () => {
+ describe('fetching groups and projects and scope', () => {
+ it('fetches groups and projects and scope correctly', () => {
const expectedVariables = {
fullPath: 'root/my-repo',
};
createComponent([
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
- [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ [
+ inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
+ inboundGroupsAndProjectsWithScopeResponseHandler,
+ ],
]);
expect(inboundJobTokenScopeEnabledResponseHandler).toHaveBeenCalledWith(expectedVariables);
- expect(inboundProjectsWithScopeResponseHandler).toHaveBeenCalledWith(expectedVariables);
+ expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledWith(
+ expectedVariables,
+ );
});
- it('handles fetch projects error correctly', async () => {
+ it('handles fetch groups and projects error correctly', async () => {
createComponent([
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
- [inboundGetProjectsWithCIJobTokenScopeQuery, failureHandler],
+ [inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, failureHandler],
]);
await waitForPromises();
@@ -120,7 +133,10 @@ describe('TokenAccess component', () => {
it('handles fetch scope error correctly', async () => {
createComponent([
[inboundGetCIJobTokenScopeQuery, failureHandler],
- [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ [
+ inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
+ inboundGroupsAndProjectsWithScopeResponseHandler,
+ ],
]);
await waitForPromises();
@@ -135,7 +151,10 @@ describe('TokenAccess component', () => {
it('the toggle is on and the alert is hidden', async () => {
createComponent([
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
- [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ [
+ inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
+ inboundGroupsAndProjectsWithScopeResponseHandler,
+ ],
]);
await waitForPromises();
@@ -147,7 +166,10 @@ describe('TokenAccess component', () => {
it('the toggle is off and the alert is visible', async () => {
createComponent([
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeDisabledResponseHandler],
- [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ [
+ inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
+ inboundGroupsAndProjectsWithScopeResponseHandler,
+ ],
]);
await waitForPromises();
@@ -206,13 +228,23 @@ describe('TokenAccess component', () => {
});
});
- describe('add project', () => {
- it('calls add project mutation', async () => {
+ describe.each`
+ type | testPath
+ ${'group'} | ${testGroupPath}
+ ${'project'} | ${testProjectPath}
+ `('add $type', ({ testPath }) => {
+ it(`calls add group or project mutation`, async () => {
createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
- [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
- [inboundAddProjectCIJobTokenScopeMutation, inboundAddProjectSuccessResponseHandler],
+ [
+ inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
+ inboundGroupsAndProjectsWithScopeResponseHandler,
+ ],
+ [
+ inboundAddGroupOrProjectCIJobTokenScopeMutation,
+ inboundAddGroupOrProjectSuccessResponseHandler,
+ ],
],
mountExtended,
);
@@ -220,21 +252,24 @@ describe('TokenAccess component', () => {
await waitForPromises();
await findToggleFormBtn().trigger('click');
- await findProjectInput().vm.$emit('input', testProjectPath);
+ await findProjectInput().vm.$emit('input', testPath);
findAddProjectBtn().trigger('click');
- expect(inboundAddProjectSuccessResponseHandler).toHaveBeenCalledWith({
+ expect(inboundAddGroupOrProjectSuccessResponseHandler).toHaveBeenCalledWith({
projectPath,
- targetProjectPath: testProjectPath,
+ targetPath: testPath,
});
});
- it('add project handles error correctly', async () => {
+ it('add group or project handles error correctly', async () => {
createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
- [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
- [inboundAddProjectCIJobTokenScopeMutation, failureHandler],
+ [
+ inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
+ inboundGroupsAndProjectsWithScopeResponseHandler,
+ ],
+ [inboundAddGroupOrProjectCIJobTokenScopeMutation, failureHandler],
],
mountExtended,
);
@@ -242,7 +277,7 @@ describe('TokenAccess component', () => {
await waitForPromises();
await findToggleFormBtn().trigger('click');
- await findProjectInput().vm.$emit('input', testProjectPath);
+ await findProjectInput().vm.$emit('input', testPath);
findAddProjectBtn().trigger('click');
await waitForPromises();
@@ -254,7 +289,10 @@ describe('TokenAccess component', () => {
createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
- [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ [
+ inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
+ inboundGroupsAndProjectsWithScopeResponseHandler,
+ ],
],
mountExtended,
);
@@ -265,7 +303,7 @@ describe('TokenAccess component', () => {
expect(findProjectInput().exists()).toBe(true);
- await findProjectInput().vm.$emit('input', testProjectPath);
+ await findProjectInput().vm.$emit('input', testPath);
await findCancelBtn().trigger('click');
@@ -277,40 +315,50 @@ describe('TokenAccess component', () => {
});
});
- describe('remove project', () => {
- it('calls remove project mutation', async () => {
+ describe.each`
+ type | index | mutation | handler | target
+ ${'group'} | ${0} | ${inboundRemoveGroupCIJobTokenScopeMutation} | ${inboundRemoveGroupSuccessHandler} | ${'targetGroupPath'}
+ ${'project'} | ${1} | ${inboundRemoveProjectCIJobTokenScopeMutation} | ${inboundRemoveProjectSuccessHandler} | ${'targetProjectPath'}
+ `('remove $type', ({ type, index, mutation, handler, target }) => {
+ it(`calls remove ${type} mutation`, async () => {
createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
- [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
- [inboundRemoveProjectCIJobTokenScopeMutation, inboundRemoveProjectSuccessHandler],
+ [
+ inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
+ inboundGroupsAndProjectsWithScopeResponseHandler,
+ ],
+ [mutation, handler],
],
mountExtended,
);
await waitForPromises();
- findRemoveProjectBtn().trigger('click');
+ findRemoveProjectBtnAt(index).trigger('click');
- expect(inboundRemoveProjectSuccessHandler).toHaveBeenCalledWith({
+ expect(handler).toHaveBeenCalledWith({
projectPath,
- targetProjectPath: 'root/ci-project',
+ [target]: expect.any(String),
});
});
- it('remove project handles error correctly', async () => {
+ it(`remove ${type} handles error correctly`, async () => {
createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
- [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
- [inboundRemoveProjectCIJobTokenScopeMutation, failureHandler],
+ [
+ inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
+ inboundGroupsAndProjectsWithScopeResponseHandler,
+ ],
+ [mutation, failureHandler],
],
mountExtended,
);
await waitForPromises();
- findRemoveProjectBtn().trigger('click');
+ findRemoveProjectBtnAt(index).trigger('click');
await waitForPromises();
diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js
index a4b5532108abd0369e861805914ac6d69b62ad5d..a3521f49eb7d6dd3d49737d6e35507415d1f4119 100644
--- a/spec/frontend/token_access/mock_data.js
+++ b/spec/frontend/token_access/mock_data.js
@@ -81,9 +81,30 @@ export const updateScopeSuccess = {
},
};
+export const mockGroups = [
+ {
+ id: 1,
+ name: 'some-group',
+ fullPath: 'some-group',
+ __typename: 'Group',
+ },
+ {
+ id: 2,
+ name: 'another-group',
+ fullPath: 'another-group',
+ __typename: 'Group',
+ },
+ {
+ id: 3,
+ name: 'a-sub-group',
+ fullPath: 'another-group/a-sub-group',
+ __typename: 'Group',
+ },
+];
+
export const mockProjects = [
{
- id: '1',
+ id: 1,
name: 'merge-train-stuff',
namespace: {
id: '1235',
@@ -94,7 +115,7 @@ export const mockProjects = [
__typename: 'Project',
},
{
- id: '2',
+ id: 2,
name: 'ci-project',
namespace: {
id: '1236',
@@ -108,12 +129,8 @@ export const mockProjects = [
export const mockFields = [
{
- key: 'project',
- label: 'Project with access',
- },
- {
- key: 'namespace',
- label: 'Namespace',
+ key: 'fullPath',
+ label: '',
},
{
key: 'actions',
@@ -147,7 +164,7 @@ export const inboundJobTokenScopeDisabledResponse = {
},
};
-export const inboundProjectsWithScopeResponse = {
+export const inboundGroupsAndProjectsWithScopeResponse = {
data: {
project: {
__typename: 'Project',
@@ -166,12 +183,23 @@ export const inboundProjectsWithScopeResponse = {
},
],
},
+ groupsAllowlist: {
+ __typename: 'GroupConnection',
+ nodes: [
+ {
+ __typename: 'Group',
+ fullPath: 'root/ci-group',
+ id: 'gid://gitlab/Group/45',
+ name: 'ci-group',
+ },
+ ],
+ },
},
},
},
};
-export const inboundAddProjectSuccessResponse = {
+export const inboundAddGroupOrProjectSuccessResponse = {
data: {
ciJobTokenScopeAddProject: {
errors: [],
@@ -180,6 +208,15 @@ export const inboundAddProjectSuccessResponse = {
},
};
+export const inboundRemoveGroupSuccess = {
+ data: {
+ ciJobTokenScopeRemoveProject: {
+ errors: [],
+ __typename: 'CiJobTokenScopeRemoveGroupPayload',
+ },
+ },
+};
+
export const inboundRemoveProjectSuccess = {
data: {
ciJobTokenScopeRemoveProject: {
diff --git a/spec/frontend/token_access/token_access_table_spec.js b/spec/frontend/token_access/token_access_table_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..df5c19028b30e8abb250973406910bacf2010de8
--- /dev/null
+++ b/spec/frontend/token_access/token_access_table_spec.js
@@ -0,0 +1,56 @@
+import { GlButton, GlTableLite } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import TokenAccessTable from '~/token_access/components/token_access_table.vue';
+import { mockGroups, mockProjects, mockFields } from './mock_data';
+
+describe('Token access table', () => {
+ let wrapper;
+
+ const createComponent = (props) => {
+ wrapper = mountExtended(TokenAccessTable, {
+ provide: {
+ fullPath: 'root/ci-project',
+ },
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findTable = () => wrapper.findComponent(GlTableLite);
+ const findDeleteButton = () => wrapper.findComponent(GlButton);
+ const findAllTableRows = (type) => wrapper.findAllByTestId(`${type}s-token-table-row`);
+ const findName = (type) => wrapper.findByTestId(`token-access-${type}-name`);
+
+ describe.each`
+ type | isGroup | items
+ ${'group'} | ${true} | ${mockGroups}
+ ${'project'} | ${false} | ${mockProjects}
+ `('when provided with $type', ({ type, isGroup, items }) => {
+ beforeEach(() => {
+ createComponent({
+ isGroup,
+ items,
+ tableFields: mockFields,
+ });
+ });
+
+ it('displays a table', () => {
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it('displays the correct amount of table rows', () => {
+ expect(findAllTableRows(type)).toHaveLength(items.length);
+ });
+
+ it('delete button emits event with correct item to delete', async () => {
+ await findDeleteButton().trigger('click');
+
+ expect(wrapper.emitted('removeItem')).toEqual([[items[0]]]);
+ });
+
+ it('displays fullpath', () => {
+ expect(findName(type).text()).toBe(items[0].fullPath);
+ });
+ });
+});
diff --git a/spec/frontend/token_access/token_projects_table_spec.js b/spec/frontend/token_access/token_projects_table_spec.js
deleted file mode 100644
index 7a78befa0d7031c1e6f51116a9163d3fd4e5f784..0000000000000000000000000000000000000000
--- a/spec/frontend/token_access/token_projects_table_spec.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import { GlTable, GlButton } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import TokenProjectsTable from '~/token_access/components/token_projects_table.vue';
-import { mockProjects, mockFields } from './mock_data';
-
-describe('Token projects table', () => {
- let wrapper;
-
- const defaultProps = {
- tableFields: mockFields,
- projects: mockProjects,
- };
-
- const createComponent = (props) => {
- wrapper = mountExtended(TokenProjectsTable, {
- provide: {
- fullPath: 'root/ci-project',
- },
- propsData: {
- ...defaultProps,
- ...props,
- },
- });
- };
-
- const findTable = () => wrapper.findComponent(GlTable);
- const findDeleteProjectBtn = () => wrapper.findComponent(GlButton);
- const findAllDeleteProjectBtn = () => wrapper.findAllComponents(GlButton);
- const findAllTableRows = () => wrapper.findAllByTestId('projects-token-table-row');
- const findProjectNameCell = () => wrapper.findByTestId('token-access-project-name');
-
- it('displays a table', () => {
- createComponent();
-
- expect(findTable().exists()).toBe(true);
- });
-
- it('displays the correct amount of table rows', () => {
- createComponent();
-
- expect(findAllTableRows()).toHaveLength(mockProjects.length);
- });
-
- it('delete project button emits event with correct project to delete', async () => {
- createComponent();
-
- await findDeleteProjectBtn().trigger('click');
-
- expect(wrapper.emitted('removeProject')).toEqual([[mockProjects[0].fullPath]]);
- });
-
- it('does not show the remove icon if the project is locked', () => {
- createComponent();
-
- // currently two mock projects with one being a locked project
- expect(findAllDeleteProjectBtn()).toHaveLength(1);
- });
-
- it('displays project fullpath', () => {
- createComponent();
-
- expect(findProjectNameCell().text()).toBe('root/merge-train-stuff');
- });
-});