Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-07-17 03:24:42 +00:00
parent dfd048606f
commit 7d0b125e88
41 changed files with 669 additions and 60 deletions

View File

@ -7,7 +7,7 @@ export const FORM_SELECTOR = '#js-new-access-token-form';
export const INITIAL_PAGE = 1;
export const PAGE_SIZE = 100;
export const FIELDS = [
const BASE_FIELDS = [
{
key: 'name',
label: __('Token name'),
@ -31,19 +31,35 @@ export const FIELDS = [
label: __('Last Used'),
sortable: true,
},
];
const ROLE_FIELD = {
key: 'role',
label: __('Role'),
sortable: true,
};
export const FIELDS = [
...BASE_FIELDS,
{
key: 'expiresAt',
label: __('Expires'),
sortable: true,
},
{
key: 'role',
label: __('Role'),
sortable: true,
},
ROLE_FIELD,
{
key: 'action',
label: __('Action'),
tdClass: 'gl-py-3!',
},
];
export const INACTIVE_TOKENS_TABLE_FIELDS = [
...BASE_FIELDS,
{
key: 'expiresAt',
label: __('Expired'),
sortable: true,
},
ROLE_FIELD,
];

View File

@ -0,0 +1,125 @@
<script>
import { GlIcon, GlLink, GlPagination, GlTable, GlTooltipDirective } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import UserDate from '~/vue_shared/components/user_date.vue';
import { INACTIVE_TOKENS_TABLE_FIELDS, INITIAL_PAGE, PAGE_SIZE } from './constants';
export default {
PAGE_SIZE,
name: 'InactiveAccessTokenTableApp',
components: {
GlIcon,
GlLink,
GlPagination,
GlTable,
TimeAgoTooltip,
UserDate,
},
directives: {
GlTooltip: GlTooltipDirective,
},
lastUsedHelpLink: helpPagePath('/user/profile/personal_access_tokens.md', {
anchor: 'view-the-last-time-a-token-was-used',
}),
i18n: {
emptyField: __('Never'),
expired: __('Expired'),
revoked: __('Revoked'),
},
INACTIVE_TOKENS_TABLE_FIELDS,
inject: [
'accessTokenType',
'accessTokenTypePlural',
'initialInactiveAccessTokens',
'noInactiveTokensMessage',
],
data() {
return {
inactiveAccessTokens: convertObjectPropsToCamelCase(this.initialInactiveAccessTokens, {
deep: true,
}),
currentPage: INITIAL_PAGE,
};
},
computed: {
showPagination() {
return this.inactiveAccessTokens.length > PAGE_SIZE;
},
},
methods: {
sortingChanged(aRow, bRow, key) {
if (['createdAt', 'lastUsedAt', 'expiresAt'].includes(key)) {
// Transform `null` value to the latest possible date
// https://stackoverflow.com/a/11526569/18428169
const maxEpoch = 8640000000000000;
const a = new Date(aRow[key] ?? maxEpoch).getTime();
const b = new Date(bRow[key] ?? maxEpoch).getTime();
return a - b;
}
// For other columns the default sorting works OK
return false;
},
},
};
</script>
<template>
<div>
<gl-table
data-testid="inactive-access-tokens"
:empty-text="noInactiveTokensMessage"
:fields="$options.INACTIVE_TOKENS_TABLE_FIELDS"
:items="inactiveAccessTokens"
:per-page="$options.PAGE_SIZE"
:current-page="currentPage"
:sort-compare="sortingChanged"
show-empty
stacked="sm"
class="gl-overflow-x-auto"
>
<template #cell(createdAt)="{ item: { createdAt } }">
<user-date :date="createdAt" />
</template>
<template #head(lastUsedAt)="{ label }">
<span>{{ label }}</span>
<gl-link :href="$options.lastUsedHelpLink"
><gl-icon name="question-o" class="gl-ml-2" /><span class="gl-sr-only">{{
s__('AccessTokens|The last time a token was used')
}}</span></gl-link
>
</template>
<template #cell(lastUsedAt)="{ item: { lastUsedAt } }">
<time-ago-tooltip v-if="lastUsedAt" :time="lastUsedAt" />
<template v-else> {{ $options.i18n.emptyField }}</template>
</template>
<template #cell(expiresAt)="{ item: { expiresAt, revoked } }">
<span v-if="revoked" v-gl-tooltip :title="$options.i18n.tokenValidity">{{
$options.i18n.revoked
}}</span>
<template v-else>
<span>{{ $options.i18n.expired }}</span>
<time-ago-tooltip :time="expiresAt" />
</template>
</template>
</gl-table>
<gl-pagination
v-if="showPagination"
v-model="currentPage"
:per-page="$options.PAGE_SIZE"
:total-items="inactiveAccessTokens.length"
:prev-text="__('Prev')"
:next-text="__('Next')"
:label-next-page="__('Go to next page')"
:label-prev-page="__('Go to previous page')"
align="center"
class="gl-mt-5"
/>
</div>
</template>

View File

@ -5,6 +5,7 @@ import { parseRailsFormFields } from '~/lib/utils/forms';
import { __, sprintf } from '~/locale';
import Translate from '~/vue_shared/translate';
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';
import NewAccessTokenApp from './components/new_access_token_app.vue';
import TokensApp from './components/tokens_app.vue';
@ -50,6 +51,44 @@ export const initAccessTokenTableApp = () => {
});
};
export const initInactiveAccessTokenTableApp = () => {
const el = document.querySelector('#js-inactive-access-token-table-app');
if (!el) {
return null;
}
const {
accessTokenType,
accessTokenTypePlural,
initialInactiveAccessTokens: initialInactiveAccessTokensJson,
noInactiveTokensMessage: noTokensMessage,
} = el.dataset;
// Default values
const noInactiveTokensMessage =
noTokensMessage ||
sprintf(__('This resource has no inactive %{accessTokenTypePlural}.'), {
accessTokenTypePlural,
});
const initialInactiveAccessTokens = JSON.parse(initialInactiveAccessTokensJson);
return new Vue({
el,
name: 'InactiveAccessTokenTableRoot',
provide: {
accessTokenType,
accessTokenTypePlural,
initialInactiveAccessTokens,
noInactiveTokensMessage,
},
render(h) {
return h(InactiveAccessTokenTableApp);
},
});
};
export const initExpiresAtField = () => {
const el = document.querySelector('.js-access-tokens-expires-at');

View File

@ -63,7 +63,7 @@ export default {
<gl-icon
v-if="upstreamPipeline"
:class="$options.arrowStyles"
name="long-arrow"
name="arrow-right"
data-testid="upstream-arrow-icon"
/>
<legacy-pipeline-stages
@ -75,7 +75,7 @@ export default {
<gl-icon
v-if="hasDownstreamPipelines"
:class="$options.arrowStyles"
name="long-arrow"
name="arrow-right"
data-testid="downstream-arrow-icon"
/>
<legacy-linked-pipelines-mini-list

View File

@ -122,7 +122,7 @@ export default {
<gl-icon
v-if="upstreamPipeline"
:class="$options.arrowStyles"
name="long-arrow"
name="arrow-right"
data-testid="upstream-arrow-icon"
/>
<pipeline-stages
@ -134,7 +134,7 @@ export default {
<gl-icon
v-if="hasDownstreamPipelines"
:class="$options.arrowStyles"
name="long-arrow"
name="arrow-right"
data-testid="downstream-arrow-icon"
/>
<downstream-pipelines

View File

@ -78,7 +78,7 @@ export default {
<transition name="issuable-header-slide">
<div
v-if="show"
class="issue-sticky-header gl-fixed gl-z-3 gl-bg-white gl-border-1 gl-border-b-solid gl-border-b-gray-100 gl-py-3"
class="issue-sticky-header gl-fixed gl-z-3 gl-bg-default gl-border-b gl-py-3"
data-testid="issue-sticky-header"
>
<div class="issue-sticky-header-text gl-flex gl-items-center gl-gap-2 gl-mx-auto">

View File

@ -160,7 +160,7 @@ export default {
@disappear="setStickyHeaderVisible(true)"
>
<div
class="issue-sticky-header merge-request-sticky-header gl-fixed gl-bg-white gl-hidden md:gl-flex gl-flex-col gl-justify-end gl-border-b"
class="issue-sticky-header merge-request-sticky-header gl-fixed gl-bg-default gl-hidden md:gl-flex gl-flex-col gl-justify-end gl-border-b"
:class="{ 'gl-invisible': !isStickyHeaderVisible }"
>
<div

View File

@ -1,5 +1,6 @@
import {
initAccessTokenTableApp,
initInactiveAccessTokenTableApp,
initExpiresAtField,
initNewAccessTokenApp,
} from '~/access_tokens';
@ -7,3 +8,7 @@ import {
initAccessTokenTableApp();
initExpiresAtField();
initNewAccessTokenApp();
if (gon.features.retainResourceAccessTokenUserAfterRevoke) {
initInactiveAccessTokenTableApp();
}

View File

@ -1,5 +1,6 @@
import {
initAccessTokenTableApp,
initInactiveAccessTokenTableApp,
initExpiresAtField,
initNewAccessTokenApp,
} from '~/access_tokens';
@ -7,3 +8,7 @@ import {
initAccessTokenTableApp();
initExpiresAtField();
initNewAccessTokenApp();
if (gon.features.retainResourceAccessTokenUserAfterRevoke) {
initInactiveAccessTokenTableApp();
}

View File

@ -87,7 +87,7 @@ export default {
<transition name="issuable-header-slide">
<div
v-if="stickyTitleVisible"
class="issue-sticky-header gl-fixed gl-z-3 gl-bg-white gl-border-1 gl-border-b-solid gl-border-b-gray-100 gl-py-3"
class="issue-sticky-header gl-fixed gl-z-3 gl-bg-default gl-border-b gl-py-3"
data-testid="header"
>
<div class="issue-sticky-header-text gl-flex gl-items-baseline gl-mx-auto gl-gap-3">

View File

@ -98,7 +98,7 @@ export default {
<transition name="issuable-header-slide">
<div
v-if="isStickyHeaderShowing"
class="issue-sticky-header gl-fixed gl-bg-white gl-border-b gl-z-3 gl-py-2"
class="issue-sticky-header gl-fixed gl-bg-default gl-border-b gl-z-3 gl-py-2"
data-testid="work-item-sticky-header"
>
<div

View File

@ -56,7 +56,7 @@ $diff-file-header: 41px;
}
&:hover {
background-color: $gray-50;
background-color: var(--gl-background-color-strong);
}
svg {

View File

@ -242,7 +242,7 @@ span.idiff {
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
background-color: $gray-10;
background-color: var(--gl-background-color-subtle);
border-bottom: 1px solid $border-color;
padding: $gl-padding-8 $gl-padding;
margin: 0;

View File

@ -997,8 +997,8 @@
height: var(--mr-review-bar-height);
padding-left: $super-sidebar-width;
padding-right: $right-sidebar-collapsed-width;
background: var(--white, $white);
border-top: 1px solid var(--border-color, $border-color);
background: var(--gl-background-color-default);
border-top: 1px solid var(--gl-border-color-default);
@apply gl-transition-padding;
@media (max-width: map-get($grid-breakpoints, sm)-1) {

View File

@ -7,6 +7,9 @@ module AccessTokensActions
before_action -> { check_permission(:read_resource_access_tokens) }, only: [:index]
before_action -> { check_permission(:destroy_resource_access_tokens) }, only: [:revoke]
before_action -> { check_permission(:create_resource_access_tokens) }, only: [:create]
before_action do
push_frontend_feature_flag(:retain_resource_access_token_user_after_revoke, resource.root_ancestor)
end
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
@ -70,6 +73,9 @@ module AccessTokensActions
@scopes = Gitlab::Auth.available_scopes_for(resource)
@active_access_tokens = active_access_tokens
if Feature.enabled?(:retain_resource_access_token_user_after_revoke, resource.root_ancestor) # rubocop:disable Style/GuardClause
@inactive_access_tokens = inactive_access_tokens
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables

View File

@ -14,6 +14,15 @@ module RenderAccessTokens
represent(tokens)
end
def inactive_access_tokens
tokens = finder(state: 'inactive', sort: 'updated_at_desc').execute.preload_users
# We don't call `add_pagination_headers` as this overrides the
# pagination of active tokens.
represent(tokens)
end
def add_pagination_headers(relation)
Gitlab::Pagination::OffsetHeaderBuilder.new(
request_context: self,

View File

@ -3,6 +3,7 @@
- type = _('group access token')
- type_plural = _('group access tokens')
- @force_desktop_expanded_sidebar = true
- shared_card_component_classes = "gl-new-card gl-border-b-0 gl-rounded-bottom-left-none gl-rounded-bottom-right-none"
.settings-section.js-search-settings-section
.settings-sticky-header
@ -25,7 +26,7 @@
#js-new-access-token-app{ data: { access_token_type: type } }
= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-border-b-0 gl-rounded-bottom-left-none gl-rounded-bottom-right-none js-toggle-container js-token-card' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body' }) do |c|
= render Pajamas::CardComponent.new(card_options: { class: "#{shared_card_component_classes} js-toggle-container js-token-card" }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body' }) do |c|
- c.with_header do
.gl-new-card-title-wrapper
%h3.gl-new-card-title
@ -54,3 +55,15 @@
help_path: help_page_path('user/group/settings/group_access_tokens', anchor: 'scopes-for-a-group-access-token')
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json, no_active_tokens_message: _('This group has no active access tokens.'), show_role: true } }
- if Feature.enabled?(:retain_resource_access_token_user_after_revoke, @group.root_ancestor)
= render Pajamas::CardComponent.new(card_options: { class: shared_card_component_classes }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body gl-px-0 gl-bg-gray-10 gl-border-b gl-rounded-bottom-base' }) do |c|
- c.with_header do
.gl-new-card-title-wrapper
%h3.gl-new-card-title
= _('Inactive group access tokens')
.gl-new-card-count
= sprite_icon('token', css_class: 'gl-mr-2')
%span.js-token-count= @inactive_access_tokens.size
- c.with_body do
#js-inactive-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_inactive_access_tokens: @inactive_access_tokens.to_json, no_inactive_tokens_message: _('This group has no inactive access tokens.')} }

View File

@ -3,6 +3,7 @@
- type = _('project access token')
- type_plural = _('project access tokens')
- @force_desktop_expanded_sidebar = true
- shared_card_component_classes = "gl-new-card gl-border-b-0 gl-rounded-bottom-left-none gl-rounded-bottom-right-none"
.settings-section.js-search-settings-section
.settings-sticky-header
@ -24,7 +25,7 @@
#js-new-access-token-app{ data: { access_token_type: type } }
= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-border-b-0 gl-rounded-bottom-left-none gl-rounded-bottom-right-none js-toggle-container js-token-card' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body' }) do |c|
= render Pajamas::CardComponent.new(card_options: { class: "#{shared_card_component_classes} js-toggle-container js-token-card" }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body' }) do |c|
- c.with_header do
.gl-new-card-title-wrapper
%h3.gl-new-card-title
@ -42,3 +43,15 @@
= render_if_exists 'projects/settings/access_tokens/form', type: type
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json, no_active_tokens_message: _('This project has no active access tokens.'), show_role: true } }
- if Feature.enabled?(:retain_resource_access_token_user_after_revoke, @project.root_ancestor)
= render Pajamas::CardComponent.new(card_options: { class: shared_card_component_classes }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body gl-px-0 gl-bg-gray-10 gl-border-b gl-rounded-bottom-base' }) do |c|
- c.with_header do
.gl-new-card-title-wrapper
%h3.gl-new-card-title
= _('Inactive project access tokens')
.gl-new-card-count
= sprite_icon('token', css_class: 'gl-mr-2')
%span.js-token-count= @inactive_access_tokens.size
- c.with_body do
#js-inactive-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_inactive_access_tokens: @inactive_access_tokens.to_json, no_inactive_tokens_message: _('This project has no inactive access tokens.')} }

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
class AddMemberRoleIdToGroupGroupLinks < Gitlab::Database::Migration[2.2]
milestone '17.3'
enable_lock_retries!
def change
add_column :group_group_links, :member_role_id, :bigint, if_not_exists: true
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddIndexToGroupGroupLinksOnMemberRoleId < Gitlab::Database::Migration[2.2]
milestone '17.3'
disable_ddl_transaction!
INDEX_NAME = 'index_group_group_links_on_member_role_id'
def up
add_concurrent_index :group_group_links, :member_role_id, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :group_group_links, INDEX_NAME
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddFkToMemberRoleOnGroupGroupLinks < Gitlab::Database::Migration[2.2]
milestone '17.3'
disable_ddl_transaction!
def up
add_concurrent_foreign_key :group_group_links, :member_roles, column: :member_role_id, on_delete: :nullify
end
def down
with_lock_retries do
remove_foreign_key :group_group_links, column: :member_role_id
end
end
end

View File

@ -0,0 +1 @@
ee3dc82029ae3d9f7ce57e49bba39841872391ae74f7671e5794849a0c4adeb5

View File

@ -0,0 +1 @@
6469c09bf160ff6c05e242b63882add76a467642df48424a2a7f3a1baefacb01

View File

@ -0,0 +1 @@
ba845f9c8129c6006e7f5c7406befd09c2e4520ed334be658a6bb8334ede42a0

View File

@ -10982,7 +10982,8 @@ CREATE TABLE group_group_links (
shared_group_id bigint NOT NULL,
shared_with_group_id bigint NOT NULL,
expires_at date,
group_access smallint DEFAULT 30 NOT NULL
group_access smallint DEFAULT 30 NOT NULL,
member_role_id bigint
);
CREATE SEQUENCE group_group_links_id_seq
@ -27440,6 +27441,8 @@ CREATE INDEX index_group_deploy_tokens_on_deploy_token_id ON group_deploy_tokens
CREATE UNIQUE INDEX index_group_deploy_tokens_on_group_and_deploy_token_ids ON group_deploy_tokens USING btree (group_id, deploy_token_id);
CREATE INDEX index_group_group_links_on_member_role_id ON group_group_links USING btree (member_role_id);
CREATE UNIQUE INDEX index_group_group_links_on_shared_group_and_shared_with_group ON group_group_links USING btree (shared_group_id, shared_with_group_id);
CREATE INDEX index_group_group_links_on_shared_with_group_and_group_access ON group_group_links USING btree (shared_with_group_id, group_access);
@ -32210,6 +32213,9 @@ ALTER TABLE ONLY duo_workflows_workflows
ALTER TABLE ONLY members
ADD CONSTRAINT fk_2f85abf8f1 FOREIGN KEY (member_namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY group_group_links
ADD CONSTRAINT fk_2fbc7071a3 FOREIGN KEY (member_role_id) REFERENCES member_roles(id) ON DELETE SET NULL;
ALTER TABLE ONLY zoekt_replicas
ADD CONSTRAINT fk_3035f4b498 FOREIGN KEY (zoekt_enabled_namespace_id) REFERENCES zoekt_enabled_namespaces(id) ON DELETE CASCADE;

View File

@ -254,11 +254,18 @@ malicious-job:
```
To help reduce the risk of accidentally leaking secrets through scripts like in `accidental-leak-job`,
all variables containing sensitive information should be [masked in job logs](#mask-a-cicd-variable).
all variables containing sensitive information should always be [masked in job logs](#mask-a-cicd-variable).
You can also [limit a variable to protected branches and tags only](#protect-a-cicd-variable).
Alternatively, use the GitLab [integration with HashiCorp Vault](../secrets/index.md)
to store and retrieve secrets.
Alternatively, use one of the native GitLab integrations to connect with third party
secrets manager providers to store and retrieve secrets:
- [HashiCorp Vault](../secrets/index.md)
- [Azure Key Vault](../secrets/azure_key_vault.md)
- [Google Secret Manager](../secrets/gcp_secret_manager.md)
You can also use [OpenID Connect (OIDC) authentication](../secrets/id_token_authentication.md)
for secrets managers which do not have a native integration.
Malicious scripts like in `malicious-job` must be caught during the review process.
Reviewers should never trigger a pipeline when they find code like this, because
@ -272,8 +279,7 @@ valid [secrets file](../../administration/backup_restore/troubleshooting_backup_
WARNING:
Masking a CI/CD variable is not a guaranteed way to prevent malicious users from
accessing variable values. The masking feature is "best-effort" and there to
help when a variable is accidentally revealed. To make variables more secure,
accessing variable values. To ensure security of sensitive information,
consider using [external secrets](../secrets/index.md) and [file type variables](#use-file-type-cicd-variables)
to prevent commands such as `env`/`printenv` from printing secret variables.
@ -295,9 +301,10 @@ To mask a variable:
The method used to mask variables [limits what can be included in a masked variable](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/13784#note_106756757).
The value of the variable must:
- Be a single line.
- Be a single line with no spaces.
- Be 8 characters or longer.
- Not match the name of an existing predefined or custom CI/CD variable.
- Not include non-alpha-numeric characters other than `@`, `_`, `-`, `:`, or `+`.
Additionally, if [variable expansion](#prevent-cicd-variable-expansion) is enabled,
the value can contain only:
@ -514,7 +521,7 @@ test-job:
Variables from `dotenv` reports [take precedence](#cicd-variable-precedence) over
certain types of new variable definitions such as job defined variables.
You can also [pass `dotenv` variables to downstream pipelines](../pipelines/downstream_pipelines.md#pass-dotenv-variables-created-in-a-job)
You can also [pass `dotenv` variables to downstream pipelines](../pipelines/downstream_pipelines.md#pass-dotenv-variables-created-in-a-job).
#### Control which jobs receive `dotenv` variables
@ -914,7 +921,7 @@ export CI_PROJECT_TITLE="GitLab"
WARNING:
Debug logging can be a serious security risk. The output contains the content of
all variables and other secrets available to the job. The output is uploaded to the
all variables available to the job. The output is uploaded to the
GitLab server and visible in job logs.
You can use debug logging to help troubleshoot problems with pipeline configuration
@ -1022,22 +1029,3 @@ WARNING:
If you add `CI_DEBUG_TRACE` as a local variable to runners, debug logs generate and are visible
to all users with access to job logs. The permission levels are not checked by the runner,
so you should only use the variable in GitLab itself.
## Known issues and workarounds
These are some known issues with CI/CD variables, and where applicable, known workarounds.
### "argument list too long"
This issue occurs when the combined length of all CI/CD variables defined for a job exceeds the limit imposed by the
shell where the job executes. This includes the names and values of pre-defined and user defined variables. This limit
is typically referred to as `ARG_MAX`, and is shell and operating system dependent. This issue also occurs when the
content of a single [File-type](#use-file-type-cicd-variables) variable exceeds `ARG_MAX`.
For more information, see [issue 392406](https://gitlab.com/gitlab-org/gitlab/-/issues/392406#note_1414219596).
As a workaround you can either:
- Use [File-type](#use-file-type-cicd-variables) CI/CD variables for large environment variables where possible.
- If a single large variable is larger than `ARG_MAX`, try using [Secure Files](../secure_files/index.md), or
bring the file to the job through some other mechanism.

View File

@ -141,6 +141,11 @@ The token generated when you create an agent for Kubernetes. Use **agent access
- secret token
- authentication token
## agnostic
Instead of **agnostic**, use **platform-independent** or **vendor-neutral**.
([Vale](../testing/vale.md) rule: [`SubstitutionWarning.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/SubstitutionWarning.yml))
## AI, artificial intelligence
Use **AI**. Do not spell out **artificial intelligence**.

View File

@ -153,7 +153,8 @@ module Gitlab
target_project_id: target_project_id,
remove_source_branch: true,
assignee_ids: usernames_to_ids(change.assignees),
reviewer_ids: usernames_to_ids(change.reviewers)
reviewer_ids: usernames_to_ids(change.reviewers),
squash: true
})
end

View File

@ -124,7 +124,8 @@ module Gitlab
end
def print_change_details(change, branch_name)
base_message = "Merge request URL: #{change.mr_web_url || '(known after create)'}, on branch #{branch_name}."
base_message = "Merge request URL: #{change.mr_web_url || '(known after create)'}, on branch #{branch_name}. " \
"Squash commits enabled."
base_message << " CI skipped." if change.push_options.ci_skip
@logger.puts base_message.yellowish

View File

@ -343,7 +343,8 @@ RSpec.describe ::Gitlab::Housekeeper::GitlabClient do
target_project_id: 456,
remove_source_branch: true,
assignee_ids: [assignee_id],
reviewer_ids: [reviewer_id]
reviewer_ids: [reviewer_id],
squash: true
},
headers: {
'Content-Type' => 'application/json',

View File

@ -43,12 +43,16 @@ module Gitlab
module MigratorOverrides
def current_version
migrations
.sort_by(&:version)
.reverse
reverse_sorted_migrations
.find { |m| migrated.include?(m.version) }
.try(:version) || 0
end
private
def reverse_sorted_migrations
@reverse_sorted_migrations ||= migrations.sort_by(&:version).reverse
end
end
def self.patch!

View File

@ -27427,6 +27427,12 @@ msgstr ""
msgid "Inactive"
msgstr ""
msgid "Inactive group access tokens"
msgstr ""
msgid "Inactive project access tokens"
msgstr ""
msgid "Incident"
msgstr ""
@ -54310,6 +54316,9 @@ msgstr ""
msgid "This group has no active access tokens."
msgstr ""
msgid "This group has no inactive access tokens."
msgstr ""
msgid "This group has no projects yet"
msgstr ""
@ -54603,6 +54612,9 @@ msgstr ""
msgid "This project has no active access tokens."
msgstr ""
msgid "This project has no inactive access tokens."
msgstr ""
msgid "This project is %{strongStart}NOT%{strongEnd} a fork, and has the following:"
msgstr ""
@ -54675,6 +54687,9 @@ msgstr ""
msgid "This resource has no comments to summarize"
msgstr ""
msgid "This resource has no inactive %{accessTokenTypePlural}."
msgstr ""
msgid "This runner will only run on pipelines triggered on protected branches"
msgstr ""

View File

@ -3,7 +3,7 @@
source 'https://rubygems.org'
gem 'gitlab-qa', '~> 14', '>= 14.12.0', require: 'gitlab/qa'
gem 'gitlab_quality-test_tooling', '~> 1.30.0', require: false
gem 'gitlab_quality-test_tooling', '~> 1.31.0', require: false
gem 'gitlab-utils', path: '../gems/gitlab-utils'
gem 'activesupport', '~> 7.0.8.4' # This should stay in sync with the root's Gemfile
gem 'allure-rspec', '~> 2.24.5'

View File

@ -148,7 +148,7 @@ GEM
rainbow (>= 3, < 4)
table_print (= 1.5.7)
zeitwerk (>= 2, < 3)
gitlab_quality-test_tooling (1.30.0)
gitlab_quality-test_tooling (1.31.0)
activesupport (>= 7.0, < 7.2)
amatch (~> 0.4.1)
gitlab (~> 4.19)
@ -405,7 +405,7 @@ DEPENDENCIES
gitlab-cng!
gitlab-qa (~> 14, >= 14.12.0)
gitlab-utils!
gitlab_quality-test_tooling (~> 1.30.0)
gitlab_quality-test_tooling (~> 1.31.0)
googleauth (~> 1.9.0)
influxdb-client (~> 3.1)
junit_merge (~> 0.1.2)

View File

@ -0,0 +1,180 @@
import { GlPagination, GlTable } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import InactiveAccessTokenTableApp from '~/access_tokens/components/inactive_access_token_table_app.vue';
import { PAGE_SIZE } from '~/access_tokens/components/constants';
import { __, s__, sprintf } from '~/locale';
describe('~/access_tokens/components/inactive_access_token_table_app', () => {
let wrapper;
const accessTokenType = 'access token';
const accessTokenTypePlural = 'access tokens';
const information = undefined;
const noInactiveTokensMessage = 'This resource has no inactive access tokens.';
const defaultInactiveAccessTokens = [
{
name: 'a',
scopes: ['api'],
created_at: '2023-05-01T00:00:00.000Z',
last_used_at: null,
expired: true,
expires_at: '2024-05-01T00:00:00.000Z',
revoked: true,
role: 'Maintainer',
},
{
name: 'b',
scopes: ['api', 'sudo'],
created_at: '2024-04-21T00:00:00.000Z',
last_used_at: '2024-04-21T00:00:00.000Z',
expired: true,
expires_at: new Date().toISOString(),
revoked: false,
role: 'Maintainer',
},
];
const createComponent = (props = {}) => {
wrapper = mountExtended(InactiveAccessTokenTableApp, {
provide: {
accessTokenType,
accessTokenTypePlural,
information,
initialInactiveAccessTokens: defaultInactiveAccessTokens,
noInactiveTokensMessage,
...props,
},
});
};
const findTable = () => wrapper.findComponent(GlTable);
const findHeaders = () => findTable().findAll('th > div > span');
const findCells = () => findTable().findAll('td');
const findPagination = () => wrapper.findComponent(GlPagination);
it('should render an empty table with a default message', () => {
createComponent({ initialInactiveAccessTokens: [] });
const cells = findCells();
expect(cells).toHaveLength(1);
expect(cells.at(0).text()).toBe(
sprintf(__('This resource has no inactive %{accessTokenTypePlural}.'), {
accessTokenTypePlural,
}),
);
});
it('should render an empty table with a custom message', () => {
const noTokensMessage = 'This group has no inactive access tokens.';
createComponent({ initialInactiveAccessTokens: [], noInactiveTokensMessage: noTokensMessage });
const cells = findCells();
expect(cells).toHaveLength(1);
expect(cells.at(0).text()).toBe(noTokensMessage);
});
describe('table headers', () => {
it('has expected columns', () => {
createComponent();
const headers = findHeaders();
expect(headers.wrappers.map((header) => header.text())).toStrictEqual([
__('Token name'),
__('Scopes'),
s__('AccessTokens|Created'),
'Last Used',
__('Expired'),
__('Role'),
]);
});
});
it('`Last Used` header should contain a link and an assistive message', () => {
createComponent();
const headers = wrapper.findAll('th');
const lastUsed = headers.at(3);
const anchor = lastUsed.find('a');
const assistiveElement = lastUsed.find('.gl-sr-only');
expect(anchor.exists()).toBe(true);
expect(anchor.attributes('href')).toBe(
'/help/user/profile/personal_access_tokens.md#view-the-last-time-a-token-was-used',
);
expect(assistiveElement.text()).toBe(s__('AccessTokens|The last time a token was used'));
});
it('sorts rows alphabetically', async () => {
createComponent();
const cells = findCells();
// First and second rows
expect(cells.at(0).text()).toBe('a');
expect(cells.at(6).text()).toBe('b');
const headers = findHeaders();
await headers.at(0).trigger('click');
await headers.at(0).trigger('click');
// First and second rows have swapped
expect(cells.at(0).text()).toBe('b');
expect(cells.at(6).text()).toBe('a');
});
it('sorts rows by last used date', async () => {
createComponent();
const cells = findCells();
// First and second rows
expect(cells.at(0).text()).toBe('a');
expect(cells.at(6).text()).toBe('b');
const headers = findHeaders();
await headers.at(3).trigger('click');
// First and second rows have swapped
expect(cells.at(0).text()).toBe('b');
expect(cells.at(6).text()).toBe('a');
});
it('sorts rows by expiry date', async () => {
createComponent();
const cells = findCells();
const headers = findHeaders();
await headers.at(4).trigger('click');
// First and second rows have swapped
expect(cells.at(0).text()).toBe('b');
expect(cells.at(6).text()).toBe('a');
});
it('shows Revoked in expiry column when revoked', () => {
createComponent();
const cells = findCells();
// First and second rows
expect(cells.at(4).text()).toBe('Revoked');
expect(cells.at(10).text()).toBe('Expired just now');
});
describe('pagination', () => {
it('does not show pagination component', () => {
createComponent({
initialInactiveAccessTokens: Array(PAGE_SIZE).fill(defaultInactiveAccessTokens[0]),
});
expect(findPagination().exists()).toBe(false);
});
it('shows the pagination component', () => {
createComponent({
initialInactiveAccessTokens: Array(PAGE_SIZE + 1).fill(defaultInactiveAccessTokens[0]),
});
expect(findPagination().exists()).toBe(true);
});
});
});

View File

@ -5,9 +5,11 @@ import {
initAccessTokenTableApp,
initExpiresAtField,
initNewAccessTokenApp,
initInactiveAccessTokenTableApp,
initTokensApp,
} from '~/access_tokens';
import AccessTokenTableApp from '~/access_tokens/components/access_token_table_app.vue';
import InactiveAccessTokenTableApp from '~/access_tokens/components/inactive_access_token_table_app.vue';
import ExpiresAtField from '~/access_tokens/components/expires_at_field.vue';
import NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue';
import TokensApp from '~/access_tokens/components/tokens_app.vue';
@ -173,4 +175,81 @@ describe('access tokens', () => {
expect(initNewAccessTokenApp()).toBe(null);
});
});
describe('initInactiveAccessTokenTableApp', () => {
const accessTokenType = 'group access token';
const accessTokenTypePlural = 'group access tokens';
const initialInactiveAccessTokens = [
{
name: 'a',
scopes: ['api'],
created_at: '2023-05-01T00:00:00.000Z',
last_used_at: null,
expired: false,
expires_at: null,
revoked: true,
role: 'Maintainer',
},
];
it('mounts the component and provides required values', () => {
setHTMLFixture(
`<div id="js-inactive-access-token-table-app"
data-access-token-type="${accessTokenType}"
data-access-token-type-plural="${accessTokenTypePlural}"
data-initial-inactive-access-tokens=${JSON.stringify(initialInactiveAccessTokens)}
>
</div>`,
);
const vueInstance = initInactiveAccessTokenTableApp();
wrapper = createWrapper(vueInstance);
const component = wrapper.findComponent({ name: 'InactiveAccessTokenTableRoot' });
expect(component.exists()).toBe(true);
expect(wrapper.findComponent(InactiveAccessTokenTableApp).vm).toMatchObject({
// Required value
accessTokenType,
accessTokenTypePlural,
initialInactiveAccessTokens,
// Default values
noInactiveTokensMessage: sprintf(
__('This resource has no inactive %{accessTokenTypePlural}.'),
{
accessTokenTypePlural,
},
),
});
});
it('mounts the component and provides all values', () => {
const noInactiveTokensMessage = 'This group has no inactive access tokens.';
setHTMLFixture(
`<div id="js-inactive-access-token-table-app"
data-access-token-type="${accessTokenType}"
data-access-token-type-plural="${accessTokenTypePlural}"
data-initial-inactive-access-tokens=${JSON.stringify(initialInactiveAccessTokens)}
data-no-inactive-tokens-message="${noInactiveTokensMessage}"
>
</div>`,
);
const vueInstance = initInactiveAccessTokenTableApp();
wrapper = createWrapper(vueInstance);
const component = wrapper.findComponent({ name: 'InactiveAccessTokenTableRoot' });
expect(component.exists()).toBe(true);
expect(component.findComponent(InactiveAccessTokenTableApp).vm).toMatchObject({
accessTokenType,
accessTokenTypePlural,
initialInactiveAccessTokens,
noInactiveTokensMessage,
});
});
it('returns `null`', () => {
expect(initInactiveAccessTokenTableApp()).toBe(null);
});
});
});

View File

@ -85,7 +85,7 @@ describe('Legacy Pipeline Mini Graph', () => {
it('should render an upstream arrow icon only', () => {
expect(findDownstreamArrowIcon().exists()).toBe(false);
expect(findUpstreamArrowIcon().exists()).toBe(true);
expect(findUpstreamArrowIcon().props('name')).toBe('long-arrow');
expect(findUpstreamArrowIcon().props('name')).toBe('arrow-right');
});
});
@ -116,7 +116,7 @@ describe('Legacy Pipeline Mini Graph', () => {
it('should render a downstream arrow icon only', () => {
expect(findUpstreamArrowIcon().exists()).toBe(false);
expect(findDownstreamArrowIcon().exists()).toBe(true);
expect(findDownstreamArrowIcon().props('name')).toBe('long-arrow');
expect(findDownstreamArrowIcon().props('name')).toBe('arrow-right');
});
});
});

View File

@ -40,6 +40,7 @@ RSpec.describe Groups::Settings::AccessTokensController, feature_category: :syst
it_behaves_like 'feature unavailable'
it_behaves_like 'GET resource access tokens available'
it_behaves_like 'GET access tokens are paginated and ordered'
it_behaves_like 'GET access tokens includes inactive tokens'
end
describe 'POST /:namespace/-/settings/access_tokens' do

View File

@ -41,6 +41,7 @@ RSpec.describe Projects::Settings::AccessTokensController, feature_category: :sy
it_behaves_like 'feature unavailable'
it_behaves_like 'GET resource access tokens available'
it_behaves_like 'GET access tokens are paginated and ordered'
it_behaves_like 'GET access tokens includes inactive tokens'
end
describe 'POST /:namespace/:project/-/settings/access_tokens' do

View File

@ -129,6 +129,21 @@ RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_tex
find("[data-testid='active-tokens']")
end
def inactive_access_tokens
find("[data-testid='inactive-access-tokens']")
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(retain_resource_access_token_user_after_revoke: false)
end
it 'does not show inactive tokens' do
visit resource_settings_access_tokens_path
expect(page).to have_no_selector("[data-testid='inactive-access-tokens']")
end
end
it 'allows revocation of an active token' do
visit resource_settings_access_tokens_path
accept_gl_confirm(button_text: 'Revoke') { click_on 'Revoke' }
@ -141,6 +156,15 @@ RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_tex
visit resource_settings_access_tokens_path
expect(active_access_tokens).to have_text(no_active_tokens_text)
expect(inactive_access_tokens).to have_text(resource_access_token.name)
end
it 'removes revoked tokens from active section' do
resource_access_token.revoke!
visit resource_settings_access_tokens_path
expect(active_access_tokens).to have_text(no_active_tokens_text)
expect(inactive_access_tokens).to have_text(resource_access_token.name)
end
context 'when resource access token creation is not allowed' do

View File

@ -58,7 +58,7 @@ RSpec.shared_examples 'GET access tokens are paginated and ordered' do
end
end
context "when tokens returned are ordered" do
context "when active tokens returned are ordered" do
let(:expires_1_day_from_now) { 1.day.from_now.to_date }
let(:expires_2_day_from_now) { 2.days.from_now.to_date }
@ -95,6 +95,33 @@ RSpec.shared_examples 'GET access tokens are paginated and ordered' do
end
end
RSpec.shared_examples 'GET access tokens includes inactive tokens' do
context "when inactive tokens returned are ordered" do
let(:one_day_ago) { 1.day.ago.to_date }
let(:two_days_ago) { 2.days.ago.to_date }
before do
create(:personal_access_token, :revoked, user: access_token_user, name: "Token1").update!(updated_at: one_day_ago)
create(:personal_access_token, :expired, user: access_token_user,
name: "Token2").update!(updated_at: two_days_ago)
end
it "orders token list descending on updated_at" do
get_access_tokens
first_token = assigns(:inactive_access_tokens).first.as_json
expect(first_token['name']).to eq("Token1")
end
end
context "when there are no inactive tokens" do
it "returns an empty array" do
get_access_tokens
expect(assigns(:inactive_access_tokens)).to eq([])
end
end
end
RSpec.shared_examples 'POST resource access tokens available' do
def created_token
PersonalAccessToken.order(:created_at).last