diff options
25 files changed, 301 insertions, 174 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d67137704dc..d0a0c7b2414 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -93,6 +93,8 @@ variables: # For the default QA image, we use $CI_COMMIT_SHA as tag since it's always available and we override it for specific workflow.rules (see above) QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_SHA}" + # Default latest tag for particular branch + QA_IMAGE_BRANCH: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_REF_SLUG}" # Preparing custom clone path to reduce space used by all random forks # on GitLab.com's Shared Runners. Our main forks - especially the security diff --git a/.gitlab/ci/build-images.gitlab-ci.yml b/.gitlab/ci/build-images.gitlab-ci.yml index 0169f017063..6a222d8937f 100644 --- a/.gitlab/ci/build-images.gitlab-ci.yml +++ b/.gitlab/ci/build-images.gitlab-ci.yml @@ -28,7 +28,8 @@ build-qa-image: script: - !reference [.base-image-build, script] - echo $QA_IMAGE - - /kaniko/executor --context=${CI_PROJECT_DIR} --dockerfile=${CI_PROJECT_DIR}/qa/Dockerfile --destination=${QA_IMAGE} --cache=true + - echo $QA_IMAGE_BRANCH + - /kaniko/executor --context=${CI_PROJECT_DIR} --dockerfile=${CI_PROJECT_DIR}/qa/Dockerfile --destination=${QA_IMAGE} --destination=${QA_IMAGE_BRANCH} --cache=true # This image is used by: # - The `CNG` pipelines (via the `review-build-cng` job): https://gitlab.com/gitlab-org/build/CNG/-/blob/cfc67136d711e1c8c409bf8e57427a644393da2f/.gitlab-ci.yml#L335 diff --git a/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue b/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue index 280c222c380..0b748f18cb2 100644 --- a/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue +++ b/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue @@ -24,6 +24,7 @@ export default { }, inject: [ 'webauthnEnabled', + 'isCurrentPasswordRequired', 'profileTwoFactorAuthPath', 'profileTwoFactorAuthMethod', 'codesProfileTwoFactorAuthPath', @@ -64,7 +65,11 @@ export default { <input type="hidden" name="_method" data-testid="test-2fa-method-field" :value="method" /> <input :value="$options.csrf.token" type="hidden" name="authenticity_token" /> - <gl-form-group :label="$options.i18n.currentPassword" label-for="current-password"> + <gl-form-group + v-if="isCurrentPasswordRequired" + :label="$options.i18n.currentPassword" + label-for="current-password" + > <gl-form-input id="current-password" type="password" diff --git a/app/assets/javascripts/authentication/two_factor_auth/index.js b/app/assets/javascripts/authentication/two_factor_auth/index.js index f663c0705e6..7d21c19ac4c 100644 --- a/app/assets/javascripts/authentication/two_factor_auth/index.js +++ b/app/assets/javascripts/authentication/two_factor_auth/index.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; import { updateHistory, removeParams } from '~/lib/utils/url_utility'; import ManageTwoFactorForm from './components/manage_two_factor_form.vue'; import RecoveryCodes from './components/recovery_codes.vue'; @@ -13,16 +14,20 @@ export const initManageTwoFactorForm = () => { const { webauthnEnabled = false, + currentPasswordRequired, profileTwoFactorAuthPath = '', profileTwoFactorAuthMethod = '', codesProfileTwoFactorAuthPath = '', codesProfileTwoFactorAuthMethod = '', } = el.dataset; + const isCurrentPasswordRequired = parseBoolean(currentPasswordRequired); + return new Vue({ el, provide: { webauthnEnabled, + isCurrentPasswordRequired, profileTwoFactorAuthPath, profileTwoFactorAuthMethod, codesProfileTwoFactorAuthPath, diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 3e397684ffe..e0b5d6be155 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -3,7 +3,9 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController skip_before_action :check_two_factor_requirement before_action :ensure_verified_primary_email, only: [:show, :create] - before_action :validate_current_password, only: [:create, :codes, :destroy] + before_action :validate_current_password, only: [:create, :codes, :destroy], if: :current_password_required? + + helper_method :current_password_required? before_action do push_frontend_feature_flag(:webauthn) @@ -144,6 +146,10 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController redirect_to profile_two_factor_auth_path, alert: _('You must provide a valid current password') end + def current_password_required? + !current_user.password_automatically_set? + end + def build_qr_code uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host) RQRCode.render_qrcode(uri, :svg, level: :m, unit: 3) diff --git a/app/controllers/projects/cluster_agents_controller.rb b/app/controllers/projects/cluster_agents_controller.rb new file mode 100644 index 00000000000..e7fbe93131d --- /dev/null +++ b/app/controllers/projects/cluster_agents_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Projects::ClusterAgentsController < Projects::ApplicationController + before_action :authorize_can_read_cluster_agent! + + feature_category :kubernetes_management + + def show + @agent_name = params[:name] + end + + private + + def authorize_can_read_cluster_agent! + return if can?(current_user, :admin_cluster, project) + + access_denied! + end +end diff --git a/app/helpers/projects/cluster_agents_helper.rb b/app/helpers/projects/cluster_agents_helper.rb new file mode 100644 index 00000000000..20fa721cc3b --- /dev/null +++ b/app/helpers/projects/cluster_agents_helper.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Projects::ClusterAgentsHelper + def js_cluster_agent_details_data(agent_name, project) + { + agent_name: agent_name, + project_path: project.full_path + } + end +end diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index d1d6b6301b8..bd3cb7e60f0 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -17,7 +17,7 @@ = _("You've already enabled two-factor authentication using one time password authenticators. In order to register a different device, you must first disable two-factor authentication.") %p = _('If you lose your recovery codes you can generate new ones, invalidating all previous codes.') - .js-manage-two-factor-form{ data: { webauthn_enabled: webauthn_enabled, profile_two_factor_auth_path: profile_two_factor_auth_path, profile_two_factor_auth_method: 'delete', codes_profile_two_factor_auth_path: codes_profile_two_factor_auth_path, codes_profile_two_factor_auth_method: 'post' } } + .js-manage-two-factor-form{ data: { webauthn_enabled: webauthn_enabled, current_password_required: current_password_required?.to_s, profile_two_factor_auth_path: profile_two_factor_auth_path, profile_two_factor_auth_method: 'delete', codes_profile_two_factor_auth_path: codes_profile_two_factor_auth_path, codes_profile_two_factor_auth_method: 'post' } } - else %p @@ -47,11 +47,12 @@ .form-group = label_tag :pin_code, _('Pin code'), class: "label-bold" = text_field_tag :pin_code, nil, class: "form-control gl-form-input", required: true, data: { qa_selector: 'pin_code_field' } - .form-group - = label_tag :current_password, _('Current password'), class: 'label-bold' - = password_field_tag :current_password, nil, required: true, class: 'form-control gl-form-input', data: { qa_selector: 'current_password_field' } - %p.form-text.text-muted - = _('Your current password is required to register a two-factor authenticator app.') + - if current_password_required? + .form-group + = label_tag :current_password, _('Current password'), class: 'label-bold' + = password_field_tag :current_password, nil, required: true, class: 'form-control gl-form-input', data: { qa_selector: 'current_password_field' } + %p.form-text.text-muted + = _('Your current password is required to register a two-factor authenticator app.') .gl-mt-3 = submit_tag _('Register with two-factor app'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'register_2fa_app_button' } diff --git a/app/views/projects/cluster_agents/show.html.haml b/app/views/projects/cluster_agents/show.html.haml new file mode 100644 index 00000000000..a2d3426d99c --- /dev/null +++ b/app/views/projects/cluster_agents/show.html.haml @@ -0,0 +1,4 @@ +- add_to_breadcrumbs _('Kubernetes'), project_clusters_path(@project) +- page_title @agent_name + +#js-cluster-agent-details{ data: js_cluster_agent_details_data(@agent_name, @project) } diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md index 1cb37e3d650..91b8bcfd337 100644 --- a/doc/ci/jobs/ci_job_token.md +++ b/doc/ci/jobs/ci_job_token.md @@ -23,8 +23,8 @@ You can use a GitLab CI/CD job token to authenticate with specific API endpoints - [Release creation](../../api/releases/index.md#create-a-release). - [Terraform plan](../../user/infrastructure/index.md). -The token has the same permissions to access the API as the user that triggers the -pipeline. Therefore, this user must be assigned to [a role that has the required privileges](../../user/permissions.md#gitlab-cicd-permissions). +The token has the same permissions to access the API as the user that executes the +job. Therefore, this user must be assigned to [a role that has the required privileges](../../user/permissions.md#gitlab-cicd-permissions). The token is valid only while the pipeline job runs. After the job finishes, you can't use the token anymore. @@ -89,7 +89,7 @@ to make an API request to a private project `B`, then `B` must be added to the a If project `B` is public or internal, it doesn't need to be added to the allowlist. The job token scope is only for controlling access to private projects. -To enable and configure the job token scope limit: +### Configure the job token scope limit 1. On the top bar, select **Menu > Projects** and find your project. 1. On the left sidebar, select **Settings > CI/CD**. @@ -162,3 +162,50 @@ build_submodule: ``` Read more about the [jobs artifacts API](../../api/job_artifacts.md#download-the-artifacts-archive). + +## Troubleshooting + +CI job token failures are usually shown as responses like `404 Not Found` or similar: + +- Unauthorized Git clone: + + ```plaintext + $ git clone https://gitlab-ci-token:$CI_JOB_TOKEN@gitlab.com/fabiopitino/test2.git + + Cloning into 'test2'... + remote: The project you were looking for could not be found or you don't have permission to view it. + fatal: repository 'https://gitlab-ci-token:[MASKED]@gitlab.com/<namespace>/<project>.git/' not found + ``` + +- Unauthorized package download: + + ```plaintext + $ wget --header="JOB-TOKEN: $CI_JOB_TOKEN" ${CI_API_V4_URL}/projects/1234/packages/generic/my_package/0.0.1/file.txt + + --2021-09-23 11:00:13-- https://gitlab.com/api/v4/projects/1234/packages/generic/my_package/0.0.1/file.txt + Resolving gitlab.com (gitlab.com)... 172.65.251.78, 2606:4700:90:0:f22e:fbec:5bed:a9b9 + Connecting to gitlab.com (gitlab.com)|172.65.251.78|:443... connected. + HTTP request sent, awaiting response... 404 Not Found + 2021-09-23 11:00:13 ERROR 404: Not Found. + ``` + +- Unauthorized API request: + + ```plaintext + $ curl --verbose --request POST --form "token=$CI_JOB_TOKEN" --form ref=master "https://gitlab.com/api/v4/projects/1234/trigger/pipeline" + + < HTTP/2 404 + < date: Thu, 23 Sep 2021 11:00:12 GMT + {"message":"404 Not Found"} + < content-type: application/json + ``` + +While troubleshooting CI/CD job token authentication issues, be aware that: + +- When the [CI/CD job token limit](#limit-gitlab-cicd-job-token-access) is enabled, + and the job token is being used to access a different project: + - The user that executes the job must be a member of the project that is being accessed. + - The user must have the [permissions](../../user/permissions.md) to perform the action. + - The target project must be [allowlisted for the job token scope limit](#configure-the-job-token-scope-limit). +- The CI job token becomes invalid if the job is no longer running, has been erased, + or if the project is in the process of being deleted. diff --git a/doc/ci/pipelines/img/multi_project_pipeline_graph.png b/doc/ci/pipelines/img/multi_project_pipeline_graph.png Binary files differdeleted file mode 100644 index 723a455cb4a..00000000000 --- a/doc/ci/pipelines/img/multi_project_pipeline_graph.png +++ /dev/null diff --git a/doc/ci/pipelines/img/multi_project_pipeline_graph_v14_3.png b/doc/ci/pipelines/img/multi_project_pipeline_graph_v14_3.png Binary files differnew file mode 100644 index 00000000000..aadf8bb0979 --- /dev/null +++ b/doc/ci/pipelines/img/multi_project_pipeline_graph_v14_3.png diff --git a/doc/ci/pipelines/img/parent_pipeline_graph_expanded_v12_6.png b/doc/ci/pipelines/img/parent_pipeline_graph_expanded_v12_6.png Binary files differdeleted file mode 100644 index db18cc201fc..00000000000 --- a/doc/ci/pipelines/img/parent_pipeline_graph_expanded_v12_6.png +++ /dev/null diff --git a/doc/ci/pipelines/img/parent_pipeline_graph_expanded_v14_3.png b/doc/ci/pipelines/img/parent_pipeline_graph_expanded_v14_3.png Binary files differnew file mode 100644 index 00000000000..206e4eeec05 --- /dev/null +++ b/doc/ci/pipelines/img/parent_pipeline_graph_expanded_v14_3.png diff --git a/doc/ci/pipelines/multi_project_pipelines.md b/doc/ci/pipelines/multi_project_pipelines.md index 8390e85d57b..afb0a75e504 100644 --- a/doc/ci/pipelines/multi_project_pipelines.md +++ b/doc/ci/pipelines/multi_project_pipelines.md @@ -321,7 +321,7 @@ downstream projects. On self-managed instances, an administrator can change this When you configure GitLab CI/CD for your project, you can visualize the stages of your [jobs](index.md#configure-a-pipeline) on a [pipeline graph](index.md#visualize-pipelines). -![Multi-project pipeline graph](img/multi_project_pipeline_graph.png) +![Multi-project pipeline graph](img/multi_project_pipeline_graph_v14_3.png) In the merge request, on the **Pipelines** tab, multi-project pipeline mini-graphs are displayed. They expand and are shown adjacent to each other when hovering (or tapping on touchscreen devices). diff --git a/doc/ci/pipelines/parent_child_pipelines.md b/doc/ci/pipelines/parent_child_pipelines.md index 71f778d81b3..46a4ff775c4 100644 --- a/doc/ci/pipelines/parent_child_pipelines.md +++ b/doc/ci/pipelines/parent_child_pipelines.md @@ -23,7 +23,7 @@ Additionally, sometimes the behavior of a pipeline needs to be more dynamic. The to choose to start sub-pipelines (or not) is a powerful ability, especially if the YAML is dynamically generated. -![Parent pipeline graph expanded](img/parent_pipeline_graph_expanded_v12_6.png) +![Parent pipeline graph expanded](img/parent_pipeline_graph_expanded_v14_3.png) Similarly to [multi-project pipelines](multi_project_pipelines.md), a pipeline can trigger a set of concurrently running child pipelines, but within the same project: diff --git a/doc/ssh/index.md b/doc/ssh/index.md index 94c157697ce..6f886309640 100644 --- a/doc/ssh/index.md +++ b/doc/ssh/index.md @@ -5,7 +5,7 @@ info: "To determine the technical writer assigned to the Stage/Group associated type: howto, reference --- -# GitLab and SSH keys +# GitLab and SSH keys **(FREE)** Git is a distributed version control system, which means you can work locally, then share or "push" your changes to a server. In this case, the server is GitLab. @@ -213,7 +213,7 @@ To use SSH with GitLab, copy your public key to your GitLab account. which starts with `ssh-ed25519` or `ssh-rsa`, and may end with a comment. 1. In the **Title** box, type a description, like `Work Laptop` or `Home Workstation`. -1. Optional. In the **Expires at** box, select an expiration date. (Introduced in [GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/36243).) +1. Optional. In the **Expires at** box, select an expiration date. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/36243) in GitLab 12.9.) In: - GitLab 13.12 and earlier, the expiration date is informational only. It doesn't prevent you from using the key. Administrators can view expiration dates and use them for diff --git a/package.json b/package.json index b55981ce95e..0ebe0245078 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "codesandbox-api": "0.0.23", "compression-webpack-plugin": "^5.0.2", "copy-webpack-plugin": "^6.4.1", - "core-js": "^3.18.1", + "core-js": "^3.18.2", "cron-validator": "^1.1.1", "cropper": "^2.3.0", "css-loader": "^2.1.1", diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb index 0af04e58903..e57bd5be937 100644 --- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb +++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb @@ -31,11 +31,12 @@ RSpec.describe Profiles::TwoFactorAuthsController do shared_examples 'user must enter a valid current password' do let(:current_password) { '123' } + let(:redirect_path) { profile_two_factor_auth_path } it 'requires the current password', :aggregate_failures do go - expect(response).to redirect_to(profile_two_factor_auth_path) + expect(response).to redirect_to(redirect_path) expect(flash[:alert]).to eq(_('You must provide a valid current password')) end @@ -48,6 +49,19 @@ RSpec.describe Profiles::TwoFactorAuthsController do expect(user.reload).to be_access_locked end end + + context 'when user authenticates with an external service' do + before do + allow(user).to receive(:password_automatically_set?).and_return(true) + end + + it 'does not require the current password', :aggregate_failures do + go + + expect(response).not_to redirect_to(redirect_path) + expect(flash[:alert]).to be_nil + end + end end describe 'GET show' do @@ -188,7 +202,9 @@ RSpec.describe Profiles::TwoFactorAuthsController do end describe 'DELETE destroy' do - subject { delete :destroy, params: { current_password: current_password } } + def go + delete :destroy, params: { current_password: current_password } + end let(:current_password) { user.password } @@ -196,40 +212,38 @@ RSpec.describe Profiles::TwoFactorAuthsController do let_it_be_with_reload(:user) { create(:user, :two_factor) } it 'disables two factor' do - subject + go expect(user.reload.two_factor_enabled?).to eq(false) end it 'redirects to profile_account_path' do - subject + go expect(response).to redirect_to(profile_account_path) end it 'displays a notice on success' do - subject + go expect(flash[:notice]) .to eq _('Two-factor authentication has been disabled successfully!') end - it_behaves_like 'user must enter a valid current password' do - let(:go) { delete :destroy, params: { current_password: current_password } } - end + it_behaves_like 'user must enter a valid current password' end context 'for a user that does not have 2FA enabled' do let_it_be_with_reload(:user) { create(:user) } it 'redirects to profile_account_path' do - subject + go expect(response).to redirect_to(profile_account_path) end it 'displays an alert on failure' do - subject + go expect(flash[:alert]) .to eq _('Two-factor authentication is not enabled for this user') diff --git a/spec/features/profiles/two_factor_auths_spec.rb b/spec/features/profiles/two_factor_auths_spec.rb index e1feca5031a..3f5789e119a 100644 --- a/spec/features/profiles/two_factor_auths_spec.rb +++ b/spec/features/profiles/two_factor_auths_spec.rb @@ -5,20 +5,16 @@ require 'spec_helper' RSpec.describe 'Two factor auths' do context 'when signed in' do before do - allow(Gitlab).to receive(:com?) { true } + sign_in(user) end context 'when user has two-factor authentication disabled' do - let(:user) { create(:user ) } - - before do - sign_in(user) - end + let_it_be(:user) { create(:user ) } it 'requires the current password to set up two factor authentication', :js do visit profile_two_factor_auth_path - register_2fa(user.reload.current_otp, '123') + register_2fa(user.current_otp, '123') expect(page).to have_content('You must provide a valid current password') @@ -31,14 +27,28 @@ RSpec.describe 'Two factor auths' do expect(page).to have_content('Status: Enabled') end - end - context 'when user has two-factor authentication enabled' do - let(:user) { create(:user, :two_factor) } + context 'when user authenticates with an external service' do + let_it_be(:user) { create(:omniauth_user) } + + it 'does not require the current password to set up two factor authentication', :js do + visit profile_two_factor_auth_path - before do - sign_in(user) + fill_in 'pin_code', with: user.current_otp + click_button 'Register with two-factor app' + + expect(page).to have_content('Please copy, download, or print your recovery codes before proceeding.') + + click_button 'Copy codes' + click_link 'Proceed' + + expect(page).to have_content('Status: Enabled') + end end + end + + context 'when user has two-factor authentication enabled' do + let_it_be(:user) { create(:user, :two_factor) } it 'requires the current_password to disable two-factor authentication', :js do visit profile_two_factor_auth_path @@ -61,7 +71,7 @@ RSpec.describe 'Two factor auths' do expect(page).to have_content('Enable two-factor authentication') end - it 'requires the current_password to regernate recovery codes', :js do + it 'requires the current_password to regenerate recovery codes', :js do visit profile_two_factor_auth_path fill_in 'current_password', with: '123' @@ -76,6 +86,29 @@ RSpec.describe 'Two factor auths' do expect(page).to have_content('Please copy, download, or print your recovery codes before proceeding.') end + + context 'when user authenticates with an external service' do + let_it_be(:user) { create(:omniauth_user, :two_factor) } + + it 'does not require the current_password to disable two-factor authentication', :js do + visit profile_two_factor_auth_path + + click_button 'Disable two-factor authentication' + + page.accept_alert + + expect(page).to have_content('Two-factor authentication has been disabled successfully!') + expect(page).to have_content('Enable two-factor authentication') + end + + it 'does not require the current_password to regenerate recovery codes', :js do + visit profile_two_factor_auth_path + + click_button 'Regenerate recovery codes' + + expect(page).to have_content('Please copy, download, or print your recovery codes before proceeding.') + end + end end def register_2fa(pin, password) diff --git a/spec/frontend/authentication/two_factor_auth/components/__snapshots__/manage_two_factor_form_spec.js.snap b/spec/frontend/authentication/two_factor_auth/components/__snapshots__/manage_two_factor_form_spec.js.snap deleted file mode 100644 index 3fe0e570a54..00000000000 --- a/spec/frontend/authentication/two_factor_auth/components/__snapshots__/manage_two_factor_form_spec.js.snap +++ /dev/null @@ -1,99 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ManageTwoFactorForm Disable button renders the component correctly 1`] = ` -VueWrapper { - "_emitted": Object {}, - "_emittedByOrder": Array [], - "isFunctionalComponent": undefined, -} -`; - -exports[`ManageTwoFactorForm Disable button renders the component correctly 2`] = ` -<form - action="#" - class="gl-display-inline-block" - method="post" -> - <input - data-testid="test-2fa-method-field" - name="_method" - type="hidden" - /> - - <input - name="authenticity_token" - type="hidden" - /> - - <div - class="form-group gl-form-group" - id="__BVID__15" - role="group" - > - <label - class="d-block col-form-label" - for="current-password" - id="__BVID__15__BV_label_" - > - Current password - </label> - <div - class="bv-no-focus-ring" - > - <input - aria-required="true" - class="gl-form-input form-control" - data-qa-selector="current_password_field" - id="current-password" - name="current_password" - required="required" - type="password" - /> - <!----> - <!----> - <!----> - </div> - </div> - - <button - class="btn btn-danger gl-mr-3 gl-display-inline-block btn-danger btn-md gl-button" - data-confirm="Are you sure? This will invalidate your registered applications and U2F devices." - data-form-action="2fa_auth_path" - data-form-method="2fa_auth_method" - data-testid="test-2fa-disable-button" - type="submit" - > - <!----> - - <!----> - - <span - class="gl-button-text" - > - - Disable two-factor authentication - - </span> - </button> - - <button - class="btn gl-display-inline-block btn-default btn-md gl-button" - data-form-action="2fa_codes_path" - data-form-method="2fa_codes_method" - data-testid="test-2fa-regenerate-codes-button" - type="submit" - > - <!----> - - <!----> - - <span - class="gl-button-text" - > - - Regenerate recovery codes - - </span> - </button> -</form> -`; diff --git a/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js index 384579c6876..870375318e3 100644 --- a/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js +++ b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js @@ -1,10 +1,18 @@ import { within } from '@testing-library/dom'; +import { GlForm } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import ManageTwoFactorForm, { i18n, } from '~/authentication/two_factor_auth/components/manage_two_factor_form.vue'; +const defaultProvide = { + profileTwoFactorAuthPath: '2fa_auth_path', + profileTwoFactorAuthMethod: '2fa_auth_method', + codesProfileTwoFactorAuthPath: '2fa_codes_path', + codesProfileTwoFactorAuthMethod: '2fa_codes_method', +}; + describe('ManageTwoFactorForm', () => { let wrapper; @@ -12,11 +20,9 @@ describe('ManageTwoFactorForm', () => { wrapper = extendedWrapper( mount(ManageTwoFactorForm, { provide: { - webauthnEnabled: options?.webauthnEnabled || false, - profileTwoFactorAuthPath: '2fa_auth_path', - profileTwoFactorAuthMethod: '2fa_auth_method', - codesProfileTwoFactorAuthPath: '2fa_codes_path', - codesProfileTwoFactorAuthMethod: '2fa_codes_method', + ...defaultProvide, + webauthnEnabled: options?.webauthnEnabled ?? false, + isCurrentPasswordRequired: options?.currentPasswordRequired ?? true, }, }), ); @@ -26,6 +32,11 @@ describe('ManageTwoFactorForm', () => { const queryByLabelText = (text, options) => within(wrapper.element).queryByLabelText(text, options); + const findForm = () => wrapper.findComponent(GlForm); + const findMethodInput = () => wrapper.findByTestId('test-2fa-method-field'); + const findDisableButton = () => wrapper.findByTestId('test-2fa-disable-button'); + const findRegenerateCodesButton = () => wrapper.findByTestId('test-2fa-regenerate-codes-button'); + beforeEach(() => { createComponent(); }); @@ -36,16 +47,30 @@ describe('ManageTwoFactorForm', () => { }); }); + describe('when current password is not required', () => { + beforeEach(() => { + createComponent({ + currentPasswordRequired: false, + }); + }); + + it('does not render the current password field', () => { + expect(queryByLabelText(i18n.currentPassword)).toBe(null); + }); + }); + describe('Disable button', () => { - it('renders the component correctly', () => { - expect(wrapper).toMatchSnapshot(); - expect(wrapper.element).toMatchSnapshot(); + it('renders the component with correct attributes', () => { + expect(findDisableButton().exists()).toBe(true); + expect(findDisableButton().attributes()).toMatchObject({ + 'data-confirm': i18n.confirm, + 'data-form-action': defaultProvide.profileTwoFactorAuthPath, + 'data-form-method': defaultProvide.profileTwoFactorAuthMethod, + }); }); it('has the right confirm text', () => { - expect(wrapper.findByTestId('test-2fa-disable-button').element.dataset.confirm).toEqual( - i18n.confirm, - ); + expect(findDisableButton().attributes('data-confirm')).toBe(i18n.confirm); }); describe('when webauthnEnabled', () => { @@ -56,23 +81,19 @@ describe('ManageTwoFactorForm', () => { }); it('has the right confirm text', () => { - expect(wrapper.findByTestId('test-2fa-disable-button').element.dataset.confirm).toEqual( - i18n.confirmWebAuthn, - ); + expect(findDisableButton().attributes('data-confirm')).toBe(i18n.confirmWebAuthn); }); }); it('modifies the form action and method when submitted through the button', async () => { - const form = wrapper.find('form'); - const disableButton = wrapper.findByTestId('test-2fa-disable-button').element; - const methodInput = wrapper.findByTestId('test-2fa-method-field').element; + const form = findForm(); + const disableButton = findDisableButton().element; + const methodInput = findMethodInput(); - form.trigger('submit', { submitter: disableButton }); + await form.vm.$emit('submit', { submitter: disableButton }); - await wrapper.vm.$nextTick(); - - expect(form.element.getAttribute('action')).toEqual('2fa_auth_path'); - expect(methodInput.getAttribute('value')).toEqual('2fa_auth_method'); + expect(form.attributes('action')).toBe(defaultProvide.profileTwoFactorAuthPath); + expect(methodInput.attributes('value')).toBe(defaultProvide.profileTwoFactorAuthMethod); }); }); @@ -82,17 +103,14 @@ describe('ManageTwoFactorForm', () => { }); it('modifies the form action and method when submitted through the button', async () => { - const form = wrapper.find('form'); - const regenerateCodesButton = wrapper.findByTestId('test-2fa-regenerate-codes-button') - .element; - const methodInput = wrapper.findByTestId('test-2fa-method-field').element; - - form.trigger('submit', { submitter: regenerateCodesButton }); + const form = findForm(); + const regenerateCodesButton = findRegenerateCodesButton().element; + const methodInput = findMethodInput(); - await wrapper.vm.$nextTick(); + await form.vm.$emit('submit', { submitter: regenerateCodesButton }); - expect(form.element.getAttribute('action')).toEqual('2fa_codes_path'); - expect(methodInput.getAttribute('value')).toEqual('2fa_codes_method'); + expect(form.attributes('action')).toBe(defaultProvide.codesProfileTwoFactorAuthPath); + expect(methodInput.attributes('value')).toBe(defaultProvide.codesProfileTwoFactorAuthMethod); }); }); }); diff --git a/spec/helpers/projects/cluster_agents_helper_spec.rb b/spec/helpers/projects/cluster_agents_helper_spec.rb new file mode 100644 index 00000000000..2935a74586b --- /dev/null +++ b/spec/helpers/projects/cluster_agents_helper_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::ClusterAgentsHelper do + describe '#js_cluster_agent_details_data' do + let_it_be(:project) { create(:project) } + + let(:agent_name) { 'agent-name' } + + subject { helper.js_cluster_agent_details_data(agent_name, project) } + + it 'returns name' do + expect(subject[:agent_name]).to eq(agent_name) + end + + it 'returns project path' do + expect(subject[:project_path]).to eq(project.full_path) + end + end +end diff --git a/spec/requests/projects/cluster_agents_controller_spec.rb b/spec/requests/projects/cluster_agents_controller_spec.rb new file mode 100644 index 00000000000..e4c4f537699 --- /dev/null +++ b/spec/requests/projects/cluster_agents_controller_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::ClusterAgentsController do + let_it_be(:cluster_agent) { create(:cluster_agent) } + + let(:project) { cluster_agent.project } + + describe 'GET #show' do + subject { get project_cluster_agent_path(project, cluster_agent.name) } + + context 'when user is unauthorized' do + let_it_be(:user) { create(:user) } + + before do + project.add_developer(user) + sign_in(user) + subject + end + + it 'shows 404' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when user is authorized' do + let(:user) { project.creator } + + before do + sign_in(user) + subject + end + + it 'renders content' do + expect(response).to be_successful + end + end + end +end diff --git a/yarn.lock b/yarn.lock index dd43540d846..0214d4be47b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3579,10 +3579,10 @@ core-js-pure@^3.0.0: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== -core-js@^3.18.1: - version "3.18.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.18.1.tgz#289d4be2ce0085d40fc1244c0b1a54c00454622f" - integrity sha512-vJlUi/7YdlCZeL6fXvWNaLUPh/id12WXj3MbkMw5uOyF0PfWPBNOCNbs53YqgrvtujLNlt9JQpruyIKkUZ+PKA== +core-js@^3.18.2: + version "3.18.2" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.18.2.tgz#63a551e8a29f305cd4123754846e65896619ba5b" + integrity sha512-zNhPOUoSgoizoSQFdX1MeZO16ORRb9FFQLts8gSYbZU5FcgXhp24iMWMxnOQo5uIaIG7/6FA/IqJPwev1o9ZXQ== core-js@~2.3.0: version "2.3.0" |