Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_inherit_date.vue4
-rw-r--r--app/graphql/resolvers/ci/runner_status_resolver.rb24
-rw-r--r--app/graphql/types/ci/runner_status_enum.rb43
-rw-r--r--app/graphql/types/ci/runner_type.rb9
-rw-r--r--app/helpers/profiles_helper.rb5
-rw-r--r--app/models/ci/runner.rb9
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml1
-rw-r--r--app/views/profiles/keys/_form.html.haml5
-rw-r--r--db/migrate/20211118114228_add_max_ssh_key_lifetime_to_application_settings.rb7
-rw-r--r--db/schema_migrations/202111181142281
-rw-r--r--db/structure.sql1
-rw-r--r--doc/api/graphql/reference/index.md28
-rw-r--r--doc/api/project_vulnerabilities.md6
-rw-r--r--doc/api/settings.md1
-rw-r--r--doc/user/admin_area/settings/account_and_limit_settings.md66
-rw-r--r--locale/gitlab.pot21
-rw-r--r--spec/frontend/google_cloud/components/app_spec.js111
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js9
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js12
-rw-r--r--spec/graphql/resolvers/ci/runner_status_resolver_spec.rb40
-rw-r--r--spec/models/ci/runner_spec.rb60
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb41
-rw-r--r--spec/views/profiles/keys/_form.html.haml_spec.rb4
24 files changed, 368 insertions, 147 deletions
diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
index d06cb4f23fb..c1a11c35bab 100644
--- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
@@ -124,6 +124,9 @@ export default {
isLoading() {
return this.$apollo.queries.issuable.loading || this.loading;
},
+ initialLoading() {
+ return this.$apollo.queries.issuable.loading;
+ },
hasDate() {
return this.dateValue !== null;
},
@@ -271,10 +274,10 @@ export default {
<span class="collapse-truncated-title">{{ formattedDate }}</span>
</div>
<sidebar-inherit-date
- v-if="canInherit"
+ v-if="canInherit && !initialLoading"
:issuable="issuable"
- :is-loading="isLoading"
:date-type="dateType"
+ :is-loading="isLoading"
@reset-date="setDate(null)"
@set-date="setFixedDate"
/>
diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_inherit_date.vue b/app/assets/javascripts/sidebar/components/date/sidebar_inherit_date.vue
index b6bfacb2e47..77f8e125dce 100644
--- a/app/assets/javascripts/sidebar/components/date/sidebar_inherit_date.vue
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_inherit_date.vue
@@ -17,8 +17,9 @@ export default {
type: Object,
},
isLoading: {
- required: true,
+ required: false,
type: Boolean,
+ default: false,
},
dateType: {
type: String,
@@ -31,6 +32,7 @@ export default {
return this.issuable?.[dateFields[this.dateType].isDateFixed] || false;
},
set(fixed) {
+ if (fixed === this.issuable[dateFields[this.dateType].isDateFixed]) return;
this.$emit('set-date', fixed);
},
},
diff --git a/app/graphql/resolvers/ci/runner_status_resolver.rb b/app/graphql/resolvers/ci/runner_status_resolver.rb
new file mode 100644
index 00000000000..d916a8a13f0
--- /dev/null
+++ b/app/graphql/resolvers/ci/runner_status_resolver.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ # NOTE: This class was introduced to allow modifying the meaning of certain values in RunnerStatusEnum
+ # while preserving backward compatibility. It can be removed in 15.0 once the API has stabilized.
+ class RunnerStatusResolver < BaseResolver
+ type Types::Ci::RunnerStatusEnum, null: false
+
+ alias_method :runner, :object
+
+ argument :legacy_mode,
+ type: GraphQL::Types::String,
+ default_value: '14.5',
+ required: false,
+ description: 'Compatibility mode. A null value turns off compatibility mode.',
+ deprecated: { reason: 'Will be removed in 15.0. From that release onward, the field will behave as if legacyMode is null', milestone: '14.6' }
+
+ def resolve(legacy_mode:, **args)
+ runner.status(legacy_mode)
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/runner_status_enum.rb b/app/graphql/types/ci/runner_status_enum.rb
index 8501ce20204..14eae1cdce5 100644
--- a/app/graphql/types/ci/runner_status_enum.rb
+++ b/app/graphql/types/ci/runner_status_enum.rb
@@ -5,24 +5,33 @@ module Types
class RunnerStatusEnum < BaseEnum
graphql_name 'CiRunnerStatus'
- ::Ci::Runner::AVAILABLE_STATUSES.each do |status|
- description = case status
- when 'active'
- "A runner that is not paused."
- when 'online'
- "A runner that contacted this instance within the last #{::Ci::Runner::ONLINE_CONTACT_TIMEOUT.inspect}."
- when 'offline'
- "A runner that has not contacted this instance within the last #{::Ci::Runner::ONLINE_CONTACT_TIMEOUT.inspect}."
- when 'not_connected'
- "A runner that has never contacted this instance."
- else
- "A runner that is #{status.to_s.tr('_', ' ')}."
- end
+ value 'ACTIVE',
+ description: 'Runner that is not paused.',
+ deprecated: { reason: 'Use CiRunnerType.active instead', milestone: '14.6' },
+ value: :active
- value status.to_s.upcase,
- description: description,
- value: status.to_sym
- end
+ value 'PAUSED',
+ description: 'Runner that is paused.',
+ deprecated: { reason: 'Use CiRunnerType.active instead', milestone: '14.6' },
+ value: :paused
+
+ value 'ONLINE',
+ description: "Runner that contacted this instance within the last #{::Ci::Runner::ONLINE_CONTACT_TIMEOUT.inspect}.",
+ value: :online
+
+ value 'OFFLINE',
+ description: "Runner that has not contacted this instance within the last #{::Ci::Runner::ONLINE_CONTACT_TIMEOUT.inspect}.",
+ deprecated: { reason: 'This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period offline', milestone: '14.6' },
+ value: :offline
+
+ value 'STALE',
+ description: "Runner that has not contacted this instance within the last #{::Ci::Runner::STALE_TIMEOUT.inspect}. Only available if legacyMode is null. Will be a possible return value starting in 15.0",
+ value: :stale
+
+ value 'NOT_CONNECTED',
+ description: 'Runner that has never contacted this instance.',
+ deprecated: { reason: 'This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period of no contact', milestone: '14.6' },
+ value: :not_connected
end
end
end
diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb
index 9bf98aa7e86..d37cca0927f 100644
--- a/app/graphql/types/ci/runner_type.rb
+++ b/app/graphql/types/ci/runner_type.rb
@@ -27,8 +27,11 @@ module Types
description: 'Access level of the runner.'
field :active, GraphQL::Types::Boolean, null: false,
description: 'Indicates the runner is allowed to receive jobs.'
- field :status, ::Types::Ci::RunnerStatusEnum, null: false,
- description: 'Status of the runner.'
+ field :status,
+ Types::Ci::RunnerStatusEnum,
+ null: false,
+ description: 'Status of the runner.',
+ resolver: ::Resolvers::Ci::RunnerStatusResolver
field :version, GraphQL::Types::String, null: true,
description: 'Version of the runner.'
field :short_sha, GraphQL::Types::String, null: true,
@@ -50,7 +53,7 @@ module Types
field :job_count, GraphQL::Types::Int, null: true,
description: "Number of jobs processed by the runner (limited to #{JOB_COUNT_LIMIT}, plus one to indicate that more items exist)."
field :admin_url, GraphQL::Types::String, null: true,
- description: 'Admin URL of the runner. Only available for adminstrators.'
+ description: 'Admin URL of the runner. Only available for administrators.'
def job_count
# We limit to 1 above the JOB_COUNT_LIMIT to indicate that more items exist after JOB_COUNT_LIMIT
diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb
index 09fc1ab9d50..0d514773891 100644
--- a/app/helpers/profiles_helper.rb
+++ b/app/helpers/profiles_helper.rb
@@ -61,6 +61,11 @@ module ProfilesHelper
def ssh_key_expires_field_description
s_('Profiles|Key can still be used after expiration.')
end
+
+ # Overridden in EE::ProfilesHelper#ssh_key_expiration_policy_enabled?
+ def ssh_key_expiration_policy_enabled?
+ false
+ end
end
ProfilesHelper.prepend_mod
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index e86f1b3cf6a..04afbec7765 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -44,7 +44,7 @@ module Ci
AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze
AVAILABLE_TYPES = runner_types.keys.freeze
- AVAILABLE_STATUSES = %w[active paused online offline not_connected].freeze
+ AVAILABLE_STATUSES = %w[active paused online offline not_connected stale].freeze
AVAILABLE_SCOPES = (AVAILABLE_TYPES_LEGACY + AVAILABLE_TYPES + AVAILABLE_STATUSES).freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
@@ -287,10 +287,15 @@ module Ci
end
def stale?
+ return false unless created_at
+
[created_at, contacted_at].compact.max < self.class.stale_deadline
end
- def status
+ def status(legacy_mode = nil)
+ return deprecated_rest_status if legacy_mode == '14.5'
+
+ return :stale if stale?
return :not_connected unless contacted_at
online? ? :online : :offline
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index 19c38d7be62..65882491575 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -32,6 +32,7 @@
= render_if_exists 'admin/application_settings/git_two_factor_session_expiry', form: f
= render_if_exists 'admin/application_settings/personal_access_token_expiration_policy', form: f
= render_if_exists 'admin/application_settings/enforce_pat_expiration', form: f
+ = render_if_exists 'admin/application_settings/ssh_key_expiration_policy', form: f
= render_if_exists 'admin/application_settings/enforce_ssh_key_expiration', form: f
.form-group
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index 74b48115d0e..2b3109225a8 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -1,3 +1,4 @@
+- max_date = ::Gitlab::CurrentSettings.max_ssh_key_lifetime_from_now.to_date if ssh_key_expiration_policy_enabled?
%div
= form_for [:profile, @key], html: { class: 'js-requires-input' } do |f|
= form_errors(@key)
@@ -13,8 +14,8 @@
%p.form-text.text-muted= s_('Profiles|Give your individual key a title. This will be publicly visible.')
.col.form-group
- = f.label :expires_at, s_('Profiles|Expires at'), class: 'label-bold'
- = f.date_field :expires_at, class: "form-control input-lg", min: Date.tomorrow, data: { qa_selector: 'key_expiry_date_field' }
+ = f.label :expires_at, s_('Profiles|Expiration date'), class: 'label-bold'
+ = f.date_field :expires_at, class: "form-control input-lg", min: Date.tomorrow, max: max_date, data: { qa_selector: 'key_expiry_date_field' }
%p.form-text.text-muted{ data: { qa_selector: 'key_expiry_date_field_description' } }= ssh_key_expires_field_description
.js-add-ssh-key-validation-warning.hide
diff --git a/db/migrate/20211118114228_add_max_ssh_key_lifetime_to_application_settings.rb b/db/migrate/20211118114228_add_max_ssh_key_lifetime_to_application_settings.rb
new file mode 100644
index 00000000000..1b0d2104c91
--- /dev/null
+++ b/db/migrate/20211118114228_add_max_ssh_key_lifetime_to_application_settings.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddMaxSshKeyLifetimeToApplicationSettings < Gitlab::Database::Migration[1.0]
+ def change
+ add_column :application_settings, :max_ssh_key_lifetime, :integer
+ end
+end
diff --git a/db/schema_migrations/20211118114228 b/db/schema_migrations/20211118114228
new file mode 100644
index 00000000000..82c7984750d
--- /dev/null
+++ b/db/schema_migrations/20211118114228
@@ -0,0 +1 @@
+7686fd3e33b25b811aba459aba514cde8e88102277edb3be7e12378cb7e8de85 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 444ff934706..3c3caac5cce 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -10477,6 +10477,7 @@ CREATE TABLE application_settings (
sentry_dsn text,
sentry_clientside_dsn text,
sentry_environment text,
+ max_ssh_key_lifetime integer,
static_objects_external_storage_auth_token_encrypted text,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 88f08eb48be..92a37a800a7 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -8750,7 +8750,7 @@ Represents the total number of issues and their weights for a particular day.
| ---- | ---- | ----------- |
| <a id="cirunneraccesslevel"></a>`accessLevel` | [`CiRunnerAccessLevel!`](#cirunneraccesslevel) | Access level of the runner. |
| <a id="cirunneractive"></a>`active` | [`Boolean!`](#boolean) | Indicates the runner is allowed to receive jobs. |
-| <a id="cirunneradminurl"></a>`adminUrl` | [`String`](#string) | Admin URL of the runner. Only available for adminstrators. |
+| <a id="cirunneradminurl"></a>`adminUrl` | [`String`](#string) | Admin URL of the runner. Only available for administrators. |
| <a id="cirunnercontactedat"></a>`contactedAt` | [`Time`](#time) | Last contact from the runner. |
| <a id="cirunnerdescription"></a>`description` | [`String`](#string) | Description of the runner. |
| <a id="cirunnerid"></a>`id` | [`CiRunnerID!`](#cirunnerid) | ID of the runner. |
@@ -8765,11 +8765,24 @@ Represents the total number of issues and their weights for a particular day.
| <a id="cirunnerrununtagged"></a>`runUntagged` | [`Boolean!`](#boolean) | Indicates the runner is able to run untagged jobs. |
| <a id="cirunnerrunnertype"></a>`runnerType` | [`CiRunnerType!`](#cirunnertype) | Type of the runner. |
| <a id="cirunnershortsha"></a>`shortSha` | [`String`](#string) | First eight characters of the runner's token used to authenticate new job requests. Used as the runner's unique ID. |
-| <a id="cirunnerstatus"></a>`status` | [`CiRunnerStatus!`](#cirunnerstatus) | Status of the runner. |
| <a id="cirunnertaglist"></a>`tagList` | [`[String!]`](#string) | Tags associated with the runner. |
| <a id="cirunneruserpermissions"></a>`userPermissions` | [`RunnerPermissions!`](#runnerpermissions) | Permissions for the current user on the resource. |
| <a id="cirunnerversion"></a>`version` | [`String`](#string) | Version of the runner. |
+#### Fields with arguments
+
+##### `CiRunner.status`
+
+Status of the runner.
+
+Returns [`CiRunnerStatus!`](#cirunnerstatus).
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnerstatuslegacymode"></a>`legacyMode` **{warning-solid}** | [`String`](#string) | **Deprecated** in 14.6. Will be removed in 15.0. From that release onward, the field will behave as if legacyMode is null. |
+
### `CiStage`
#### Fields
@@ -15956,11 +15969,12 @@ Values for sorting runners.
| Value | Description |
| ----- | ----------- |
-| <a id="cirunnerstatusactive"></a>`ACTIVE` | A runner that is not paused. |
-| <a id="cirunnerstatusnot_connected"></a>`NOT_CONNECTED` | A runner that has never contacted this instance. |
-| <a id="cirunnerstatusoffline"></a>`OFFLINE` | A runner that has not contacted this instance within the last 2 hours. |
-| <a id="cirunnerstatusonline"></a>`ONLINE` | A runner that contacted this instance within the last 2 hours. |
-| <a id="cirunnerstatuspaused"></a>`PAUSED` | A runner that is paused. |
+| <a id="cirunnerstatusactive"></a>`ACTIVE` **{warning-solid}** | **Deprecated** in 14.6. Use CiRunnerType.active instead. |
+| <a id="cirunnerstatusnot_connected"></a>`NOT_CONNECTED` **{warning-solid}** | **Deprecated** in 14.6. This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period of no contact. |
+| <a id="cirunnerstatusoffline"></a>`OFFLINE` **{warning-solid}** | **Deprecated** in 14.6. This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period offline. |
+| <a id="cirunnerstatusonline"></a>`ONLINE` | Runner that contacted this instance within the last 2 hours. |
+| <a id="cirunnerstatuspaused"></a>`PAUSED` **{warning-solid}** | **Deprecated** in 14.6. Use CiRunnerType.active instead. |
+| <a id="cirunnerstatusstale"></a>`STALE` | Runner that has not contacted this instance within the last 3 months. Only available if legacyMode is null. Will be a possible return value starting in 15.0. |
### `CiRunnerType`
diff --git a/doc/api/project_vulnerabilities.md b/doc/api/project_vulnerabilities.md
index 7ba359587f6..1267f748633 100644
--- a/doc/api/project_vulnerabilities.md
+++ b/doc/api/project_vulnerabilities.md
@@ -10,9 +10,11 @@ type: reference, api
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10242) in GitLab 12.6.
WARNING:
-This API is in an alpha stage and considered unstable.
+This API is in the process of being deprecated and considered unstable.
The response payload may be subject to change or breakage
-across GitLab releases.
+across GitLab releases. Please use the
+[GraphQL API](graphql/reference/index.md#queryvulnerabilities)
+instead.
Every API call to vulnerabilities must be [authenticated](index.md#authentication).
diff --git a/doc/api/settings.md b/doc/api/settings.md
index b0c9c08e3a5..1a3ea6b1fcf 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -343,6 +343,7 @@ listed in the descriptions of the relevant settings.
| `max_import_size` | integer | no | Maximum import size in MB. 0 for unlimited. Default = 0 (unlimited) [Modified](https://gitlab.com/gitlab-org/gitlab/-/issues/251106) from 50MB to 0 in GitLab 13.8. |
| `max_pages_size` | integer | no | Maximum size of pages repositories in MB. |
| `max_personal_access_token_lifetime` | integer | no | **(ULTIMATE SELF)** Maximum allowable lifetime for personal access tokens in days. |
+| `max_ssh_key_lifetime` | integer | no | **(ULTIMATE SELF)** Maximum allowable lifetime for SSH keys in days. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1007) in GitLab 14.6. |
| `metrics_method_call_threshold` | integer | no | A method call is only tracked when it takes longer than the given amount of milliseconds. |
| `mirror_available` | boolean | no | Allow repository mirroring to configured by project Maintainers. If disabled, only Administrators can configure repository mirroring. |
| `mirror_capacity_threshold` | integer | no | **(PREMIUM)** Minimum capacity to be available before scheduling more mirrors preemptively. |
diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md
index afc40995c9c..243ff8ad76b 100644
--- a/doc/user/admin_area/settings/account_and_limit_settings.md
+++ b/doc/user/admin_area/settings/account_and_limit_settings.md
@@ -192,38 +192,45 @@ To set a limit on how long these sessions are valid:
1. Fill in the **Session duration for Git operations when 2FA is enabled (minutes)** field.
1. Click **Save changes**.
-## Limit the lifetime of personal access tokens **(ULTIMATE SELF)**
+## Limit the lifetime of SSH keys **(ULTIMATE SELF)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3649) in GitLab 12.6.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1007) in GitLab 14.6 [with a flag](../../../administration/feature_flags.md) named `ff_limit_ssh_key_lifetime`. Disabled by default.
+
+FLAG:
+On self-managed GitLab, by default this feature is not available. To make it available,
+ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `ff_limit_ssh_key_lifetime`.
+On GitLab.com, this feature is not available. The feature is not ready for production use.
Users can optionally specify a lifetime for
-[personal access tokens](../../profile/personal_access_tokens.md).
+[SSH keys](../../../ssh/index.md).
This lifetime is not a requirement, and can be set to any arbitrary number of days.
-Personal access tokens are the only tokens needed for programmatic access to GitLab.
+SSH keys are user credentials to access GitLab.
However, organizations with security requirements may want to enforce more protection by
-requiring the regular rotation of these tokens.
+requiring the regular rotation of these keys.
### Set a lifetime
Only a GitLab administrator can set a lifetime. Leaving it empty means
there are no restrictions.
-To set a lifetime on how long personal access tokens are valid:
+To set a lifetime on how long SSH keys are valid:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > General**.
1. Expand the **Account and limit** section.
-1. Fill in the **Maximum allowable lifetime for personal access tokens (days)** field.
+1. Fill in the **Maximum allowable lifetime for SSH keys (days)** field.
1. Click **Save changes**.
-Once a lifetime for personal access tokens is set, GitLab:
+Once a lifetime for SSH keys is set, GitLab:
-- Applies the lifetime for new personal access tokens, and require users to set an expiration date
- and a date no later than the allowed lifetime.
-- After three hours, revoke old tokens with no expiration date or with a lifetime longer than the
- allowed lifetime. Three hours is given to allow administrators to change the allowed lifetime,
- or remove it, before revocation takes place.
+- Requires users to set an expiration date that is no later than the allowed lifetime on new
+ SSH keys.
+- Applies the lifetime restriction to existing SSH keys. Keys with no expiry or a lifetime
+ greater than the maximum immediately become invalid.
+
+NOTE:
+When a user's SSH key becomes invalid they can delete and re-add the same key again.
## Allow expired SSH keys to be used **(ULTIMATE SELF)**
@@ -241,6 +248,39 @@ To allow the use of expired SSH keys:
Disabling SSH key expiration immediately enables all expired SSH keys.
+## Limit the lifetime of personal access tokens **(ULTIMATE SELF)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3649) in GitLab 12.6.
+
+Users can optionally specify a lifetime for
+[personal access tokens](../../profile/personal_access_tokens.md).
+This lifetime is not a requirement, and can be set to any arbitrary number of days.
+
+Personal access tokens are the only tokens needed for programmatic access to GitLab.
+However, organizations with security requirements may want to enforce more protection by
+requiring the regular rotation of these tokens.
+
+### Set a lifetime
+
+Only a GitLab administrator can set a lifetime. Leaving it empty means
+there are no restrictions.
+
+To set a lifetime on how long personal access tokens are valid:
+
+1. On the top bar, select **Menu > Admin**.
+1. On the left sidebar, select **Settings > General**.
+1. Expand the **Account and limit** section.
+1. Fill in the **Maximum allowable lifetime for personal access tokens (days)** field.
+1. Click **Save changes**.
+
+Once a lifetime for personal access tokens is set, GitLab:
+
+- Applies the lifetime for new personal access tokens, and require users to set an expiration date
+ and a date no later than the allowed lifetime.
+- After three hours, revoke old tokens with no expiration date or with a lifetime longer than the
+ allowed lifetime. Three hours is given to allow administrators to change the allowed lifetime,
+ or remove it, before revocation takes place.
+
## Allow expired Personal Access Tokens to be used **(ULTIMATE SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214723) in GitLab 13.1.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 9c970d48c3e..11eba8b3392 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -21496,6 +21496,9 @@ msgstr ""
msgid "Maximum allowable lifetime for personal access token (days)"
msgstr ""
+msgid "Maximum allowed lifetime for SSH keys (in days)"
+msgstr ""
+
msgid "Maximum artifacts size"
msgstr ""
@@ -26705,13 +26708,13 @@ msgstr ""
msgid "Profiles|Enter your pronouns to let people know how to refer to you"
msgstr ""
-msgid "Profiles|Expired key is not valid."
+msgid "Profiles|Expiration date"
msgstr ""
-msgid "Profiles|Expired:"
+msgid "Profiles|Expired key is not valid."
msgstr ""
-msgid "Profiles|Expires at"
+msgid "Profiles|Expired:"
msgstr ""
msgid "Profiles|Expires:"
@@ -26753,13 +26756,16 @@ msgstr ""
msgid "Profiles|Key"
msgstr ""
-msgid "Profiles|Key can still be used after expiration."
+msgid "Profiles|Key becomes invalid on this date."
msgstr ""
-msgid "Profiles|Key usable beyond expiration date."
+msgid "Profiles|Key becomes invalid on this date. Maximum lifetime for SSH keys is %{max_ssh_key_lifetime} days"
msgstr ""
-msgid "Profiles|Key will be deleted on this date."
+msgid "Profiles|Key can still be used after expiration."
+msgstr ""
+
+msgid "Profiles|Key usable beyond expiration date."
msgstr ""
msgid "Profiles|Last used:"
@@ -39221,6 +39227,9 @@ msgstr ""
msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
+msgid "When enabled, SSH keys with no expiry date or an invalid expiration date are no longer accepted. Leave blank for no limit."
+msgstr ""
+
msgid "When enabled, existing personal access tokens may be revoked. Leave blank for no limit."
msgstr ""
diff --git a/spec/frontend/google_cloud/components/app_spec.js b/spec/frontend/google_cloud/components/app_spec.js
index 64e5ac8586f..570ac1e6ed1 100644
--- a/spec/frontend/google_cloud/components/app_spec.js
+++ b/spec/frontend/google_cloud/components/app_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { mapValues } from 'lodash';
import App from '~/google_cloud/components/app.vue';
import Home from '~/google_cloud/components/home.vue';
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
@@ -8,103 +9,59 @@ import NoGcpProjects from '~/google_cloud/components/errors/no_gcp_projects.vue'
const BASE_FEEDBACK_URL =
'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/meta/-/issues/new';
+const SCREEN_COMPONENTS = {
+ Home,
+ ServiceAccountsForm,
+ GcpError,
+ NoGcpProjects,
+};
+const SERVICE_ACCOUNTS_FORM_PROPS = {
+ gcpProjects: [1, 2, 3],
+ environments: [4, 5, 6],
+ cancelPath: '',
+};
+const HOME_PROPS = {
+ serviceAccounts: [{}, {}],
+ createServiceAccountUrl: '#url-create-service-account',
+ emptyIllustrationUrl: '#url-empty-illustration',
+};
describe('google_cloud App component', () => {
let wrapper;
const findIncubationBanner = () => wrapper.findComponent(IncubationBanner);
- const findGcpError = () => wrapper.findComponent(GcpError);
- const findNoGcpProjects = () => wrapper.findComponent(NoGcpProjects);
- const findServiceAccountsForm = () => wrapper.findComponent(ServiceAccountsForm);
- const findHome = () => wrapper.findComponent(Home);
afterEach(() => {
wrapper.destroy();
});
- describe('for gcp_error screen', () => {
- beforeEach(() => {
- const propsData = {
- screen: 'gcp_error',
- error: 'mock_gcp_client_error',
- };
- wrapper = shallowMount(App, { propsData });
- });
-
- it('renders the gcp_error screen', () => {
- expect(findGcpError().exists()).toBe(true);
- });
-
- it('should contain incubation banner', () => {
- expect(findIncubationBanner().props()).toEqual({
- shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
- reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,
- featureRequestUrl: `${BASE_FEEDBACK_URL}?issuable_template=feature_request`,
- });
- });
- });
-
- describe('for no_gcp_projects screen', () => {
- beforeEach(() => {
- const propsData = {
- screen: 'no_gcp_projects',
- };
- wrapper = shallowMount(App, { propsData });
- });
-
- it('renders the no_gcp_projects screen', () => {
- expect(findNoGcpProjects().exists()).toBe(true);
- });
+ describe.each`
+ screen | extraProps | componentName
+ ${'gcp_error'} | ${{ error: 'mock_gcp_client_error' }} | ${'GcpError'}
+ ${'no_gcp_projects'} | ${{}} | ${'NoGcpProjects'}
+ ${'service_accounts_form'} | ${SERVICE_ACCOUNTS_FORM_PROPS} | ${'ServiceAccountsForm'}
+ ${'home'} | ${HOME_PROPS} | ${'Home'}
+ `('for screen=$screen', ({ screen, extraProps, componentName }) => {
+ const component = SCREEN_COMPONENTS[componentName];
- it('should contain incubation banner', () => {
- expect(findIncubationBanner().props()).toEqual({
- shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
- reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,
- featureRequestUrl: `${BASE_FEEDBACK_URL}?issuable_template=feature_request`,
- });
- });
- });
-
- describe('for service_accounts_form screen', () => {
beforeEach(() => {
- const propsData = {
- screen: 'service_accounts_form',
- gcpProjects: [1, 2, 3],
- environments: [4, 5, 6],
- cancelPath: '',
- };
- wrapper = shallowMount(App, { propsData });
+ wrapper = shallowMount(App, { propsData: { screen, ...extraProps } });
});
- it('renders the service_accounts_form screen', () => {
- expect(findServiceAccountsForm().exists()).toBe(true);
- });
+ it(`renders only ${componentName}`, () => {
+ const existences = mapValues(SCREEN_COMPONENTS, (x) => wrapper.findComponent(x).exists());
- it('should contain incubation banner', () => {
- expect(findIncubationBanner().props()).toEqual({
- shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
- reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,
- featureRequestUrl: `${BASE_FEEDBACK_URL}?issuable_template=feature_request`,
+ expect(existences).toEqual({
+ ...mapValues(SCREEN_COMPONENTS, () => false),
+ [componentName]: true,
});
});
- });
-
- describe('for home screen', () => {
- beforeEach(() => {
- const propsData = {
- screen: 'home',
- serviceAccounts: [{}, {}],
- createServiceAccountUrl: '#url-create-service-account',
- emptyIllustrationUrl: '#url-empty-illustration',
- };
- wrapper = shallowMount(App, { propsData });
- });
- it('renders the home screen', () => {
- expect(findHome().exists()).toBe(true);
+ it(`renders the ${componentName} with props`, () => {
+ expect(wrapper.findComponent(component).props()).toEqual(extraProps);
});
- it('should contain incubation banner', () => {
+ it('renders incubation banner', () => {
expect(findIncubationBanner().props()).toEqual({
shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,
diff --git a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
index 619e89beb23..1e2173e2988 100644
--- a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
+++ b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
@@ -145,13 +145,20 @@ describe('Sidebar date Widget', () => {
${false} | ${SidebarInheritDate} | ${'SidebarInheritDate'} | ${false}
`(
'when canInherit is $canInherit, $componentName display is $expected',
- ({ canInherit, component, expected }) => {
+ async ({ canInherit, component, expected }) => {
createComponent({ canInherit });
+ await waitForPromises();
expect(wrapper.find(component).exists()).toBe(expected);
},
);
+ it('does not render SidebarInheritDate when canInherit is true and date is loading', async () => {
+ createComponent({ canInherit: true });
+
+ expect(wrapper.find(SidebarInheritDate).exists()).toBe(false);
+ });
+
it('displays a flash message when query is rejected', async () => {
createComponent({
dueDateQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
diff --git a/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js b/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js
index 4d38eba8035..fda21e06987 100644
--- a/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js
+++ b/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js
@@ -10,7 +10,7 @@ describe('SidebarInheritDate', () => {
const findFixedRadio = () => wrapper.findAll(GlFormRadio).at(0);
const findInheritRadio = () => wrapper.findAll(GlFormRadio).at(1);
- const createComponent = () => {
+ const createComponent = ({ dueDateIsFixed = false } = {}) => {
wrapper = shallowMount(SidebarInheritDate, {
provide: {
canUpdate: true,
@@ -18,11 +18,10 @@ describe('SidebarInheritDate', () => {
propsData: {
issuable: {
dueDate: '2021-04-15',
- dueDateIsFixed: true,
+ dueDateIsFixed,
dueDateFixed: '2021-04-15',
dueDateFromMilestones: '2021-05-15',
},
- isLoading: false,
dateType: 'dueDate',
},
});
@@ -45,6 +44,13 @@ describe('SidebarInheritDate', () => {
expect(findInheritRadio().text()).toBe('Inherited:');
});
+ it('does not emit set-date if fixed value does not change', () => {
+ createComponent({ dueDateIsFixed: true });
+ findFixedRadio().vm.$emit('input', true);
+
+ expect(wrapper.emitted('set-date')).toBeUndefined();
+ });
+
it('emits set-date event on click on radio button', () => {
findFixedRadio().vm.$emit('input', true);
diff --git a/spec/graphql/resolvers/ci/runner_status_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_status_resolver_spec.rb
new file mode 100644
index 00000000000..fbef07b72e6
--- /dev/null
+++ b/spec/graphql/resolvers/ci/runner_status_resolver_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::RunnerStatusResolver do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ let(:user) { build(:user) }
+ let(:runner) { build(:ci_runner) }
+
+ subject(:resolve_subject) { resolve(described_class, ctx: { current_user: user }, obj: runner, args: args) }
+
+ context 'with legacy_mode' do
+ context 'set to 14.5' do
+ let(:args) do
+ { legacy_mode: '14.5' }
+ end
+
+ it 'calls runner.status with specified legacy_mode' do
+ expect(runner).to receive(:status).with('14.5').once.and_return(:online)
+
+ expect(resolve_subject).to eq(:online)
+ end
+ end
+
+ context 'set to nil' do
+ let(:args) do
+ { legacy_mode: nil }
+ end
+
+ it 'calls runner.status with specified legacy_mode' do
+ expect(runner).to receive(:status).with(nil).once.and_return(:stale)
+
+ expect(resolve_subject).to eq(:stale)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 9138bb0717b..4ffcca24bf8 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -342,6 +342,7 @@ RSpec.describe Ci::Runner do
using RSpec::Parameterized::TableSyntax
where(:created_at, :contacted_at, :expected_stale?) do
+ nil | nil | false
3.months.ago - 1.second | 3.months.ago - 0.001.seconds | true
3.months.ago - 1.second | 3.months.ago + 1.hour | false
3.months.ago - 1.second | nil | true
@@ -376,6 +377,8 @@ RSpec.describe Ci::Runner do
end
def stub_redis_runner_contacted_at(value)
+ return unless created_at
+
Gitlab::Redis::Cache.with do |redis|
cache_key = runner.send(:cache_attribute_key)
expect(redis).to receive(:get).with(cache_key)
@@ -419,7 +422,7 @@ RSpec.describe Ci::Runner do
it { is_expected.to be_falsey }
end
- context 'contacted long time ago time' do
+ context 'contacted long time ago' do
before do
runner.contacted_at = 1.year.ago
end
@@ -437,7 +440,7 @@ RSpec.describe Ci::Runner do
end
context 'with cache value' do
- context 'contacted long time ago time' do
+ context 'contacted long time ago' do
before do
runner.contacted_at = 1.year.ago
stub_redis_runner_contacted_at(1.year.ago.to_s)
@@ -699,16 +702,33 @@ RSpec.describe Ci::Runner do
end
describe '#status' do
- let(:runner) { build(:ci_runner, :instance) }
+ let(:runner) { build(:ci_runner, :instance, created_at: 4.months.ago) }
+ let(:legacy_mode) { }
- subject { runner.status }
+ subject { runner.status(legacy_mode) }
context 'never connected' do
before do
runner.contacted_at = nil
end
- it { is_expected.to eq(:not_connected) }
+ context 'with legacy_mode enabled' do
+ let(:legacy_mode) { '14.5' }
+
+ it { is_expected.to eq(:not_connected) }
+ end
+
+ context 'with legacy_mode disabled' do
+ it { is_expected.to eq(:stale) }
+ end
+
+ context 'created recently' do
+ before do
+ runner.created_at = 1.day.ago
+ end
+
+ it { is_expected.to eq(:not_connected) }
+ end
end
context 'inactive but online' do
@@ -717,7 +737,15 @@ RSpec.describe Ci::Runner do
runner.active = false
end
- it { is_expected.to eq(:online) }
+ context 'with legacy_mode enabled' do
+ let(:legacy_mode) { '14.5' }
+
+ it { is_expected.to eq(:paused) }
+ end
+
+ context 'with legacy_mode disabled' do
+ it { is_expected.to eq(:online) }
+ end
end
context 'contacted 1s ago' do
@@ -728,13 +756,29 @@ RSpec.describe Ci::Runner do
it { is_expected.to eq(:online) }
end
- context 'contacted long time ago' do
+ context 'contacted recently' do
before do
- runner.contacted_at = 1.year.ago
+ runner.contacted_at = (3.months - 1.hour).ago
end
it { is_expected.to eq(:offline) }
end
+
+ context 'contacted long time ago' do
+ before do
+ runner.contacted_at = (3.months + 1.second).ago
+ end
+
+ context 'with legacy_mode enabled' do
+ let(:legacy_mode) { '14.5' }
+
+ it { is_expected.to eq(:offline) }
+ end
+
+ context 'with legacy_mode disabled' do
+ it { is_expected.to eq(:stale) }
+ end
+ end
end
describe '#deprecated_rest_status' do
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index ab53ff654e9..66601c0e810 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -63,7 +63,7 @@ RSpec.describe 'Query.runner(id)' do
'revision' => runner.revision,
'locked' => false,
'active' => runner.active,
- 'status' => runner.status.to_s.upcase,
+ 'status' => runner.status('14.5').to_s.upcase,
'maximumTimeout' => runner.maximum_timeout,
'accessLevel' => runner.access_level.to_s.upcase,
'runUntagged' => runner.run_untagged,
@@ -221,6 +221,45 @@ RSpec.describe 'Query.runner(id)' do
end
end
+ describe 'for runner with status' do
+ let_it_be(:stale_runner) { create(:ci_runner, description: 'Stale runner 1', created_at: 3.months.ago) }
+
+ let(:query) do
+ %(
+ query {
+ staleRunner: runner(id: "#{stale_runner.to_global_id}") {
+ status
+ legacyStatusWithExplicitVersion: status(legacyMode: "14.5")
+ newStatus: status(legacyMode: null)
+ }
+ pausedRunner: runner(id: "#{inactive_instance_runner.to_global_id}") {
+ status
+ legacyStatusWithExplicitVersion: status(legacyMode: "14.5")
+ newStatus: status(legacyMode: null)
+ }
+ }
+ )
+ end
+
+ it 'retrieves status fields with expected values' do
+ post_graphql(query, current_user: user)
+
+ stale_runner_data = graphql_data_at(:stale_runner)
+ expect(stale_runner_data).to match a_hash_including(
+ 'status' => 'NOT_CONNECTED',
+ 'legacyStatusWithExplicitVersion' => 'NOT_CONNECTED',
+ 'newStatus' => 'STALE'
+ )
+
+ paused_runner_data = graphql_data_at(:paused_runner)
+ expect(paused_runner_data).to match a_hash_including(
+ 'status' => 'PAUSED',
+ 'legacyStatusWithExplicitVersion' => 'PAUSED',
+ 'newStatus' => 'OFFLINE'
+ )
+ end
+ end
+
describe 'for multiple runners' do
let_it_be(:project1) { create(:project, :test_repo) }
let_it_be(:project2) { create(:project, :test_repo) }
diff --git a/spec/views/profiles/keys/_form.html.haml_spec.rb b/spec/views/profiles/keys/_form.html.haml_spec.rb
index 0f4d7ecc699..d5a605958dc 100644
--- a/spec/views/profiles/keys/_form.html.haml_spec.rb
+++ b/spec/views/profiles/keys/_form.html.haml_spec.rb
@@ -33,8 +33,8 @@ RSpec.describe 'profiles/keys/_form.html.haml' do
end
it 'has the expires at field', :aggregate_failures do
- expect(rendered).to have_field('Expires at', type: 'date')
- expect(page.find_field('Expires at')['min']).to eq(l(1.day.from_now, format: "%Y-%m-%d"))
+ expect(rendered).to have_field('Expiration date', type: 'date')
+ expect(page.find_field('Expiration date')['min']).to eq(l(1.day.from_now, format: "%Y-%m-%d"))
expect(rendered).to have_text('Key can still be used after expiration.')
end