diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index 30f25e8fdaa..21adc032940 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -20,12 +20,8 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController def revoke @personal_access_token = finder.find(params[:id]) - - if @personal_access_token.revoke! - flash[:notice] = _("Revoked personal access token %{personal_access_token_name}!") % { personal_access_token_name: @personal_access_token.name } - else - flash[:alert] = _("Could not revoke personal access token %{personal_access_token_name}.") % { personal_access_token_name: @personal_access_token.name } - end + service = PersonalAccessTokens::RevokeService.new(current_user, token: @personal_access_token).execute + service.success? ? flash[:notice] = service.message : flash[:alert] = service.message redirect_to profile_personal_access_tokens_path end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 7cf68b26eab..3b061ce2e67 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -194,6 +194,10 @@ module ApplicationHelper 'https://' + promo_host end + def contact_sales_url + promo_url + '/sales' + end + def support_url Gitlab::CurrentSettings.current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/' end diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 47ac19635ab..6df1538ce8c 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -284,7 +284,7 @@ module Ci def expire_in=(value) self.expire_at = if value - ChronicDuration.parse(value)&.seconds&.from_now + ::Gitlab::Ci::Build::Artifacts::ExpireInParser.new(value).seconds_from_now end end diff --git a/app/policies/personal_access_token_policy.rb b/app/policies/personal_access_token_policy.rb index c9b4c2d44d5..aa87550fd6b 100644 --- a/app/policies/personal_access_token_policy.rb +++ b/app/policies/personal_access_token_policy.rb @@ -3,7 +3,8 @@ class PersonalAccessTokenPolicy < BasePolicy condition(:is_owner) { user && subject.user_id == user.id } - rule { is_owner | admin }.policy do + rule { is_owner | admin & ~blocked }.policy do enable :read_token + enable :revoke_token end end diff --git a/app/services/metrics/dashboard/pod_dashboard_service.rb b/app/services/metrics/dashboard/pod_dashboard_service.rb index a4b3393409b..310f78c6a4b 100644 --- a/app/services/metrics/dashboard/pod_dashboard_service.rb +++ b/app/services/metrics/dashboard/pod_dashboard_service.rb @@ -7,7 +7,7 @@ module Metrics DASHBOARD_NAME = N_('K8s pod health') # SHA256 hash of dashboard content - DASHBOARD_VERSION = '0515db7a99078a2423b037f99251ba16bd163603c0a30229ae8aa7386e96421c' + DASHBOARD_VERSION = '3a91b32f91b2dd3d90275333c0ea3630b3f3f37c4296ede5b5eef59bf523d66b' SEQUENCE = [ STAGES::MetricEndpointInserter, diff --git a/app/services/personal_access_tokens/revoke_service.rb b/app/services/personal_access_tokens/revoke_service.rb new file mode 100644 index 00000000000..16ba42bd317 --- /dev/null +++ b/app/services/personal_access_tokens/revoke_service.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module PersonalAccessTokens + class RevokeService + attr_reader :token, :current_user + + def initialize(current_user = nil, params = { token: nil }) + @current_user = current_user + @token = params[:token] + end + + def execute + return ServiceResponse.error(message: 'Not permitted to revoke') unless revocation_permitted? + + if token.revoke! + ServiceResponse.success(message: success_message) + else + ServiceResponse.error(message: error_message) + end + end + + private + + def error_message + _("Could not revoke personal access token %{personal_access_token_name}.") % { personal_access_token_name: token.name } + end + + def success_message + _("Revoked personal access token %{personal_access_token_name}!") % { personal_access_token_name: token.name } + end + + def revocation_permitted? + Ability.allowed?(current_user, :revoke_token, token) + end + end +end diff --git a/changelogs/unreleased/219867-replace-deprecated-button.yml b/changelogs/unreleased/219867-replace-deprecated-button.yml new file mode 100644 index 00000000000..b454cc85104 --- /dev/null +++ b/changelogs/unreleased/219867-replace-deprecated-button.yml @@ -0,0 +1,5 @@ +--- +title: Replace deprecated button on vulnerability details page +merge_request: 38679 +author: +type: other diff --git a/changelogs/unreleased/feat-artifact-expire-never-keyword.yml b/changelogs/unreleased/feat-artifact-expire-never-keyword.yml new file mode 100644 index 00000000000..62d47e33187 --- /dev/null +++ b/changelogs/unreleased/feat-artifact-expire-never-keyword.yml @@ -0,0 +1,5 @@ +--- +title: Add support for never keyword in expire_in job artifacts +merge_request: 38578 +author: Fabio Huser +type: added diff --git a/config/prometheus/pod_metrics.yml b/config/prometheus/pod_metrics.yml index d5c418ba98e..b9395124405 100644 --- a/config/prometheus/pod_metrics.yml +++ b/config/prometheus/pod_metrics.yml @@ -15,55 +15,101 @@ panel_groups: panels: - title: "CPU usage" type: "line-chart" - y_label: "Cores per pod" + y_label: "Cores per container" metrics: - id: pod_cpu_usage_seconds_total - query_range: 'rate(container_cpu_usage_seconds_total{pod="{{pod}}",container="POD"}[5m])' + query_range: >- + sum( + rate(container_cpu_usage_seconds_total{pod="{{pod}}",container!="POD"}[5m]) + ) + by (container) unit: "cores" - label: pod + label: container + + - title: "CPU throttling" + type: "line-chart" + y_label: "Cores per container" + metrics: + - id: pod_cpu_cfs_throttle + query_range: >- + sum( + rate(container_cpu_cfs_throttled_seconds_total{pod="{{pod}}"}[5m]) + ) + by (container) + unit: "cores" + label: container + - group: Memory metrics panels: - title: "Memory usage working set" type: "line-chart" - y_label: "Working set memory (MiB)" + y_label: "Working set memory" metrics: - id: pod_memory_working_set - query_range: 'container_memory_working_set_bytes{pod="{{pod}}",container="POD"}/1024/1024' - unit: "MiB" - label: pod + query_range: >- + sum( + container_memory_working_set_bytes{pod="{{pod}}",container!="POD"} + ) by (container) + unit: "bytes" + label: container + - group: Network metrics panels: - title: "Network Receive (In)" type: "line-chart" - y_label: "Received (KiB/sec)" + y_label: "Received (bytes/sec)" metrics: - id: pod_network_receive - query_range: 'rate(container_network_receive_bytes_total{pod="{{pod}}",container="POD"}[5m])/1024' - unit: "KiB / sec" + query_range: >- + sum( + rate( + container_network_receive_bytes_total{pod="{{pod}}"}[5m] + ) + ) by (pod) + unit: "bytes" label: pod + - title: "Network Transmit (Out)" type: "line-chart" - y_label: "Transmitted (KiB/sec)" + y_label: "Transmitted (bytes/sec)" metrics: - id: pod_network_transmit - query_range: 'rate(container_network_transmit_bytes_total{pod="{{pod}}",container="POD"}[5m])/1024' - unit: "KiB / sec" + query_range: >- + sum( + rate( + container_network_transmit_bytes_total{pod="{{pod}}"}[5m] + ) + ) by (pod) + unit: bytes label: pod + - group: Disk metrics panels: - title: "Disk Reads" type: "line-chart" - y_label: "Disk reads (KiB/sec)" + y_label: "Disk reads (bytes/sec)" metrics: - id: pod_disk_reads - query_range: 'rate(container_fs_reads_bytes_total{container="POD",pod="{{pod}}"}[5m])/1024' - unit: "KiB / sec" - label: pod + query_range: >- + sum( + rate( + container_fs_reads_bytes_total{pod="{{pod}}", container!="POD"}[5m] + ) + ) by (container,device) + + unit: "bytes / sec" + label: "{{container}} {{device}}" + - title: "Disk Writes" type: "line-chart" - y_label: "Disk writes (KiB/sec)" + y_label: "Disk writes (bytes/sec)" metrics: - id: pod_disk_writes - query_range: 'rate(container_fs_writes_bytes_total{container="POD",pod="{{pod}}"}[5m])/1024' - unit: "KiB / sec" - label: pod + query_range: >- + sum( + rate( + container_fs_writes_bytes_total{pod="{{pod}}", container!="POD"}[5m] + ) + ) by (container,device) + unit: "bytes / sec" + label: "{{container}} {{device}}" diff --git a/doc/administration/geo/replication/configuration.md b/doc/administration/geo/replication/configuration.md index 25c8e7c408a..74fa8e3b8f2 100644 --- a/doc/administration/geo/replication/configuration.md +++ b/doc/administration/geo/replication/configuration.md @@ -194,14 +194,13 @@ keys must be manually replicated to the **secondary** node. 1. Visit the **primary** node's **Admin Area > Geo** (`/admin/geo/nodes`) in your browser. 1. Click the **New node** button. - ![Add secondary node](img/adding_a_secondary_node.png) + ![Add secondary node](img/adding_a_secondary_node_v13_3.png) 1. Fill in **Name** with the `gitlab_rails['geo_node_name']` in `/etc/gitlab/gitlab.rb`. These values must always match *exactly*, character for character. 1. Fill in **URL** with the `external_url` in `/etc/gitlab/gitlab.rb`. These values must always match, but it doesn't matter if one ends with a `/` and the other doesn't. -1. **Do NOT** check the **This is a primary node** checkbox. 1. Optionally, choose which groups or storage shards should be replicated by the **secondary** node. Leave blank to replicate all. Read more in [selective synchronization](#selective-synchronization). diff --git a/doc/administration/geo/replication/img/adding_a_secondary_node.png b/doc/administration/geo/replication/img/adding_a_secondary_node.png deleted file mode 100644 index e33b690da18..00000000000 Binary files a/doc/administration/geo/replication/img/adding_a_secondary_node.png and /dev/null differ diff --git a/doc/administration/geo/replication/img/adding_a_secondary_node_v13_3.png b/doc/administration/geo/replication/img/adding_a_secondary_node_v13_3.png new file mode 100644 index 00000000000..023cfe263b9 Binary files /dev/null and b/doc/administration/geo/replication/img/adding_a_secondary_node_v13_3.png differ diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 1d412174a7b..9e2aa602767 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -204,6 +204,7 @@ control over how the Pages daemon runs and serves content in your environment. | `external_https` | Configure Pages to bind to one or more secondary IP addresses, serving HTTPS requests. Multiple addresses can be given as an array, along with exact ports, for example `['1.2.3.4', '1.2.3.5:8063']`. Sets value for `listen_https`. | `gitlab_client_http_timeout` | GitLab API HTTP client connection timeout in seconds (default: 10s). | `gitlab_client_jwt_expiry` | JWT Token expiry time in seconds (default: 30s). +| `domain_config_source` | Domain configuration source (default: `disk`) | `gitlab_id` | The OAuth application public ID. Leave blank to automatically fill when Pages authenticates with GitLab. | `gitlab_secret` | The OAuth application secret. Leave blank to automatically fill when Pages authenticates with GitLab. | `gitlab_server` | Server to use for authentication when access control is enabled; defaults to GitLab `external_url`. @@ -601,6 +602,43 @@ configuring a load balancer to work at the IP level, and so on. If you wish to set up GitLab Pages on multiple servers, perform the above procedure for each Pages server. +## Domain source configuration + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217912) in GitLab 13.3. + +GitLab Pages can use different sources to get domain configuration. +The default value is `nil`; however, GitLab Pages will default to `disk`. + + ```ruby + gitlab_pages['domain_config_source'] = nil + ``` + +You can specify `gitlab` to enable [API-based configuration](#gitlab-api-based-configuration). + +For more details see this [blog post](https://about.gitlab.com/blog/2020/08/03/how-gitlab-pages-uses-the-gitlab-api-to-serve-content/). + +### GitLab API-based configuration + +GitLab Pages can use an API-based configuration. This replaces disk source configuration, which +was used prior to GitLab 13.0. Follow these steps to enable it: + +1. Add the following to your `/etc/gitlab/gitlab.erb` file: + + ```ruby + gitlab_pages['domain_config_source'] = "gitlab" + ``` + +1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. + +If you encounter an issue, you can disable it by choosing `disk` or `nil`: + +```ruby +gitlab_pages['domain_config_source'] = nil +``` + +For other common issues, see the [troubleshooting section](#failed-to-connect-to-the-internal-gitlab-api) +or report an issue. + ## Backup GitLab Pages are part of the [regular backup](../../raketasks/backup_restore.md), so there is no separate backup to configure. @@ -696,3 +734,24 @@ date > /var/opt/gitlab/gitlab-rails/shared/pages/.update ``` If you've customized the Pages storage path, adjust the command above to use your custom path. + +### Failed to connect to the internal GitLab API + +If you have enabled [API-based configuration](#gitlab-api-based-configuration) and see the following error: + +```plaintext +ERRO[0010] Failed to connect to the internal GitLab API after 0.50s error="failed to connect to internal Pages API: HTTP status: 401" +``` + +If you are [Running GitLab Pages on a separate server](#running-gitlab-pages-on-a-separate-server) +you must copy the `/etc/gitlab/gitlab-secrets.json` file +from the **GitLab server** to the **Pages server** after upgrading to GitLab 13.3, +as described in that section. + +Other reasons may include network connectivity issues between your +**GitLab server** and your **Pages server** such as firewall configurations or closed ports. +For example, if there is a connection timeout: + +```plaintext +error="failed to connect to internal Pages API: Get \"https://gitlab.example.com:3000/api/v4/internal/pages/status\": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)" +``` diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index bc117d774d5..0b2afc3791f 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -3184,8 +3184,11 @@ stored on GitLab. If the expiry time is not defined, it defaults to the [instance wide setting](../../user/admin_area/settings/continuous_integration.md#default-artifacts-expiration-core-only) (30 days by default). -You can use the **Keep** button on the job page to override expiration and -keep artifacts forever. +To override the expiration time and keep artifacts forever: + +- Use the **Keep** button on the job page. +- Set the value of `expire_in` to `never`. [Available](https://gitlab.com/gitlab-org/gitlab/-/issues/22761) + in GitLab 13.3 and later. After their expiry, artifacts are deleted hourly by default (via a cron job), and are not accessible anymore. @@ -3200,6 +3203,7 @@ provided. Examples of valid values: - `6 mos 1 day` - `47 yrs 6 mos and 4d` - `3 weeks and 2 days` +- `never` To expire artifacts 1 week after being uploaded: diff --git a/doc/user/project/clusters/add_eks_clusters.md b/doc/user/project/clusters/add_eks_clusters.md index be4e9c28380..a1d957ce128 100644 --- a/doc/user/project/clusters/add_eks_clusters.md +++ b/doc/user/project/clusters/add_eks_clusters.md @@ -65,7 +65,7 @@ To create and add a new Kubernetes cluster to your project, group, or instance: 1. In the [IAM Management Console](https://console.aws.amazon.com/iam/home), create an EKS management IAM role. To do so, follow the [Amazon EKS cluster IAM role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html) instructions to create a IAM role suitable for managing the AWS EKS cluster's resources on your behalf. - In addition to the policies that guide suggests, you must also include the `AmazonEKSServicePolicy` + In addition to the policies that guide suggests, you must also include the `AmazonEKSClusterPolicy` policy for this role in order for GitLab to manage the EKS cluster correctly. 1. In the [IAM Management Console](https://console.aws.amazon.com/iam/home), create an IAM role: 1. From the left panel, select **Roles**. @@ -208,7 +208,7 @@ NOTE: **Note:** This role should be the role you created by following the [EKS cluster IAM role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html) guide. In addition to the policies that guide suggests, you must also include the -`AmazonEKSServicePolicy` policy for this role in order for GitLab to manage the EKS cluster correctly. +`AmazonEKSClusterPolicy` policy for this role in order for GitLab to manage the EKS cluster correctly. ## Existing EKS cluster diff --git a/lib/gitlab/ci/build/artifacts/expire_in_parser.rb b/lib/gitlab/ci/build/artifacts/expire_in_parser.rb new file mode 100644 index 00000000000..3e8a1fb86fc --- /dev/null +++ b/lib/gitlab/ci/build/artifacts/expire_in_parser.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Build + module Artifacts + class ExpireInParser + def self.validate_duration(value) + new(value).validate_duration + end + + def initialize(value) + @value = value + end + + def validate_duration + return true if never? + + parse + rescue ChronicDuration::DurationParseError + false + end + + def seconds_from_now + parse&.seconds&.from_now + end + + private + + attr_reader :value + + def parse + return if never? + + ChronicDuration.parse(value) + end + + def never? + value.to_s.casecmp('never') == 0 + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb index a9a9636637f..206dbaea272 100644 --- a/lib/gitlab/ci/config/entry/artifacts.rb +++ b/lib/gitlab/ci/config/entry/artifacts.rb @@ -42,7 +42,7 @@ module Gitlab inclusion: { in: %w[on_success on_failure always], message: 'should be on_success, on_failure ' \ 'or always' } - validates :expire_in, duration: true + validates :expire_in, duration: { parser: ::Gitlab::Ci::Build::Artifacts::ExpireInParser } end end diff --git a/lib/gitlab/config/entry/legacy_validation_helpers.rb b/lib/gitlab/config/entry/legacy_validation_helpers.rb index ea5c887552e..415f6f77214 100644 --- a/lib/gitlab/config/entry/legacy_validation_helpers.rb +++ b/lib/gitlab/config/entry/legacy_validation_helpers.rb @@ -6,17 +6,27 @@ module Gitlab module LegacyValidationHelpers private - def validate_duration(value) - value.is_a?(String) && ChronicDuration.parse(value) + def validate_duration(value, parser = nil) + return false unless value.is_a?(String) + + if parser && parser.respond_to?(:validate_duration) + parser.validate_duration(value) + else + ChronicDuration.parse(value) + end rescue ChronicDuration::DurationParseError false end - def validate_duration_limit(value, limit) + def validate_duration_limit(value, limit, parser = nil) return false unless value.is_a?(String) - ChronicDuration.parse(value).second.from_now < - ChronicDuration.parse(limit).second.from_now + if parser && parser.respond_to?(:validate_duration_limit) + parser.validate_duration_limit(value, limit) + else + ChronicDuration.parse(value).second.from_now < + ChronicDuration.parse(limit).second.from_now + end rescue ChronicDuration::DurationParseError false end diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb index 813ec9126f0..a7ec98ace6e 100644 --- a/lib/gitlab/config/entry/validators.rb +++ b/lib/gitlab/config/entry/validators.rb @@ -106,12 +106,12 @@ module Gitlab include LegacyValidationHelpers def validate_each(record, attribute, value) - unless validate_duration(value) + unless validate_duration(value, options[:parser]) record.errors.add(attribute, 'should be a duration') end if options[:limit] - unless validate_duration_limit(value, options[:limit]) + unless validate_duration_limit(value, options[:limit], options[:parser]) record.errors.add(attribute, 'should not exceed the limit') end end diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb index 2dd06504542..87bc0238033 100644 --- a/lib/gitlab/experimentation.rb +++ b/lib/gitlab/experimentation.rb @@ -56,6 +56,9 @@ module Gitlab }, terms_opt_in: { tracking_category: 'Growth::Acquisition::Experiment::TermsOptIn' + }, + contact_sales_btn_in_app: { + tracking_category: 'Growth::Conversion::Experiment::ContactSalesInApp' } }.freeze diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b2b3c17d8e1..fc991854eb1 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3824,6 +3824,9 @@ msgstr "" msgid "BillingPlans|per user" msgstr "" +msgid "BillingPlan|Contact sales" +msgstr "" + msgid "BillingPlan|Upgrade" msgstr "" @@ -21303,6 +21306,9 @@ msgstr "" msgid "SecurityConfiguration|Could not retrieve configuration data. Please refresh the page, or try again later." msgstr "" +msgid "SecurityConfiguration|Create Merge Request" +msgstr "" + msgid "SecurityConfiguration|Customize common SAST settings to suit your requirements. Configuration changes made here override those provided by GitLab and are excluded from updates. For details of more advanced configuration options, see the %{linkStart}GitLab SAST documentation%{linkEnd}." msgstr "" diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index 21a0d01a9bf..4438831fb76 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -100,14 +100,11 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do context "when revocation fails" do it "displays an error message" do visit profile_personal_access_tokens_path - allow_any_instance_of(PersonalAccessToken).to receive(:update!).and_return(false) - - errors = ActiveModel::Errors.new(PersonalAccessToken.new).tap { |e| e.add(:name, "cannot be nil") } - allow_any_instance_of(PersonalAccessToken).to receive(:errors).and_return(errors) + allow_any_instance_of(PersonalAccessTokens::RevokeService).to receive(:revocation_permitted?).and_return(false) accept_confirm { click_on "Revoke" } expect(active_personal_access_tokens).to have_text(personal_access_token.name) - expect(page).to have_content("Could not revoke") + expect(page).to have_content("Not permitted to revoke") end end end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 08107b841d7..3538fe98628 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -168,6 +168,19 @@ RSpec.describe ApplicationHelper do it { expect(helper.active_when(false)).to eq(nil) } end + describe '#contact_sales_url' do + subject { helper.contact_sales_url } + + it 'passes a smoke test' do + is_expected.to eq('https://about.gitlab.com/sales') + end + + it 'changes if promo_url changes' do + allow(helper).to receive(:promo_url).and_return('https://somewhere.else') + is_expected.to eq('https://somewhere.else/sales') + end + end + describe '#support_url' do context 'when alternate support url is specified' do let(:alternate_url) { 'http://company.example.com/getting-help' } diff --git a/spec/lib/gitlab/ci/build/artifacts/expire_in_parser_spec.rb b/spec/lib/gitlab/ci/build/artifacts/expire_in_parser_spec.rb new file mode 100644 index 00000000000..0e26a9fa571 --- /dev/null +++ b/spec/lib/gitlab/ci/build/artifacts/expire_in_parser_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Build::Artifacts::ExpireInParser do + describe '.validate_duration' do + subject { described_class.validate_duration(value) } + + context 'with never' do + let(:value) { 'never' } + + it { is_expected.to be_truthy } + end + + context 'with never value camelized' do + let(:value) { 'Never' } + + it { is_expected.to be_truthy } + end + + context 'with a duration' do + let(:value) { '1 Day' } + + it { is_expected.to be_truthy } + end + + context 'without a duration' do + let(:value) { 'something' } + + it { is_expected.to be_falsy } + end + end + + describe '#seconds_from_now' do + subject { described_class.new(value).seconds_from_now } + + context 'with never' do + let(:value) { 'never' } + + it { is_expected.to be_nil } + end + + context 'with an empty string' do + let(:value) { '' } + + it { is_expected.to be_nil } + end + + context 'with a duration' do + let(:value) { '1 day' } + + it { is_expected.to be_like_time(1.day.from_now) } + end + end +end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 3c5c0639d9c..80ec67e8cd5 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -1559,6 +1559,21 @@ module Gitlab }) end + it "returns artifacts with expire_in never keyword" do + config = YAML.dump({ + rspec: { + script: "rspec", + artifacts: { paths: ["releases/"], expire_in: "never" } + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + builds = config_processor.stage_builds_attributes("test") + + expect(builds.size).to eq(1) + expect(builds.first[:options][:artifacts][:expire_in]).to eq('never') + end + %w[on_success on_failure always].each do |when_state| it "returns artifacts for when #{when_state} defined" do config = YAML.dump({ diff --git a/spec/policies/personal_access_token_policy_spec.rb b/spec/policies/personal_access_token_policy_spec.rb index 2236af81763..706150597b2 100644 --- a/spec/policies/personal_access_token_policy_spec.rb +++ b/spec/policies/personal_access_token_policy_spec.rb @@ -14,7 +14,7 @@ RSpec.describe PersonalAccessTokenPolicy do end with_them do - context 'determine if a token is readable by a user' do + context 'determine if a token is readable or revocable by a user' do let(:user) { build_stubbed(user_type) } let(:token_owner) { owned_by_same_user ? user : build(:user) } let(:token) { build(:personal_access_token, user: token_owner) } @@ -26,6 +26,17 @@ RSpec.describe PersonalAccessTokenPolicy do end it { is_expected.to(expected_permitted? ? be_allowed(:read_token) : be_disallowed(:read_token)) } + it { is_expected.to(expected_permitted? ? be_allowed(:revoke_token) : be_disallowed(:revoke_token)) } end end + + context 'current_user is a blocked administrator', :enable_admin_mode do + subject { described_class.new(current_user, token) } + + let(:current_user) { create(:user, :admin, :blocked) } + let(:token) { create(:personal_access_token) } + + it { is_expected.to be_disallowed(:revoke_token) } + it { is_expected.to be_disallowed(:read_token) } + end end diff --git a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb index 9be69d4c562..e5c60bb539b 100644 --- a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb +++ b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb @@ -479,6 +479,16 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do expect(job.reload.artifacts_expire_at).to be_nil end end + + context 'when value is never' do + let(:expire_in) { 'never' } + let(:default_artifacts_expire_in) { '5 days' } + + it 'does not set expire_in' do + expect(response).to have_gitlab_http_status(:created) + expect(job.reload.artifacts_expire_at).to be_nil + end + end end end end diff --git a/spec/services/ci/create_job_artifacts_service_spec.rb b/spec/services/ci/create_job_artifacts_service_spec.rb index 3f5cf079025..72b0d220b11 100644 --- a/spec/services/ci/create_job_artifacts_service_spec.rb +++ b/spec/services/ci/create_job_artifacts_service_spec.rb @@ -73,7 +73,7 @@ RSpec.describe Ci::CreateJobArtifactsService do expect(metadata_artifact.expire_at).to be_within(1.minute).of(expected_expire_at) end - context 'when expire_in params is set' do + context 'when expire_in params is set to a specific value' do before do params.merge!('expire_in' => '2 hours') end @@ -89,6 +89,23 @@ RSpec.describe Ci::CreateJobArtifactsService do expect(metadata_artifact.expire_at).to be_within(1.minute).of(expected_expire_at) end end + + context 'when expire_in params is set to `never`' do + before do + params.merge!('expire_in' => 'never') + end + + it 'sets expiration date according to the parameter' do + expected_expire_at = nil + + expect(subject).to be_truthy + archive_artifact, metadata_artifact = job.job_artifacts.last(2) + + expect(job.artifacts_expire_at).to eq(expected_expire_at) + expect(archive_artifact.expire_at).to eq(expected_expire_at) + expect(metadata_artifact.expire_at).to eq(expected_expire_at) + end + end end end diff --git a/spec/services/personal_access_tokens/revoke_service_spec.rb b/spec/services/personal_access_tokens/revoke_service_spec.rb new file mode 100644 index 00000000000..5afa43cef76 --- /dev/null +++ b/spec/services/personal_access_tokens/revoke_service_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe PersonalAccessTokens::RevokeService do + shared_examples_for 'a successfully revoked token' do + it { expect(subject.success?).to be true } + it { expect(service.token.revoked?).to be true } + end + + shared_examples_for 'an unsuccessfully revoked token' do + it { expect(subject.success?).to be false } + it { expect(service.token.revoked?).to be false } + end + + describe '#execute' do + subject { service.execute } + + let(:service) { described_class.new(current_user, token: token) } + + context 'when current_user is an administrator' do + let_it_be(:current_user) { create(:admin) } + let_it_be(:token) { create(:personal_access_token) } + + it_behaves_like 'a successfully revoked token' + end + + context 'when current_user is not an administrator' do + let_it_be(:current_user) { create(:user) } + + context 'token belongs to a different user' do + let_it_be(:token) { create(:personal_access_token) } + + it_behaves_like 'an unsuccessfully revoked token' + end + + context 'token belongs to current_user' do + let_it_be(:token) { create(:personal_access_token, user: current_user) } + + it_behaves_like 'a successfully revoked token' + end + end + end +end