Commit 09a529ad authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 8dad2b39 f27efb8a
......@@ -61,7 +61,13 @@ export default {
},
},
methods: {
...mapActions(['setOverride', 'setIsSaving', 'setIsTesting', 'setIsResetting']),
...mapActions([
'setOverride',
'setIsSaving',
'setIsTesting',
'setIsResetting',
'fetchResetIntegration',
]),
onSaveClick() {
this.setIsSaving(true);
eventHub.$emit('saveIntegration');
......@@ -70,7 +76,9 @@ export default {
this.setIsTesting(true);
eventHub.$emit('testIntegration');
},
onResetClick() {},
onResetClick() {
this.fetchResetIntegration();
},
},
};
</script>
......
import axios from 'axios';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import * as types from './mutation_types';
export const setOverride = ({ commit }, override) => commit(types.SET_OVERRIDE, override);
......@@ -5,3 +7,22 @@ export const setIsSaving = ({ commit }, isSaving) => commit(types.SET_IS_SAVING,
export const setIsTesting = ({ commit }, isTesting) => commit(types.SET_IS_TESTING, isTesting);
export const setIsResetting = ({ commit }, isResetting) =>
commit(types.SET_IS_RESETTING, isResetting);
export const requestResetIntegration = ({ commit }) => {
commit(types.REQUEST_RESET_INTEGRATION);
};
export const receiveResetIntegrationSuccess = () => {
refreshCurrentPage();
};
export const receiveResetIntegrationError = ({ commit }) => {
commit(types.RECEIVE_RESET_INTEGRATION_ERROR);
};
export const fetchResetIntegration = ({ dispatch, getters }) => {
dispatch('requestResetIntegration');
return axios
.post(getters.propsSource.resetPath, { params: { format: 'json' } })
.then(() => dispatch('receiveResetIntegrationSuccess'))
.catch(() => dispatch('receiveResetIntegrationError'));
};
......@@ -2,3 +2,6 @@ export const SET_OVERRIDE = 'SET_OVERRIDE';
export const SET_IS_SAVING = 'SET_IS_SAVING';
export const SET_IS_TESTING = 'SET_IS_TESTING';
export const SET_IS_RESETTING = 'SET_IS_RESETTING';
export const REQUEST_RESET_INTEGRATION = 'REQUEST_RESET_INTEGRATION';
export const RECEIVE_RESET_INTEGRATION_ERROR = 'RECEIVE_RESET_INTEGRATION_ERROR';
......@@ -13,4 +13,10 @@ export default {
[types.SET_IS_RESETTING](state, isResetting) {
state.isResetting = isResetting;
},
[types.REQUEST_RESET_INTEGRATION](state) {
state.isResetting = true;
},
[types.RECEIVE_RESET_INTEGRATION_ERROR](state) {
state.isResetting = false;
},
};
import { unwrapStagesWithNeeds } from '../unwrapping_utils';
const addMulti = (mainId, pipeline) => {
return { ...pipeline, multiproject: mainId !== pipeline.id };
};
const unwrapPipelineData = (mainPipelineId, data) => {
if (!data?.project?.pipeline) {
return null;
......@@ -10,35 +16,13 @@ const unwrapPipelineData = (mainPipelineId, data) => {
stages: { nodes: stages },
} = data.project.pipeline;
const unwrappedNestedGroups = stages.map(stage => {
const {
groups: { nodes: groups },
} = stage;
return { ...stage, groups };
});
const nodes = unwrappedNestedGroups.map(({ name, status, groups }) => {
const groupsWithJobs = groups.map(group => {
const jobs = group.jobs.nodes.map(job => {
const { needs } = job;
return { ...job, needs: needs.nodes.map(need => need.name) };
});
return { ...group, jobs };
});
return { name, status, groups: groupsWithJobs };
});
const addMulti = pipeline => {
return { ...pipeline, multiproject: mainPipelineId !== pipeline.id };
};
const nodes = unwrapStagesWithNeeds(stages);
return {
id,
stages: nodes,
upstream: upstream ? [upstream].map(addMulti) : [],
downstream: downstream ? downstream.map(addMulti) : [],
upstream: upstream ? [upstream].map(addMulti.bind(null, mainPipelineId)) : [],
downstream: downstream ? downstream.map(addMulti.bind(null, mainPipelineId)) : [],
};
};
......
const unwrapGroups = stages => {
return stages.map(stage => {
const {
groups: { nodes: groups },
} = stage;
return { ...stage, groups };
});
};
const unwrapNodesWithName = (jobArray, prop, field = 'name') => {
return jobArray.map(job => {
return { ...job, [prop]: job[prop].nodes.map(item => item[field]) };
});
};
const unwrapJobWithNeeds = denodedJobArray => {
return unwrapNodesWithName(denodedJobArray, 'needs');
};
const unwrapStagesWithNeeds = denodedStages => {
const unwrappedNestedGroups = unwrapGroups(denodedStages);
const nodes = unwrappedNestedGroups.map(node => {
const { groups } = node;
const groupsWithJobs = groups.map(group => {
const jobs = unwrapJobWithNeeds(group.jobs.nodes);
return { ...group, jobs };
});
return { ...node, groups: groupsWithJobs };
});
return nodes;
};
export { unwrapGroups, unwrapNodesWithName, unwrapJobWithNeeds, unwrapStagesWithNeeds };
......@@ -72,6 +72,16 @@ class Admin::UsersController < Admin::ApplicationController
end
end
def reject
result = Users::RejectService.new(current_user).execute(user)
if result[:status] == :success
redirect_to admin_users_path, status: :found, notice: _("You've rejected %{user}" % { user: user.name })
else
redirect_back_or_admin_user(alert: result[:message])
end
end
def activate
return redirect_back_or_admin_user(notice: _("Error occurred. A blocked user must be unblocked to be activated")) if user.blocked?
......
......@@ -43,6 +43,12 @@ module IntegrationsActions
render json: {}, status: :ok
end
def reset
flash[:notice] = s_('Integrations|This integration, and inheriting projects were reset.')
render json: {}, status: :ok
end
private
def integrations_enabled?
......
......@@ -75,7 +75,15 @@ module ServicesHelper
end
end
def integration_form_data(integration)
def scoped_reset_integration_path(integration, group: nil)
if group.present?
reset_group_settings_integration_path(group, integration)
else
reset_admin_application_settings_integration_path(integration)
end
end
def integration_form_data(integration, group: nil)
{
id: integration.id,
show_active: integration.show_active_box?.to_s,
......@@ -94,7 +102,7 @@ module ServicesHelper
cancel_path: scoped_integrations_path,
can_test: integration.can_test?.to_s,
test_path: scoped_test_integration_path(integration),
reset_path: ''
reset_path: reset_integrations?(group: group) ? scoped_reset_integration_path(integration, group: group) : ''
}
end
......@@ -122,6 +130,10 @@ module ServicesHelper
!Gitlab.com?
end
def reset_integrations?(group: nil)
Feature.enabled?(:reset_integrations, group, type: :development)
end
extend self
private
......
......@@ -18,6 +18,14 @@ module Emails
subject: subject(_("GitLab Account Request")))
end
def user_admin_rejection_email(name, email)
@name = name
profile_email_with_layout(
to: email,
subject: subject(_("GitLab account request rejected")))
end
# rubocop: disable CodeReuse/ActiveRecord
def new_ssh_key_email(key_id)
@key = Key.find_by(id: key_id)
......
......@@ -99,6 +99,7 @@ class GlobalPolicy < BasePolicy
enable :read_custom_attribute
enable :update_custom_attribute
enable :approve_user
enable :reject_user
end
# We can't use `read_statistics` because the user may have different permissions for different projects
......
......@@ -380,6 +380,10 @@ class NotificationService
end
end
def user_admin_rejection(name, email)
mailer.user_admin_rejection_email(name, email).deliver_later
end
# Members
def new_access_request(member)
return true unless member.notifiable?(:subscription)
......
# frozen_string_literal: true
module Users
class RejectService < BaseService
def initialize(current_user)
@current_user = current_user
end
def execute(user)
return error(_('You are not allowed to reject a user')) unless allowed?
return error(_('This user does not have a pending request')) unless user.blocked_pending_approval?
user.delete_async(deleted_by: current_user, params: { hard_delete: true })
NotificationService.new.user_admin_rejection(user.name, user.email)
success
end
private
attr_reader :current_user
def allowed?
can?(current_user, :reject_user)
end
end
end
.card.border-danger
.card-header.bg-danger.gl-text-white
= s_('AdminUsers|This user has requested access')
.card-body
= render partial: 'admin/users/user_reject_effects'
%br
= link_to s_('AdminUsers|Reject request'), reject_admin_user_path(user), method: :delete, class: "btn gl-button btn-danger", data: { confirm: s_('AdminUsers|Are you sure?') }
......@@ -37,8 +37,7 @@
- elsif user.blocked?
- if user.blocked_pending_approval?
= link_to s_('AdminUsers|Approve'), approve_admin_user_path(user), method: :put
%button.btn.btn-default-tertiary.js-confirm-modal-button{ data: user_block_data(user, user_block_effects) }
= s_('AdminUsers|Block')
= link_to s_('AdminUsers|Reject'), reject_admin_user_path(user), method: :delete
- else
%button.btn.btn-default-tertiary.js-confirm-modal-button{ data: user_unblock_data(user) }
= s_('AdminUsers|Unblock')
......@@ -56,7 +55,7 @@
- if user.access_locked?
%li
= link_to _('Unlock'), unlock_admin_user_path(user), method: :put, data: { confirm: _('Are you sure?') }
- if can?(current_user, :destroy_user, user)
- if can?(current_user, :destroy_user, user) && !user.blocked_pending_approval?
%li.divider
- if user.can_be_removed?
%li
......
%p
= s_('AdminUsers|Rejected users:')
%ul
%li
= s_('AdminUsers|Cannot sign in or access instance information')
%li
= s_('AdminUsers|Will be deleted')
%p
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path("user/profile/account/delete_account", anchor: "associated-records") }
= s_('AdminUsers|For more information, please refer to the %{link_start}user account deletion documentation.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
......@@ -172,7 +172,7 @@
- if @user.blocked?
- if @user.blocked_pending_approval?
= render 'admin/users/approve_user', user: @user
= render 'admin/users/block_user', user: @user
= render 'admin/users/reject_pending_user', user: @user
- else
.card.border-info
.card-header.gl-bg-blue-500.gl-text-white
......@@ -196,52 +196,52 @@
%p This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account.
%br
= link_to 'Unlock user', unlock_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?' }
.card.border-danger
.card-header.bg-danger.text-white
= s_('AdminUsers|Delete user')
.card-body
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p Deleting a user has the following effects:
= render 'users/deletion_guidance', user: @user
%br
%button.delete-user-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete',
delete_user_url: admin_user_path(@user),
block_user_url: block_admin_user_path(@user),
username: sanitize_name(@user.name) } }
= s_('AdminUsers|Delete user')
- else
- if @user.solo_owned_groups.present?
%p
This user is currently an owner in these groups:
%strong= @user.solo_owned_groups.map(&:name).join(', ')
- if !@user.blocked_pending_approval?
.card.border-danger
.card-header.bg-danger.text-white
= s_('AdminUsers|Delete user')
.card-body
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p Deleting a user has the following effects:
= render 'users/deletion_guidance', user: @user
%br
%button.delete-user-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete',
delete_user_url: admin_user_path(@user),
block_user_url: block_admin_user_path(@user),
username: sanitize_name(@user.name) } }
= s_('AdminUsers|Delete user')
- else
- if @user.solo_owned_groups.present?
%p
This user is currently an owner in these groups:
%strong= @user.solo_owned_groups.map(&:name).join(', ')
%p
You must transfer ownership or delete these groups before you can delete this user.
- else
%p
You don't have access to delete this user.
.card.border-danger
.card-header.bg-danger.text-white
= s_('AdminUsers|Delete user and contributions')
.card-body
- if can?(current_user, :destroy_user, @user)
%p
You must transfer ownership or delete these groups before you can delete this user.
This option deletes the user and any contributions that
would usually be moved to the
= succeed "." do
= link_to "system ghost user", help_page_path("user/profile/account/delete_account")
As well as the user's personal projects, groups owned solely by
the user, and projects in them, will also be removed. Commits
to other projects are unaffected.
%br
%button.delete-user-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete-with-contributions',
delete_user_url: admin_user_path(@user, hard_delete: true),
block_user_url: block_admin_user_path(@user),
username: @user.name } }
= s_('AdminUsers|Delete user and contributions')
- else
%p
You don't have access to delete this user.
.card.border-danger
.card-header.bg-danger.text-white
= s_('AdminUsers|Delete user and contributions')
.card-body
- if can?(current_user, :destroy_user, @user)
%p
This option deletes the user and any contributions that
would usually be moved to the
= succeed "." do
= link_to "system ghost user", help_page_path("user/profile/account/delete_account")
As well as the user's personal projects, groups owned solely by
the user, and projects in them, will also be removed. Commits
to other projects are unaffected.
%br
%button.delete-user-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete-with-contributions',
delete_user_url: admin_user_path(@user, hard_delete: true),
block_user_url: block_admin_user_path(@user),
username: @user.name } }
= s_('AdminUsers|Delete user and contributions')
- else
%p
You don't have access to delete this user.
= render partial: 'admin/users/modals'
= email_default_heading(_('Hello %{name},') % { name: @name })
%p
= _('Your request to join %{host} has been rejected.').html_safe % { host: link_to(root_url, root_url) }
%p
= _('Please contact your GitLab administrator if you think this is an error.')
<%= _('Hello %{name},') % { name: @name } %>
<%= _('Your request to join %{host} has been rejected.') % { host: root_url } %>
<%= _('Please contact your GitLab administrator if you think this is an error.') %>
......@@ -9,5 +9,5 @@
.service-settings
- if @default_integration
.js-vue-default-integration-settings{ data: integration_form_data(@default_integration) }
.js-vue-integration-settings{ data: integration_form_data(integration) }
.js-vue-default-integration-settings{ data: integration_form_data(@default_integration, group: @group) }
.js-vue-integration-settings{ data: integration_form_data(integration, group: @group) }
---
title: Add index for the `vulnerabilities` table on `project_id`, `state`, and `severity`
columns
merge_request: 48930
author:
type: added
---
title: Add migration to populate remaining dismissal information for vulnerabilities
merge_request: 48472
author:
type: added
---
title: Email user when registration request is rejected
merge_request: 48185
author:
type: added
---
name: reset_integrations
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47546
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/283875
milestone: '13.7'
type: development
group: group::ecosystem
default_enabled: false
......@@ -18,6 +18,7 @@ namespace :admin do
put :unlock
put :confirm
put :approve
delete :reject
post :impersonate
patch :disable_two_factor
delete 'remove/:email_id', action: 'remove_email', as: 'remove_email'
......@@ -126,6 +127,7 @@ namespace :admin do
resources :integrations, only: [:edit, :update] do
member do
put :test
post :reset
end
end
......
......@@ -46,6 +46,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resources :integrations, only: [:index, :edit, :update] do
member do
put :test
post :reset
end
end
end
......
# frozen_string_literal: true
class AddIndexVulnerabilitiesOnProjectIdAndStateAndSeverity < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_vulnerabilities_on_project_id_and_state_and_severity'
disable_ddl_transaction!
def up
add_concurrent_index :vulnerabilities, [:project_id, :state, :severity], name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :vulnerabilities, INDEX_NAME
end
end
# frozen_string_literal: true
class DropIndexVulnerabilitiesOnProjectId < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_vulnerabilities_on_project_id'
disable_ddl_transaction!
def up
remove_concurrent_index_by_name :vulnerabilities, INDEX_NAME
end
def down
add_concurrent_index :vulnerabilities, :project_id, name: INDEX_NAME
end
end
# frozen_string_literal: true
class PopulateRemainingMissingDismissalInformationForVulnerabilities < ActiveRecord::Migration[6.0]
DOWNTIME = false
disable_ddl_transaction!
def up
Gitlab::BackgroundMigration.steal('PopulateMissingVulnerabilityDismissalInformation')
::Gitlab::BackgroundMigration::PopulateMissingVulnerabilityDismissalInformation::Vulnerability.broken.each_batch(of: 100) do |batch, index|
vulnerability_ids = batch.pluck(:id)
::Gitlab::BackgroundMigration::PopulateMissingVulnerabilityDismissalInformation.new.perform(*vulnerability_ids)
end
end
def down
# no-op
end
end
1113642dfc9069dcea01ac12b1653dfcf67b3aea449538e9747d3bc857ce88d8
\ No newline at end of file
54ed18361a28d0b750cbbdb3bfb53b7e4bbe3d1d7264de51522796d3bd15f7a5
\ No newline at end of file
cb11dc9996b1706feaa8a53f96cbaa6209a4d07b3be9e88ebc3d1e1ada561287
\ No newline at end of file
......@@ -22557,7 +22557,7 @@ CREATE INDEX index_vulnerabilities_on_last_edited_by_id ON vulnerabilities USING
CREATE INDEX index_vulnerabilities_on_milestone_id ON vulnerabilities USING btree (milestone_id);
CREATE INDEX index_vulnerabilities_on_project_id ON vulnerabilities USING btree (project_id);
CREATE INDEX index_vulnerabilities_on_project_id_and_state_and_severity ON vulnerabilities USING btree (project_id, state, severity);
CREATE INDEX index_vulnerabilities_on_resolved_by_id ON vulnerabilities USING btree (resolved_by_id);
......
---
stage: Configure
group: Configure
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Kubernetes Agent identity and authentication **(PREMIUM ONLY)**
This page uses the word `agent` to describe the concept of the
GitLab Kubernetes Agent. The program that implements the concept is called `agentk`.
Read the
[architecture page](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/architecture.md)
for more information.
## Agent identity and name
In a GitLab installation, each agent must have a unique, immutable name. This
name must be unique in the project the agent is attached to, and this name must
follow the [DNS label standard from RFC 1123](https://tools.ietf.org/html/rfc1123).
The name must:
- Contain at most 63 characters.
- Contain only lowercase alphanumeric characters or `-`.
- Start with an alphanumeric character.
- End with an alphanumeric character.
Kubernetes uses the
[same naming restriction](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names)
for some names.
The regex for names is: `/\A[a-z0-9]([-a-z0-9]*[a-z0-9])?\z/`.
## Multiple agents in a cluster
A Kubernetes cluster may have 0 or more agents running in it. Each agent likely
has a different configuration. Some may enable features A and B, and some may
enable features B and C. This flexibility enables different groups of people to
use different features of the agent in the same cluster.
For example, [Priyanka (Platform Engineer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#priyanka-platform-engineer)
may want to use cluster-wide features of the agent, while
[Sasha (Software Developer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#sasha-software-developer)
uses the agent that only has access to a particular namespace.
Each agent is likely running using a
[`ServiceAccount`](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/),
a distinct Kubernetes identity, with a distinct set of permissions attached to it.
These permissions enable the agent administrator to follow the
[principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege)
and minimize the permissions each particular agent needs.
## Kubernetes Agent authentication
When adding a new agent, GitLab provides the user with a bearer access token. The
agent uses this token to authenticate with GitLab. This token is a random string
and does not encode any information in it, but it is secret and must
be treated with care. Store it as a `Secret` in Kubernetes.
Each agent can have 0 or more tokens in GitLab's database. Having several valid
tokens helps you rotate tokens without needing to re-register an agent. Each token
record in the database has the following fields:
- Agent identity it belongs to.
- Token value. Encrypted at rest.
- Creation time.
- Who created it.
- Revocation flag to mark token as revoked.
- Revocation time.
- Who revoked it.
- A text field to store any comments the administrator may want to make about the token for future self.
Tokens can be managed by users with `maintainer` and higher level of
[permissions](../../user/permissions.md).
Tokens are immutable, and only the following fields can be updated:
- Revocation flag. Can only be updated to `true` once, but immutable after that.
- Revocation time. Set to the current time when revocation flag is set, but immutable after that.
- Comments field. Can be updated any number of times, including after the token has been revoked.
The agent sends its token, along with each request, to GitLab to authenticate itself.
For each request, GitLab checks the token's validity:
- Does the token exist in the database?
- Has the token been revoked?
This information may be cached for some time to reduce load on the database.
## Kubernetes Agent authorization
GitLab provides the following information in its response for a given Agent access token:
- Agent configuration Git repository. (The agent doesn't support per-folder authorization.)
- Agent name.
......@@ -72,6 +72,10 @@ graph TB
- (Optional) Sending notifications through ActionCable for events received from `agentk`.
- Polling manifest repositories for [GitOps support](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/gitops.md) by communicating with Gitaly.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
To learn more about how the repository is structured, see
[GitLab Kubernetes Agent repository overview](https://www.youtube.com/watch?v=j8CyaCWroUY).
## Guiding principles
GitLab prefers to add logic into `kas` rather than `agentk`. `agentk` should be kept
......
---
stage: Configure
group: Configure
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Run the Kubernetes Agent locally **(PREMIUM ONLY)**
You can run `kas` and `agentk` locally to test the [Kubernetes Agent](index.md) yourself.
1. Create a `cfg.yaml` file from the contents of
[`kas_config_example.yaml`](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/kas_config_example.yaml), or this example:
```yaml
listen_agent:
network: tcp
address: 127.0.0.1:8150
websocket: false
gitlab:
address: http://localhost:3000
authentication_secret_file: /Users/tkuah/code/ee-gdk/gitlab/.gitlab_kas_secret
agent:
gitops:
poll_period: "10s"
```
1. Create a `token.txt`. This is the token for
[the agent you created](../../user/clusters/agent/index.md#create-an-agent-record-in-gitlab). This file must not contain a newline character. You can create the file with this command:
```shell
echo -n "<TOKEN>" > token.txt
```
1. Start the binaries with the following commands:
```shell
# Need GitLab to start
gdk start
# Stop GDK's version of kas
gdk stop gitlab-k8s-agent
# Start kas
bazel run //cmd/kas -- --configuration-file="$(pwd)/cfg.yaml"
```
1. In a new terminal window, run this command to start agentk:
```shell
bazel run //cmd/agentk -- --kas-address=grpc://127.0.0.1:8150 --token-file="$(pwd)/token.txt"
```
You can also inspect the
[Makefile](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/Makefile)
for more targets.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
To learn more about how the repository is structured, see
[GitLab Kubernetes Agent repository overview](https://www.youtube.com/watch?v=j8CyaCWroUY).
......@@ -10,7 +10,7 @@ module EE
end
override :integration_form_data
def integration_form_data(integration)
def integration_form_data(integration, group: nil)
form_data = super
if integration.is_a?(JiraService)
......
......@@ -2119,6 +2119,9 @@ msgstr ""
msgid "AdminUsers|Blocking user has the following effects:"
msgstr ""
msgid "AdminUsers|Cannot sign in or access instance information"
msgstr ""
msgid "AdminUsers|Cannot unblock LDAP blocked users"
msgstr ""
......@@ -2155,6 +2158,9 @@ msgstr ""
msgid "AdminUsers|External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects, groups, or personal snippets."
msgstr ""
msgid "AdminUsers|For more information, please refer to the %{link_start}user account deletion documentation.%{link_end}"
msgstr ""
msgid "AdminUsers|Is using seat"
msgstr ""
......@@ -2191,6 +2197,15 @@ msgstr ""
msgid "AdminUsers|Regular users have access to their groups and projects"
msgstr ""
msgid "AdminUsers|Reject"
msgstr ""
msgid "AdminUsers|Reject request"
msgstr ""
msgid "AdminUsers|Rejected users:"
msgstr ""
msgid "AdminUsers|Restore user access to the account, including web, Git and API."
msgstr ""
......@@ -2248,6 +2263,9 @@ msgstr ""
msgid "AdminUsers|When the user logs back in, their account will reactivate as a fully active account"
msgstr ""
msgid "AdminUsers|Will be deleted"
msgstr ""
msgid "AdminUsers|Without projects"
msgstr ""
......@@ -12773,6 +12791,9 @@ msgstr ""
msgid "GitLab Workhorse"
msgstr ""
msgid "GitLab account request rejected"
msgstr ""
msgid "GitLab commit"
msgstr ""
......@@ -13811,6 +13832,9 @@ msgstr ""
msgid "HealthCheck|Unhealthy"
msgstr ""
msgid "Hello %{name},"
msgstr ""
msgid "Hello there"
msgstr ""
......@@ -14790,6 +14814,9 @@ msgstr ""
msgid "Integrations|Standard"
msgstr ""
msgid "Integrations|This integration, and inheriting projects were reset."
msgstr ""
msgid "Integrations|To keep this project going, create a new issue."
msgstr ""
......@@ -20317,6 +20344,9 @@ msgstr ""
msgid "Please complete your profile with email address"
msgstr ""
msgid "Please contact your GitLab administrator if you think this is an error."
msgstr ""
msgid "Please contact your administrator with any questions."
msgstr ""
......@@ -28110,6 +28140,9 @@ msgstr ""
msgid "This user cannot be unlocked manually from GitLab"
msgstr ""
msgid "This user does not have a pending request"
msgstr ""
msgid "This user has no active %{type}."
msgstr ""
......@@ -31045,6 +31078,9 @@ msgstr ""
msgid "You are not allowed to push into this branch. Create another branch or open a merge request."
msgstr ""
msgid "You are not allowed to reject a user"
msgstr ""
msgid "You are not allowed to unlink your primary login account"
msgstr ""
......@@ -31513,6 +31549,9 @@ msgstr ""
msgid "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."
msgstr ""
msgid "You've rejected %{user}"
msgstr ""
msgid "YouTube"
msgstr ""
......@@ -31747,6 +31786,9 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
msgid "Your request to join %{host} has been rejected."
msgstr ""
msgid "Your requirements are being imported. Once finished, you'll receive a confirmation email."
msgstr ""
......
......@@ -73,4 +73,20 @@ RSpec.describe Admin::IntegrationsController do
end
end
end
describe '#reset' do
let(:integration) { create(:jira_service, :instance) }
before do
post :reset, params: { id: integration.class.to_param }
end
it 'returns 200 OK' do
expected_json = {}.to_json
expect(flash[:notice]).to eq('This integration, and inheriting projects were reset.')
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to eq(expected_json)
end
end
end
......@@ -102,6 +102,57 @@ RSpec.describe Admin::UsersController do
end
end
describe 'DELETE #reject' do
subject { put :reject, params: { id: user.username } }
context 'when rejecting a pending user' do
let(:user) { create(:user, :blocked_pending_approval) }
it 'hard deletes the user', :sidekiq_inline do
subject
expect(User.exists?(user.id)).to be_falsy
end
it 'displays the rejection message' do
subject
expect(response).to redirect_to(admin_users_path)
expect(flash[:notice]).to eq("You've rejected #{user.name}")
end
it 'sends the user a rejection email' do
expect_next_instance_of(NotificationService) do |notification|
allow(notification).to receive(:user_admin_rejection).with(user.name, user.notification_email)
end
subject
end
end
context 'when user is not pending' do
let(:user) { create(:user, state: 'active') }
it 'does not reject and delete the user' do
subject
expect(User.exists?(user.id)).to be_truthy
end
it 'displays the error' do
subject
expect(flash[:alert]).to eq('This user does not have a pending request')
end
it 'does not email the user' do
expect(NotificationService).not_to receive(:new)
subject
end
end
end
describe 'PUT #approve' do
let(:user) { create(:user, :blocked_pending_approval) }
......
......@@ -37,9 +37,7 @@ RSpec.describe 'Admin::Users::User' do
expect(page).to have_content(user.name)
expect(page).to have_content('Pending approval')
expect(page).to have_link('Approve user')
expect(page).to have_button('Block user')
expect(page).to have_button('Delete user')
expect(page).to have_button('Delete user and contributions')
expect(page).to have_link('Reject request')
end
end
......
import testAction from 'helpers/vuex_action_helper';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import createState from '~/integrations/edit/store/state';
import {
setOverride,
setIsSaving,
setIsTesting,
setIsResetting,
requestResetIntegration,
receiveResetIntegrationSuccess,
receiveResetIntegrationError,
} from '~/integrations/edit/store/actions';
import * as types from '~/integrations/edit/store/mutation_types';
jest.mock('~/lib/utils/url_utility');
describe('Integration form store actions', () => {
let state;
......@@ -40,4 +46,28 @@ describe('Integration form store actions', () => {
]);
});
});
describe('requestResetIntegration', () => {
it('should commit REQUEST_RESET_INTEGRATION mutation', () => {
return testAction(requestResetIntegration, null, state, [
{ type: types.REQUEST_RESET_INTEGRATION },
]);
});
});
describe('receiveResetIntegrationSuccess', () => {
it('should call refreshCurrentPage()', () => {
return testAction(receiveResetIntegrationSuccess, null, state, [], [], () => {
expect(refreshCurrentPage).toHaveBeenCalled();
});
});
});
describe('receiveResetIntegrationError', () => {
it('should commit RECEIVE_RESET_INTEGRATION_ERROR mutation', () => {
return testAction(receiveResetIntegrationError, null, state, [
{ type: types.RECEIVE_RESET_INTEGRATION_ERROR },
]);
});
});
});
......@@ -40,4 +40,20 @@ describe('Integration form store mutations', () => {
expect(state.isResetting).toBe(true);
});
});
describe(`${types.REQUEST_RESET_INTEGRATION}`, () => {
it('sets isResetting', () => {
mutations[types.REQUEST_RESET_INTEGRATION](state);
expect(state.isResetting).toBe(true);
});
});
describe(`${types.RECEIVE_RESET_INTEGRATION_ERROR}`, () => {
it('sets isResetting', () => {
mutations[types.RECEIVE_RESET_INTEGRATION_ERROR](state);
expect(state.isResetting).toBe(false);
});
});
});
import {
unwrapGroups,
unwrapNodesWithName,
unwrapStagesWithNeeds,
} from '~/pipelines/components/unwrapping_utils';
const groupsArray = [
{
name: 'build_a',
size: 1,
status: {
label: 'passed',
group: 'success',
icon: 'status_success',
},
},
{
name: 'bob_the_build',
size: 1,
status: {
label: 'passed',
group: 'success',
icon: 'status_success',
},
},
];
const basicStageInfo = {
name: 'center_stage',
status: {
action: null,
},
};
const stagesAndGroups = [
{
...basicStageInfo,
groups: {
nodes: groupsArray,
},
},
];
const needArray = [
{
name: 'build_b',
},
];
const elephantArray = [
{
name: 'build_b',
elephant: 'gray',
},
];
const baseJobs = {
name: 'test_d',
status: {
icon: 'status_success',
tooltip: null,
hasDetails: true,
detailsPath: '/root/abcd-dag/-/pipelines/162',
group: 'success',
action: null,
},
};
const jobArrayWithNeeds = [
{
...baseJobs,
needs: {
nodes: needArray,
},
},
];
const jobArrayWithElephant = [
{
...baseJobs,
needs: {
nodes: elephantArray,
},
},
];
const completeMock = [
{
...basicStageInfo,
groups: {
nodes: groupsArray.map(group => ({ ...group, jobs: { nodes: jobArrayWithNeeds } })),
},
},
];
describe('Shared pipeline unwrapping utils', () => {
describe('unwrapGroups', () => {
it('takes stages without nodes and returns the unwrapped groups', () => {
expect(unwrapGroups(stagesAndGroups)[0].groups).toEqual(groupsArray);
});
it('keeps other stage properties intact', () => {
expect(unwrapGroups(stagesAndGroups)[0]).toMatchObject(basicStageInfo);
});
});
describe('unwrapNodesWithName', () => {
it('works with no field argument', () => {
expect(unwrapNodesWithName(jobArrayWithNeeds, 'needs')[0].needs).toEqual([needArray[0].name]);
});
it('works with custom field argument', () => {
expect(unwrapNodesWithName(jobArrayWithElephant, 'needs', 'elephant')[0].needs).toEqual([
elephantArray[0].elephant,
]);
});
});
describe('unwrapStagesWithNeeds', () => {
it('removes nodes from groups, jobs, and needs', () => {
const firstProcessedGroup = unwrapStagesWithNeeds(completeMock)[0].groups[0];
expect(firstProcessedGroup).toMatchObject(groupsArray[0]);
expect(firstProcessedGroup.jobs[0]).toMatchObject(baseJobs);
expect(firstProcessedGroup.jobs[0].needs[0]).toBe(needArray[0].name);
});
});
});
......@@ -59,4 +59,57 @@ RSpec.describe ServicesHelper do
end
end
end
describe '#scoped_reset_integration_path' do
let(:integration) { build_stubbed(:jira_service) }
let(:group) { nil }
subject { helper.scoped_reset_integration_path(integration, group: group) }
context 'when no group is present' do
it 'returns instance-level path' do
is_expected.to eq(reset_admin_application_settings_integration_path(integration))
end
end
context 'when group is present' do
let(:group) { build_stubbed(:group) }
it 'returns group-level path' do
is_expected.to eq(reset_group_settings_integration_path(group, integration))
end
end
end
describe '#reset_integrations?' do
let(:group) { nil }
subject { helper.reset_integrations?(group: group) }
context 'when `reset_integrations` is not enabled' do
it 'returns false' do
stub_feature_flags(reset_integrations: false)
is_expected.to eq(false)
end
end
context 'when `reset_integrations` is enabled' do
it 'returns true' do
stub_feature_flags(reset_integrations: true)
is_expected.to eq(true)
end
end
context 'when `reset_integrations` is enabled for a group' do
let(:group) { build_stubbed(:group) }
it 'returns true' do
stub_feature_flags(reset_integrations: group)
is_expected.to eq(true)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe PopulateRemainingMissingDismissalInformationForVulnerabilities do
let(:users) { table(:users) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:vulnerabilities) { table(:vulnerabilities) }
let(:user) { users.create!(name: 'test', email: 'test@example.com', projects_limit: 5) }
let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
let(:project) { projects.create!(namespace_id: namespace.id, name: 'foo') }
let(:states) { { detected: 1, dismissed: 2, resolved: 3, confirmed: 4 } }
let!(:vulnerability_1) { vulnerabilities.create!(title: 'title', state: states[:detected], severity: 0, confidence: 5, report_type: 2, project_id: project.id, author_id: user.id) }
let!(:vulnerability_2) { vulnerabilities.create!(title: 'title', state: states[:dismissed], severity: 0, confidence: 5, report_type: 2, project_id: project.id, author_id: user.id) }
let!(:vulnerability_3) { vulnerabilities.create!(title: 'title', state: states[:resolved], severity: 0, confidence: 5, report_type: 2, project_id: project.id, author_id: user.id) }
let!(:vulnerability_4) { vulnerabilities.create!(title: 'title', state: states[:confirmed], severity: 0, confidence: 5, report_type: 2, project_id: project.id, author_id: user.id) }
describe '#perform' do
it 'calls the background migration class instance with broken vulnerability IDs' do
expect_next_instance_of(::Gitlab::BackgroundMigration::PopulateMissingVulnerabilityDismissalInformation) do |migrator|
expect(migrator).to receive(:perform).with(vulnerability_2.id)
end
migrate!
end
end
end
......@@ -150,6 +150,24 @@ RSpec.describe GlobalPolicy do
end
end
describe 'rejecting users' do
context 'regular user' do
it { is_expected.not_to be_allowed(:reject_user) }
end
context 'admin' do
let(:current_user) { create(:admin) }
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:reject_user) }
end
context 'when admin mode is disabled' do
it { is_expected.to be_disallowed(:reject_user) }
end
end
end
describe 'using project statistics filters' do
context 'regular user' do
it { is_expected.not_to be_allowed(:use_project_statistics_filters) }
......
......@@ -2326,6 +2326,20 @@ RSpec.describe NotificationService, :mailer do
end
end
describe '#user_admin_rejection', :deliver_mails_inline do
let_it_be(:user) { create(:user, :blocked_pending_approval) }
before do
reset_delivered_emails!
end
it 'sends the user a rejection email' do
notification.user_admin_rejection(user.name, user.email)
should_only_email(user)
end
end
describe 'GroupMember', :deliver_mails_inline do
let(:added_user) { create(:user) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Users::RejectService do
let_it_be(:current_user) { create(:admin) }
let(:user) { create(:user, :blocked_pending_approval) }
subject(:execute) { described_class.new(current_user).execute(user) }
describe '#execute' do
context 'failures' do
context 'when the executor user is not allowed to reject users' do
let(:current_user) { create(:user) }
it 'returns error result' do
expect(subject[:status]).to eq(:error)
expect(subject[:message]).to match(/You are not allowed to reject a user/)
end
end
context 'when the executor user is an admin in admin mode', :enable_admin_mode do
context 'when user is not in pending approval state' do
let(:user) { create(:user, state: 'active') }
it 'returns error result' do
expect(subject[:status]).to eq(:error)
expect(subject[:message])
.to match(/This user does not have a pending request/)
end
end
end
end
context 'success' do
context 'when the executor user is an admin in admin mode', :enable_admin_mode do
it 'deletes the user', :sidekiq_inline do
subject
expect(subject[:status]).to eq(:success)
expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'emails the user on rejection' do
expect_next_instance_of(NotificationService) do |notification|
allow(notification).to receive(:user_admin_rejection).with(user.name, user.notification_email)
end
subject
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment