Commit c4db541c authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 603c7d4c
......@@ -58,9 +58,6 @@ export default {
hasDiff() {
return hasDiff(this.file);
},
isActive() {
return this.currentDiffFileId === this.file.file_hash;
},
isFileTooLarge() {
return this.file.viewer.error === diffViewerErrors.too_large;
},
......@@ -146,7 +143,7 @@ export default {
<div
:id="file.file_hash"
:class="{
'is-active': isActive,
'is-active': currentDiffFileId === file.file_hash,
}"
class="diff-file file-holder"
>
......@@ -156,7 +153,6 @@ export default {
:collapsible="true"
:expanded="!isCollapsed"
:add-merge-request-buttons="true"
:is-active="isActive"
class="js-file-title file-title"
@toggleFile="handleToggle"
@showForkMessage="showForkMessage"
......
......@@ -55,11 +55,6 @@ export default {
type: Boolean,
required: true,
},
isActive: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapGetters('diffs', ['diffHasExpandedDiscussions', 'diffHasDiscussions']),
......@@ -163,9 +158,6 @@ export default {
<div
ref="header"
class="js-file-title file-title file-title-flex-parent"
:class="{
'is-active': isActive,
}"
@click.self="handleToggleFile"
>
<div class="file-header-content">
......
......@@ -26,7 +26,7 @@ export default {
};
},
computed: {
...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId', 'viewedDiffFileIds']),
...mapState('diffs', ['tree', 'renderTreeList']),
...mapGetters('diffs', ['allBlobs']),
filteredTreeList() {
const search = this.search.toLowerCase().trim();
......@@ -96,8 +96,6 @@ export default {
:level="0"
:hide-file-stats="hideFileStats"
:file-row-component="$options.DiffFileRow"
:active-file="currentDiffFileId"
:viewed-files="viewedDiffFileIds"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="scrollToFile"
/>
......
......@@ -26,7 +26,6 @@ export default () => ({
showTreeList: true,
currentDiffFileId: '',
projectPath: '',
viewedDiffFileIds: [],
commentForms: [],
highlightedRow: null,
renderTreeList: true,
......
......@@ -284,9 +284,6 @@ export default {
},
[types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) {
state.currentDiffFileId = fileId;
if (!state.viewedDiffFileIds.includes(fileId)) {
state.viewedDiffFileIds = [fileId, ...state.viewedDiffFileIds];
}
},
[types.OPEN_DIFF_FILE_COMMENT_FORM](state, formData) {
state.commentForms.push({
......
import Vue from 'vue';
import ReleaseEditApp from './components/app_edit.vue';
import createStore from './stores';
import detailModule from './stores/modules/detail';
import createDetailModule from './stores/modules/detail';
export default () => {
const el = document.getElementById('js-edit-release-page');
const store = createStore({
modules: {
detail: detailModule,
detail: createDetailModule(el.dataset),
},
featureFlags: {
releaseShowPage: Boolean(gon.features?.releaseShowPage),
},
});
store.dispatch('detail/setInitialState', el.dataset);
return new Vue({
el,
store,
......
import Vue from 'vue';
import ReleaseShowApp from './components/app_show.vue';
import createStore from './stores';
import detailModule from './stores/modules/detail';
import createDetailModule from './stores/modules/detail';
export default () => {
const el = document.getElementById('js-show-release-page');
const store = createStore({
modules: {
detail: detailModule,
detail: createDetailModule(el.dataset),
},
});
store.dispatch('detail/setInitialState', el.dataset);
return new Vue({
el,
......
......@@ -5,9 +5,6 @@ import { s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export const setInitialState = ({ commit }, initialState) =>
commit(types.SET_INITIAL_STATE, initialState);
export const requestRelease = ({ commit }) => commit(types.REQUEST_RELEASE);
export const receiveReleaseSuccess = ({ commit }, data) =>
commit(types.RECEIVE_RELEASE_SUCCESS, data);
......
import * as actions from './actions';
import mutations from './mutations';
import state from './state';
import createState from './state';
export default {
export default initialState => ({
namespaced: true,
actions,
mutations,
state,
};
state: createState(initialState),
});
export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
export const REQUEST_RELEASE = 'REQUEST_RELEASE';
export const RECEIVE_RELEASE_SUCCESS = 'RECEIVE_RELEASE_SUCCESS';
export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR';
......
import * as types from './mutation_types';
export default {
[types.SET_INITIAL_STATE](state, initialState) {
Object.keys(state).forEach(key => {
state[key] = initialState[key];
});
},
[types.REQUEST_RELEASE](state) {
state.isFetchingRelease = true;
},
......
export default () => ({
projectId: null,
tagName: null,
releasesPagePath: null,
markdownDocsPath: null,
markdownPreviewPath: null,
updateReleaseApiDocsPath: null,
export default ({
projectId,
tagName,
releasesPagePath,
markdownDocsPath,
markdownPreviewPath,
updateReleaseApiDocsPath,
}) => ({
projectId,
tagName,
releasesPagePath,
markdownDocsPath,
markdownPreviewPath,
updateReleaseApiDocsPath,
release: null,
......
......@@ -18,16 +18,6 @@ export default {
type: Number,
required: true,
},
activeFile: {
type: String,
required: false,
default: '',
},
viewedFiles: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
isTree() {
......@@ -44,8 +34,8 @@ export default {
fileClass() {
return {
'file-open': this.isBlob && this.file.opened,
'is-active': this.isBlob && (this.file.active || this.activeFile === this.file.fileHash),
'is-viewed': this.isBlob && this.viewedFiles.includes(this.file.fileHash),
'is-active': this.isBlob && this.file.active,
folder: this.isTree,
'is-open': this.file.opened,
};
},
......@@ -117,23 +107,15 @@ export default {
v-else
:class="fileClass"
:title="file.name"
class="file-row text-left px-1 py-2 ml-n2 d-flex align-items-center"
class="file-row"
role="button"
@click="clickFile"
@mouseleave="$emit('mouseleave', $event)"
>
<div class="file-row-name-container w-100 d-flex align-items-center">
<span
ref="textOutput"
:style="levelIndentation"
class="file-row-name str-truncated d-inline-block"
:class="[
{ 'folder font-weight-normal': isTree },
fileClass['is-viewed'] ? 'font-weight-normal' : 'font-weight-bold',
]"
>
<div class="file-row-name-container">
<span ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated">
<file-icon
class="file-row-icon align-middle mr-1"
class="file-row-icon"
:class="{ 'text-secondary': file.type === 'tree' }"
:file-name="file.name"
:loading="file.loading"
......@@ -150,8 +132,14 @@ export default {
<style>
.file-row {
display: flex;
align-items: center;
height: 32px;
padding: 4px 8px;
margin-left: -8px;
margin-right: -8px;
border-radius: 3px;
text-align: left;
cursor: pointer;
}
......@@ -169,15 +157,24 @@ export default {
}
.file-row-name-container {
display: flex;
width: 100%;
align-items: center;
overflow: visible;
}
.file-row-name {
display: inline-block;
flex: 1;
max-width: inherit;
height: 20px;
height: 19px;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-row-name .file-row-icon {
margin-right: 2px;
vertical-align: middle;
}
</style>
......@@ -69,11 +69,6 @@
}
}
.file-title.is-active,
.file-title-flex-parent.is-active {
background-color: $gray-200;
}
@media (min-width: map-get($grid-breakpoints, md)) {
&.conflict .file-title,
&.conflict .file-title-flex-parent {
......
......@@ -7,7 +7,7 @@ class X509Issuer < ApplicationRecord
validates :subject_key_identifier, presence: true, format: { with: /\A(\h{2}:){19}\h{2}\z/ }
# rfc 5280 - 4.1.2.4 Issuer
validates :subject, presence: true
# rfc 5280 - 4.2.1.14 CRL Distribution Points
# rfc 5280 - 4.2.1.13 CRL Distribution Points
# cRLDistributionPoints extension using URI:http
validates :crl_url, presence: true, public_url: true
......
---
title: Highlight currently focused/viewed file in file tree
merge_request: 27703
author:
type: changed
---
title: Update ApplicationLimits to prefer defaults
merge_request: 27574
author:
type: changed
---
title: Extract X509::Signature from X509::Commit
merge_request: 27327
author: Roger Meier
type: changed
# frozen_string_literal: true
class UpdatePlanLimitsDefaults < ActiveRecord::Migration[6.0]
DOWNTIME = false
def up
change_column_default :plan_limits, :project_hooks, 100
change_column_default :plan_limits, :group_hooks, 50
change_column_default :plan_limits, :ci_project_subscriptions, 2
change_column_default :plan_limits, :ci_pipeline_schedules, 10
end
def down
change_column_default :plan_limits, :project_hooks, 0
change_column_default :plan_limits, :group_hooks, 0
change_column_default :plan_limits, :ci_project_subscriptions, 0
change_column_default :plan_limits, :ci_pipeline_schedules, 0
end
end
......@@ -4508,10 +4508,10 @@ CREATE TABLE public.plan_limits (
ci_active_pipelines integer DEFAULT 0 NOT NULL,
ci_pipeline_size integer DEFAULT 0 NOT NULL,
ci_active_jobs integer DEFAULT 0 NOT NULL,
project_hooks integer DEFAULT 0 NOT NULL,
group_hooks integer DEFAULT 0 NOT NULL,
ci_project_subscriptions integer DEFAULT 0 NOT NULL,
ci_pipeline_schedules integer DEFAULT 0 NOT NULL
project_hooks integer DEFAULT 100 NOT NULL,
group_hooks integer DEFAULT 50 NOT NULL,
ci_project_subscriptions integer DEFAULT 2 NOT NULL,
ci_pipeline_schedules integer DEFAULT 10 NOT NULL
);
CREATE SEQUENCE public.plan_limits_id_seq
......@@ -12747,6 +12747,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20200318164448'),
('20200318165448'),
('20200318175008'),
('20200319123041'),
('20200319203901'),
('20200323075043'),
('20200323122201');
......
......@@ -405,198 +405,4 @@ network and LDAP server response time will affect these metrics.
## Troubleshooting
### Referral error
If you see `LDAP search error: Referral` in the logs, or when troubleshooting
LDAP Group Sync, this error may indicate a configuration problem. The LDAP
configuration `/etc/gitlab/gitlab.rb` (Omnibus) or `config/gitlab.yml` (source)
is in YAML format and is sensitive to indentation. Check that `group_base` and
`admin_group` configuration keys are indented 2 spaces past the server
identifier. The default identifier is `main` and an example snippet looks like
the following:
```yaml
main: # 'main' is the GitLab 'provider ID' of this LDAP server
label: 'LDAP'
host: 'ldap.example.com'
...
group_base: 'cn=my_group,ou=groups,dc=example,dc=com'
admin_group: 'my_admin_group'
```
[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[restart]: ../restart_gitlab.md#installations-from-source
[^1]: In Active Directory, a user is marked as disabled/blocked if the user
account control attribute (`userAccountControl:1.2.840.113556.1.4.803`)
has bit 2 set. See <https://ctovswild.com/2009/09/03/bitmask-searches-in-ldap/>
for more information.
### User DN has changed
When an LDAP user is created in GitLab, their LDAP DN is stored for later reference.
If GitLab cannot find a user by their DN, it will attempt to fallback
to finding the user by their email. If the lookup is successful, GitLab will
update the stored DN to the new value.
### User is not being added to a group
Sometimes you may think a particular user should be added to a GitLab group via
LDAP group sync, but for some reason it's not happening. There are several
things to check to debug the situation.
- Ensure LDAP configuration has a `group_base` specified. This configuration is
required for group sync to work properly.
- Ensure the correct LDAP group link is added to the GitLab group. Check group
links by visiting the GitLab group, then **Settings dropdown > LDAP groups**.
- Check that the user has an LDAP identity:
1. Sign in to GitLab as an administrator user.
1. Navigate to **Admin Area > Users**.
1. Search for the user
1. Open the user, by clicking on their name. Do not click 'Edit'.
1. Navigate to the **Identities** tab. There should be an LDAP identity with
an LDAP DN as the 'Identifier'.
If all of the above looks good, jump in to a little more advanced debugging.
Often, the best way to learn more about why group sync is behaving a certain
way is to enable debug logging. There is verbose output that details every
step of the sync.
1. Start a Rails console:
```shell
# For Omnibus installations
sudo gitlab-rails console
# For installations from source
sudo -u git -H bundle exec rails console -e production
```
1. Set the log level to debug (only for this session):
```ruby
Rails.logger.level = Logger::DEBUG
```
1. Choose a GitLab group to test with. This group should have an LDAP group link
already configured. If the output is `nil`, the group could not be found.
If a bunch of group attributes are output, your group was found successfully.
```ruby
group = Group.find_by(name: 'my_group')
# Output
=> #<Group:0x007fe825196558 id: 1234, name: "my_group"...>
```
1. Run a group sync for this particular group.
```ruby
EE::Gitlab::Auth::Ldap::Sync::Group.execute_all_providers(group)
```
1. Look through the output of the sync. See [example log output](#example-log-output)
below for more information about the output.
1. If you still aren't able to see why the user isn't being added, query the
LDAP group directly to see what members are listed. Still in the Rails console,
run the following query:
```ruby
adapter = Gitlab::Auth::Ldap::Adapter.new('ldapmain') # If `main` is the LDAP provider
ldap_group = EE::Gitlab::Auth::Ldap::Group.find_by_cn('group_cn_here', adapter)
# Output
=> #<EE::Gitlab::Auth::Ldap::Group:0x007fcbdd0bb6d8
```
1. Query the LDAP group's member DNs and see if the user's DN is in the list.
One of the DNs here should match the 'Identifier' from the LDAP identity
checked earlier. If it doesn't, the user does not appear to be in the LDAP
group.
```ruby
ldap_group.member_dns
# Output
=> ["uid=john,ou=people,dc=example,dc=com", "uid=mary,ou=people,dc=example,dc=com"]
```
1. Some LDAP servers don't store members by DN. Rather, they use UIDs instead.
If you didn't see results from the last query, try querying by UIDs instead.
```ruby
ldap_group.member_uids
# Output
=> ['john','mary']
```
#### Example log output
The output of the last command will be very verbose, but contains lots of
helpful information. For the most part you can ignore log entries that are SQL
statements.
Indicates the point where syncing actually begins:
```shell
Started syncing all providers for 'my_group' group
```
The follow entry shows an array of all user DNs GitLab sees in the LDAP server.
Note that these are the users for a single LDAP group, not a GitLab group. If
you have multiple LDAP groups linked to this GitLab group, you will see multiple
log entries like this - one for each LDAP group. If you don't see an LDAP user
DN in this log entry, LDAP is not returning the user when we do the lookup.
Verify the user is actually in the LDAP group.
```shell
Members in 'ldap_group_1' LDAP group: ["uid=john0,ou=people,dc=example,dc=com",
"uid=mary0,ou=people,dc=example,dc=com", "uid=john1,ou=people,dc=example,dc=com",
"uid=mary1,ou=people,dc=example,dc=com", "uid=john2,ou=people,dc=example,dc=com",
"uid=mary2,ou=people,dc=example,dc=com", "uid=john3,ou=people,dc=example,dc=com",
"uid=mary3,ou=people,dc=example,dc=com", "uid=john4,ou=people,dc=example,dc=com",
"uid=mary4,ou=people,dc=example,dc=com"]
```
Shortly after each of the above entries, you will see a hash of resolved member
access levels. This hash represents all user DNs GitLab thinks should have
access to this group, and at which access level (role). This hash is additive,
and more DNs may be added, or existing entries modified, based on additional
LDAP group lookups. The very last occurrence of this entry should indicate
exactly which users GitLab believes should be added to the group.
NOTE: **Note:**
10 is 'Guest', 20 is 'Reporter', 30 is 'Developer', 40 is 'Maintainer'
and 50 is 'Owner'.
```shell
Resolved 'my_group' group member access: {"uid=john0,ou=people,dc=example,dc=com"=>30,
"uid=mary0,ou=people,dc=example,dc=com"=>30, "uid=john1,ou=people,dc=example,dc=com"=>30,
"uid=mary1,ou=people,dc=example,dc=com"=>30, "uid=john2,ou=people,dc=example,dc=com"=>30,
"uid=mary2,ou=people,dc=example,dc=com"=>30, "uid=john3,ou=people,dc=example,dc=com"=>30,
"uid=mary3,ou=people,dc=example,dc=com"=>30, "uid=john4,ou=people,dc=example,dc=com"=>30,
"uid=mary4,ou=people,dc=example,dc=com"=>30}
```
It's not uncommon to see warnings like the following. These indicate that GitLab
would have added the user to a group, but the user could not be found in GitLab.
Usually this is not a cause for concern.
If you think a particular user should already exist in GitLab, but you're seeing
this entry, it could be due to a mismatched DN stored in GitLab. See
[User DN has changed](#User-DN-has-changed) to update the user's LDAP identity.
```shell
User with DN `uid=john0,ou=people,dc=example,dc=com` should have access
to 'my_group' group but there is no user in GitLab with that
identity. Membership will be updated once the user signs in for
the first time.
```
Finally, the following entry says syncing has finished for this group:
```shell
Finished syncing all providers for 'my_group' group
```
Please see our [administrator guide to troubleshooting LDAP](ldap-troubleshooting.md).
This diff is collapsed.
......@@ -552,74 +552,4 @@ be mandatory and clients cannot be authenticated with the TLS protocol.
## Troubleshooting
If a user account is blocked or unblocked due to the LDAP configuration, a
message will be logged to `application.log`.
If there is an unexpected error during an LDAP lookup (configuration error,
timeout), the login is rejected and a message will be logged to
`production.log`.
### Debug LDAP user filter with ldapsearch
This example uses `ldapsearch` and assumes you are using ActiveDirectory. The
following query returns the login names of the users that will be allowed to
log in to GitLab if you configure your own user_filter.
```shell
ldapsearch -H ldaps://$host:$port -D "$bind_dn" -y bind_dn_password.txt -b "$base" "$user_filter" sAMAccountName
```
- Variables beginning with a `$` refer to a variable from the LDAP section of
your configuration file.
- Replace `ldaps://` with `ldap://` if you are using the plain authentication method.
Port `389` is the default `ldap://` port and `636` is the default `ldaps://`
port.
- We are assuming the password for the bind_dn user is in bind_dn_password.txt.
### Invalid credentials when logging in
- Make sure the user you are binding with has enough permissions to read the user's
tree and traverse it.
- Check that the `user_filter` is not blocking otherwise valid users.
- Run the following check command to make sure that the LDAP settings are
correct and GitLab can see your users:
```shell
# For Omnibus installations
sudo gitlab-rake gitlab:ldap:check
# For installations from source
sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production
```
### Connection refused
If you are getting 'Connection Refused' errors when trying to connect to the
LDAP server please double-check the LDAP `port` and `encryption` settings used by
GitLab. Common combinations are `encryption: 'plain'` and `port: 389`, OR
`encryption: 'simple_tls'` and `port: 636`.
### Connection times out
If GitLab cannot reach your LDAP endpoint, you will see a message like this:
```plaintext
Could not authenticate you from Ldapmain because "Connection timed out - user specified timeout".
```
If your configured LDAP provider and/or endpoint is offline or otherwise unreachable by GitLab, no LDAP user will be able to authenticate and log in. GitLab does not cache or store credentials for LDAP users to provide authentication during an LDAP outage.
Contact your LDAP provider or administrator if you are seeing this error.
### No file specified as Settingslogic source
If `sudo gitlab-ctl reconfigure` fails with the following error, or you are seeing it in
the logs, you may have malformed YAML in `/etc/gitlab/gitlab.rb`:
```plaintext
Errno::ENOENT: No such file or directory - No file specified as Settingslogic source
```
This issue is frequently due to the spacing in your YAML file. To fix the problem,
verify the syntax with **spacing** against the
[documentation for the configuration of LDAP](#configuration).
Please see our [administrator guide to troubleshooting LDAP](ldap-troubleshooting.md).
......@@ -28,7 +28,7 @@ rake gitlab:ldap:check[50]
## Run a Group Sync
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/14735) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.3.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/14735) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.2.
The following task will run a [group sync](../auth/ldap-ee.md#group-sync) immediately. This is valuable
when you'd like to update all configured group memberships against LDAP without
......
......@@ -536,98 +536,6 @@ group = Group.find_by_path_or_name('group-name')
group.project_creation_level=0
```
## LDAP
### LDAP commands in the rails console
TIP: **TIP:**
Use the rails runner to avoid entering the rails console in the first place.
This is great when only a single command (such as a UserSync or GroupSync)
is needed.
```ruby
# Get debug output
Rails.logger.level = Logger::DEBUG
# Run a UserSync (normally performed once a day)
LdapSyncWorker.new.perform
# Run a GroupSync for all groups (9.3-)
LdapGroupSyncWorker.new.perform
# Run a GroupSync for all groups (9.3+)
LdapAllGroupsSyncWorker.new.perform
# Run a GroupSync for a single group (10.6-)
group = Group.find_by(name: 'my_gitlab_group')
EE::Gitlab::LDAP::Sync::Group.execute_all_providers(group)
# Run a GroupSync for a single group (10.6+)
group = Group.find_by(name: 'my_gitlab_group')
EE::Gitlab::Auth::Ldap::Sync::Group.execute_all_providers(group)
# Query an LDAP group directly (10.6-)
adapter = Gitlab::LDAP::Adapter.new('ldapmain') # If `main` is the LDAP provider
ldap_group = EE::Gitlab::LDAP::Group.find_by_cn('group_cn_here', adapter)
ldap_group.member_dns
ldap_group.member_uids
# Query an LDAP group directly (10.6+)
adapter = Gitlab::Auth::Ldap::Adapter.new('ldapmain') # If `main` is the LDAP provider
ldap_group = EE::Gitlab::Auth::Ldap::Group.find_by_cn('group_cn_here', adapter)
ldap_group.member_dns
ldap_group.member_uids
# Lookup a particular user (10.6+)
# This could expose potential errors connecting to and/or querying LDAP that may seem to
# fail silently in the GitLab UI
adapter = Gitlab::Auth::Ldap::Adapter.new('ldapmain') # If `main` is the LDAP provider
user = Gitlab::Auth::Ldap::Person.find_by_uid('<username>',adapter)
# Query the LDAP server directly (10.6+)
## For an example, see https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/ee/gitlab/auth/ldap/adapter.rb
adapter = Gitlab::Auth::Ldap::Adapter.new('ldapmain')
options = {
# the :base is required
# use adapter.config.base for the base or .group_base for the group_base
base: adapter.config.group_base,
# :filter is optional
# 'cn' looks for all "cn"s under :base
# '*' is the search string - here, it's a wildcard
filter: Net::LDAP::Filter.eq('cn', '*'),
# :attributes is optional
# the attributes we want to get returned
attributes: %w(dn cn memberuid member submember uniquemember memberof)
}
adapter.ldap_search(options)
```
### Update user accounts when the `dn` and email change
The following will require that any accounts with the new email address are removed.
Emails have to be unique in GitLab. This is expected to work but unverified as of yet:
```ruby
# Here's an example with a couple users.
# Each entry will have to include the old username and the new email
emails = {
'ORIGINAL_USERNAME' => 'NEW_EMAIL_ADDRESS',
...
}
emails.each do |username, email|
user = User.find_by_username(username)
user.email = email
user.skip_reconfirmation!
user.save!
end
# Run the UserSync to update the above users' data
LdapSyncWorker.new.perform
```
## Routes
### Remove redirecting routes
......
......@@ -20,40 +20,45 @@ limits](https://about.gitlab.com/handbook/product/#introducing-application-limit
In the `plan_limits` table, you have to create a new column and insert the
limit values. It's recommended to create separate migration script files.
1. Add new column to the `plan_limits` table with non-null default value 0, eg:
```ruby
add_column(:plan_limits, :project_hooks, :integer, default: 0, null: false)
```
NOTE: **Note:** Plan limits entries set to `0` mean that limits are not
enabled.
1. Insert plan limits values into the database using
`create_or_update_plan_limit` migration helper, eg:
```ruby
def up
return unless Gitlab.com?
create_or_update_plan_limit('project_hooks', 'free', 100)
create_or_update_plan_limit('project_hooks', 'bronze', 100)
create_or_update_plan_limit('project_hooks', 'silver', 100)
create_or_update_plan_limit('project_hooks', 'gold', 100)
end
def down
return unless Gitlab.com?
create_or_update_plan_limit('project_hooks', 'free', 0)
create_or_update_plan_limit('project_hooks', 'bronze', 0)
create_or_update_plan_limit('project_hooks', 'silver', 0)
create_or_update_plan_limit('project_hooks', 'gold', 0)
end
```
NOTE: **Note:** Some plans exist only on GitLab.com. You can check if the
migration is running on GitLab.com with `Gitlab.com?`.
1. Add new column to the `plan_limits` table with non-null default value
that represents desired limit, eg:
```ruby
add_column(:plan_limits, :project_hooks, :integer, default: 100, null: false)
```
NOTE: **Note:** Plan limits entries set to `0` mean that limits are not
enabled. You should use this setting only in special and documented circumstances.
1. (Optionally) Create the database migration that fine-tunes each level with
a desired limit using `create_or_update_plan_limit` migration helper, eg:
```ruby
class InsertProjectHooksPlanLimits < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
create_or_update_plan_limit('project_hooks', 'default', 0)
create_or_update_plan_limit('project_hooks', 'free', 10)
create_or_update_plan_limit('project_hooks', 'bronze', 20)
create_or_update_plan_limit('project_hooks', 'silver', 30)
create_or_update_plan_limit('project_hooks', 'gold', 100)
end
def down
create_or_update_plan_limit('project_hooks', 'default', 0)
create_or_update_plan_limit('project_hooks', 'free', 0)
create_or_update_plan_limit('project_hooks', 'bronze', 0)
create_or_update_plan_limit('project_hooks', 'silver', 0)
create_or_update_plan_limit('project_hooks', 'gold', 0)
end
end
```
NOTE: **Note:** Some plans exist only on GitLab.com. This will be no-op
for plans that do not exist.
### Plan limits validation
......
......@@ -246,21 +246,96 @@ From [vuex mutations docs](https://vuex.vuejs.org/guide/mutations.html):
export const ADD_USER = 'ADD_USER';
```
### How to include the store in your application
### Initializing a store's state
The store should be included in the main component of your application:
It's common for a Vuex store to need some initial state before its `action`s can
be used. Often this includes data like API endpoints, documentation URLs, or
IDs.
To set this initial state, pass it as a parameter to your store's creation
function when mounting your Vue component:
```javascript
// app.vue
import store from './store'; // it will include the index.js file
// in the Vue app's initialization script (e.g. mount_show.js)
export default {
name: 'application',
store,
...
};
import Vue from 'vue';
import createStore from './stores';
import AwesomeVueApp from './components/awesome_vue_app.vue'
export default () => {
const el = document.getElementById('js-awesome-vue-app');
return new Vue({
el,
store: createStore(el.dataset),
render: h => h(AwesomeVueApp)
});
};
```
The store function, in turn, can pass this data along to the state's creation
function:
```javascript
// in store/index.js
import * as actions from './actions';
import mutations from './mutations';
import createState from './state';
export default initialState => ({
actions,
mutations,
state: createState(initialState),
});
```
And the state function can accept this initial data as a parameter and bake it
into the `state` object it returns:
```javascript
// in store/state.js
export default ({
projectId,
documentationPath,
anOptionalProperty = true
}) => ({
projectId,
documentationPath,
anOptionalProperty,
// other state properties here
});
```
#### Why not just ...spread the initial state?
The astute reader will see an opportunity to cut out a few lines of code from
the example above:
```javascript
// Don't do this!
export default initialState => ({
...initialState,
// other state properties here
});
```
We've made the conscious decision to avoid this pattern to aid in the
discoverability and searchability of our frontend codebase. The reasoning for
this is described in [this
discussion](https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/56#note_302514865):
> Consider a `someStateKey` is being used in the store state. You _may_ not be
> able to grep for it directly if it was provided only by `el.dataset`. Instead,
> you'd have to grep for `some_state_key`, since it could have come from a rails
> template. The reverse is also true: if you're looking at a rails template, you
> might wonder what uses `some_state_key`, but you'd _have_ to grep for
> `someStateKey`
### Communicating with the Store
```javascript
......
......@@ -332,6 +332,19 @@ background job.
If a past `released_at` is used, no Evidence is collected for the Release.
## GitLab Releaser
> [Introduced](https://gitlab.com/gitlab-org/gitlab-releaser/-/merge_requests/6) in GitLab 12.10.
GitLab Releaser is a CLI tool for managing GitLab Releases from the command line or from
GitLab CI/CD's configuration file, `.gitlab-ci.yml`.
With it, you can create, update, modify, and delete Releases right through the
terminal.
Read the [GitLab Releaser documentation](https://gitlab.com/gitlab-org/gitlab-releaser/-/tree/master/docs/index.md)
for details.
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
......
......@@ -1121,11 +1121,14 @@ into similar problems in the future (e.g. when new tables are created).
end
def create_or_update_plan_limit(limit_name, plan_name, limit_value)
limit_name_quoted = quote_column_name(limit_name)
plan_name_quoted = quote(plan_name)
limit_value_quoted = quote(limit_value)
execute <<~SQL
INSERT INTO plan_limits (plan_id, #{quote_column_name(limit_name)})
VALUES
((SELECT id FROM plans WHERE name = #{quote(plan_name)} LIMIT 1), #{quote(limit_value)})
ON CONFLICT (plan_id) DO UPDATE SET #{quote_column_name(limit_name)} = EXCLUDED.#{quote_column_name(limit_name)};
INSERT INTO plan_limits (plan_id, #{limit_name_quoted})
SELECT id, #{limit_value_quoted} FROM plans WHERE name = #{plan_name_quoted} LIMIT 1
ON CONFLICT (plan_id) DO UPDATE SET #{limit_name_quoted} = EXCLUDED.#{limit_name_quoted};
SQL
end
......
......@@ -31,175 +31,23 @@ module Gitlab
end
end
def verified_signature
strong_memoize(:verified_signature) { verified_signature? }
end
def cert
strong_memoize(:cert) do
signer_certificate(p7) if valid_signature?
end
end
def cert_store
strong_memoize(:cert_store) do
store = OpenSSL::X509::Store.new
store.set_default_paths
# valid_signing_time? checks the time attributes already
# this flag is required, otherwise expired certificates would become
# unverified when notAfter within certificate attribute is reached
store.flags = OpenSSL::X509::V_FLAG_NO_CHECK_TIME
store
end
end
def p7
strong_memoize(:p7) do
pkcs7_text = signature_text.sub('-----BEGIN SIGNED MESSAGE-----', '-----BEGIN PKCS7-----')
pkcs7_text = pkcs7_text.sub('-----END SIGNED MESSAGE-----', '-----END PKCS7-----')
OpenSSL::PKCS7.new(pkcs7_text)
rescue
nil
end
end
def valid_signing_time?
# rfc 5280 - 4.1.2.5 Validity
# check if signed_time is within the time range (notBefore/notAfter)
# non-rfc - git specific check: signed_time >= commit_time
p7.signers[0].signed_time.between?(cert.not_before, cert.not_after) &&
p7.signers[0].signed_time >= @commit.created_at
end
def valid_signature?
p7.verify([], cert_store, signed_text, OpenSSL::PKCS7::NOVERIFY)
rescue
nil
end
def verified_signature?
# verify has multiple options but only a boolean return value
# so first verify without certificate chain
if valid_signature?
if valid_signing_time?
# verify with system certificate chain
p7.verify([], cert_store, signed_text)
else
false
end
else
nil
end
rescue
nil
end
def signer_certificate(p7)
p7.certificates.each do |cert|
next if cert.serial != p7.signers[0].serial
return cert
end
end
def certificate_crl
extension = get_certificate_extension('crlDistributionPoints')
crl_url = nil
extension.each_line do |line|
break if crl_url
line.split('URI:').each do |item|
item.strip
if item.start_with?("http")
crl_url = item.strip
break
end
end
end
crl_url
end
def get_certificate_extension(extension)
cert.extensions.each do |ext|
if ext.oid == extension
return ext.value
end
end
end
def issuer_subject_key_identifier
get_certificate_extension('authorityKeyIdentifier').gsub("keyid:", "").delete!("\n")
end
def certificate_subject_key_identifier
get_certificate_extension('subjectKeyIdentifier')
end
def certificate_issuer
cert.issuer.to_s(OpenSSL::X509::Name::RFC2253)
end
def certificate_subject
cert.subject.to_s(OpenSSL::X509::Name::RFC2253)
end
def certificate_email
get_certificate_extension('subjectAltName').split('email:')[1]
end
def issuer_attributes
return if verified_signature.nil?
{
subject_key_identifier: issuer_subject_key_identifier,
subject: certificate_issuer,
crl_url: certificate_crl
}
end
def certificate_attributes
return if verified_signature.nil?
issuer = X509Issuer.safe_create!(issuer_attributes)
{
subject_key_identifier: certificate_subject_key_identifier,
subject: certificate_subject,
email: certificate_email,
serial_number: cert.serial,
x509_issuer_id: issuer.id
}
end
def attributes
return if verified_signature.nil?
return if @commit.sha.nil? || @commit.project.nil?
certificate = X509Certificate.safe_create!(certificate_attributes)
signature = X509::Signature.new(signature_text, signed_text, @commit.committer_email, @commit.created_at)
return if signature.verified_signature.nil? || signature.x509_certificate.nil?
{
commit_sha: @commit.sha,
project: @commit.project,
x509_certificate_id: certificate.id,
verification_status: verification_status(certificate)
x509_certificate_id: signature.x509_certificate.id,
verification_status: signature.verification_status
}
end
def verification_status(certificate)
return :unverified if certificate.revoked?
if verified_signature && certificate_email == @commit.committer_email
:verified
else
:unverified
end
end
def create_cached_signature!
return if verified_signature.nil?
return if attributes.nil?
return X509CommitSignature.new(attributes) if Gitlab::Database.read_only?
......
# frozen_string_literal: true
require 'openssl'
require 'digest'
module Gitlab
module X509
class Signature
include Gitlab::Utils::StrongMemoize
attr_reader :signature_text, :signed_text, :created_at
def initialize(signature_text, signed_text, email, created_at)
@signature_text = signature_text
@signed_text = signed_text
@email = email
@created_at = created_at
end
def x509_certificate
return if certificate_attributes.nil?
X509Certificate.safe_create!(certificate_attributes) unless verified_signature.nil?
end
def verified_signature
strong_memoize(:verified_signature) { verified_signature? }
end
def verification_status
return :unverified if x509_certificate.nil? || x509_certificate.revoked?
if verified_signature && certificate_email == @email
:verified
else
:unverified
end
end
private
def cert
strong_memoize(:cert) do
signer_certificate(p7) if valid_signature?
end
end
def cert_store
strong_memoize(:cert_store) do
store = OpenSSL::X509::Store.new
store.set_default_paths
# valid_signing_time? checks the time attributes already
# this flag is required, otherwise expired certificates would become
# unverified when notAfter within certificate attribute is reached
store.flags = OpenSSL::X509::V_FLAG_NO_CHECK_TIME
store
end
end
def p7
strong_memoize(:p7) do
pkcs7_text = signature_text.sub('-----BEGIN SIGNED MESSAGE-----', '-----BEGIN PKCS7-----')
pkcs7_text = pkcs7_text.sub('-----END SIGNED MESSAGE-----', '-----END PKCS7-----')
OpenSSL::PKCS7.new(pkcs7_text)
rescue
nil
end
end
def valid_signing_time?
# rfc 5280 - 4.1.2.5 Validity
# check if signed_time is within the time range (notBefore/notAfter)
# non-rfc - git specific check: signed_time >= commit_time
p7.signers[0].signed_time.between?(cert.not_before, cert.not_after) &&
p7.signers[0].signed_time >= created_at
end
def valid_signature?
p7.verify([], cert_store, signed_text, OpenSSL::PKCS7::NOVERIFY)
rescue
nil
end
def verified_signature?
# verify has multiple options but only a boolean return value
# so first verify without certificate chain
if valid_signature?
if valid_signing_time?
# verify with system certificate chain
p7.verify([], cert_store, signed_text)
else
false
end
else
nil
end
rescue
nil
end
def signer_certificate(p7)
p7.certificates.each do |cert|
next if cert.serial != p7.signers[0].serial
return cert
end
end
def certificate_crl
extension = get_certificate_extension('crlDistributionPoints')
return if extension.nil?
crl_url = nil
extension.each_line do |line|
break if crl_url
line.split('URI:').each do |item|
item.strip
if item.start_with?("http")
crl_url = item.strip
break
end
end
end
crl_url
end
def get_certificate_extension(extension)
ext = cert.extensions.detect { |ext| ext.oid == extension }
ext&.value
end
def issuer_subject_key_identifier
key_identifier = get_certificate_extension('authorityKeyIdentifier')
return if key_identifier.nil?
key_identifier.gsub("keyid:", "").delete!("\n")
end
def certificate_subject_key_identifier
key_identifier = get_certificate_extension('subjectKeyIdentifier')
return if key_identifier.nil?
key_identifier
end
def certificate_issuer
cert.issuer.to_s(OpenSSL::X509::Name::RFC2253)
end
def certificate_subject
cert.subject.to_s(OpenSSL::X509::Name::RFC2253)
end
def certificate_email
email = nil
get_certificate_extension('subjectAltName').split(',').each do |item|
if item.strip.start_with?("email")
email = item.split('email:')[1]
break
end
end
return if email.nil?
email
end
def x509_issuer
return if verified_signature.nil? || issuer_subject_key_identifier.nil? || certificate_crl.nil?
attributes = {
subject_key_identifier: issuer_subject_key_identifier,
subject: certificate_issuer,
crl_url: certificate_crl
}
X509Issuer.safe_create!(attributes) unless verified_signature.nil?
end
def certificate_attributes
return if verified_signature.nil? || certificate_subject_key_identifier.nil? || x509_issuer.nil?
{
subject_key_identifier: certificate_subject_key_identifier,
subject: certificate_subject,
email: certificate_email,
serial_number: cert.serial.to_i,
x509_issuer_id: x509_issuer.id
}
end
end
end
end
......@@ -5,10 +5,6 @@ describe('BlobFileDropzone', () => {
preloadFixtures('blob/show.html');
let dropzone;
let replaceFileButton;
const jQueryMock = {
enable: jest.fn(),
disable: jest.fn(),
};
beforeEach(() => {
loadFixtures('blob/show.html');
......@@ -18,7 +14,6 @@ describe('BlobFileDropzone', () => {
dropzone = $('.js-upload-blob-form .dropzone').get(0).dropzone;
dropzone.processQueue = jest.fn();
replaceFileButton = $('#submit-all');
$.fn.extend(jQueryMock);
});
describe('submit button', () => {
......@@ -43,7 +38,7 @@ describe('BlobFileDropzone', () => {
replaceFileButton.click();
expect(window.alert).not.toHaveBeenCalled();
expect(jQueryMock.enable).toHaveBeenCalled();
expect(replaceFileButton.is(':disabled')).toEqual(true);
expect(dropzone.processQueue).toHaveBeenCalled();
});
});
......
/* global List */
import $ from 'jquery';
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
......@@ -15,9 +14,6 @@ describe('Issue boards new issue form', () => {
let list;
let mock;
let newIssueMock;
const jQueryMock = {
enable: jest.fn(),
};
const promiseReturn = {
data: {
iid: 100,
......@@ -53,8 +49,6 @@ describe('Issue boards new issue form', () => {
},
}).$mount(document.querySelector('.test-container'));
$.fn.extend(jQueryMock);
return Vue.nextTick();
});
......@@ -118,7 +112,7 @@ describe('Issue boards new issue form', () => {
return Vue.nextTick()
.then(submitIssue)
.then(() => {
expect(jQueryMock.enable).toHaveBeenCalled();
expect(vm.$el.querySelector('.btn-success').disabled).toBe(false);
});
});
......
......@@ -61,7 +61,6 @@ describe('DiffFileHeader component', () => {
const findTitleLink = () => wrapper.find({ ref: 'titleWrapper' });
const findExpandButton = () => wrapper.find({ ref: 'expandDiffToFullFileButton' });
const findFileActions = () => wrapper.find('.file-actions');
const findActiveHeader = () => wrapper.find('.is-active');
const findModeChangedLine = () => wrapper.find({ ref: 'fileMode' });
const findLfsLabel = () => wrapper.find('.label-lfs');
const findToggleDiscussionsButton = () => wrapper.find({ ref: 'toggleDiscussionsButton' });
......@@ -144,11 +143,6 @@ describe('DiffFileHeader component', () => {
expect(wrapper.find(ClipboardButton).exists()).toBe(true);
});
it('contains a active header class if this is the active file header', () => {
createComponent({ isActive: true });
expect(findActiveHeader().exists()).toBe(true);
});
describe('for submodule', () => {
const submoduleDiffFile = {
...diffFile,
......
import $ from 'jquery';
// Expose jQuery so specs using jQuery plugins can be imported nicely.
// Here is an issue to explore better alternatives:
// https://gitlab.com/gitlab-org/gitlab/issues/12448
global.$ = $;
global.jQuery = $;
// Fail tests for unmocked requests
$.ajax = () => {
const err = new Error(
'Unexpected unmocked jQuery.ajax() call! Make sure to mock jQuery.ajax() in tests.',
);
global.fail(err);
throw err;
};
export default $;
/* eslint-disable import/no-commonjs */
const $ = jest.requireActual('jquery');
// Fail tests for unmocked requests
$.ajax = () => {
const err = new Error(
'Unexpected unmocked jQuery.ajax() call! Make sure to mock jQuery.ajax() in tests.',
);
global.fail(err);
throw err;
};
// jquery is not an ES6 module
module.exports = $;
import $ from 'helpers/jquery';
import $ from 'jquery';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import { mount } from '@vue/test-utils';
......
......@@ -24,7 +24,14 @@ describe('Release detail actions', () => {
let error;
beforeEach(() => {
state = createState();
state = createState({
projectId: '18',
tagName: 'v1.3',
releasesPagePath: 'path/to/releases/page',
markdownDocsPath: 'path/to/markdown/docs',
markdownPreviewPath: 'path/to/markdown/preview',
updateReleaseApiDocsPath: 'path/to/api/docs',
});
release = cloneDeep(originalRelease);
mock = new MockAdapter(axios);
gon.api_version = 'v4';
......@@ -36,16 +43,6 @@ describe('Release detail actions', () => {
mock.restore();
});
describe('setInitialState', () => {
it(`commits ${types.SET_INITIAL_STATE} with the provided object`, () => {
const initialState = {};
return testAction(actions.setInitialState, initialState, state, [
{ type: types.SET_INITIAL_STATE, payload: initialState },
]);
});
});
describe('requestRelease', () => {
it(`commits ${types.REQUEST_RELEASE}`, () =>
testAction(actions.requestRelease, undefined, state, [{ type: types.REQUEST_RELEASE }]));
......
......@@ -5,115 +5,106 @@
* is resolved
*/
import state from '~/releases/stores/modules/detail/state';
import createState from '~/releases/stores/modules/detail/state';
import mutations from '~/releases/stores/modules/detail/mutations';
import * as types from '~/releases/stores/modules/detail/mutation_types';
import { release } from '../../../mock_data';
describe('Release detail mutations', () => {
let stateClone;
let state;
let releaseClone;
beforeEach(() => {
stateClone = state();
releaseClone = JSON.parse(JSON.stringify(release));
});
describe(types.SET_INITIAL_STATE, () => {
it('populates the state with initial values', () => {
const initialState = {
projectId: '18',
tagName: 'v1.3',
releasesPagePath: 'path/to/releases/page',
markdownDocsPath: 'path/to/markdown/docs',
markdownPreviewPath: 'path/to/markdown/preview',
};
mutations[types.SET_INITIAL_STATE](stateClone, initialState);
expect(stateClone).toEqual(expect.objectContaining(initialState));
state = createState({
projectId: '18',
tagName: 'v1.3',
releasesPagePath: 'path/to/releases/page',
markdownDocsPath: 'path/to/markdown/docs',
markdownPreviewPath: 'path/to/markdown/preview',
updateReleaseApiDocsPath: 'path/to/api/docs',
});
releaseClone = JSON.parse(JSON.stringify(release));
});
describe(types.REQUEST_RELEASE, () => {
it('set state.isFetchingRelease to true', () => {
mutations[types.REQUEST_RELEASE](stateClone);
mutations[types.REQUEST_RELEASE](state);
expect(stateClone.isFetchingRelease).toEqual(true);
expect(state.isFetchingRelease).toEqual(true);
});
});
describe(types.RECEIVE_RELEASE_SUCCESS, () => {
it('handles a successful response from the server', () => {
mutations[types.RECEIVE_RELEASE_SUCCESS](stateClone, releaseClone);
mutations[types.RECEIVE_RELEASE_SUCCESS](state, releaseClone);
expect(stateClone.fetchError).toEqual(undefined);
expect(state.fetchError).toEqual(undefined);
expect(stateClone.isFetchingRelease).toEqual(false);
expect(state.isFetchingRelease).toEqual(false);
expect(stateClone.release).toEqual(releaseClone);
expect(state.release).toEqual(releaseClone);
});
});
describe(types.RECEIVE_RELEASE_ERROR, () => {
it('handles an unsuccessful response from the server', () => {
const error = { message: 'An error occurred!' };
mutations[types.RECEIVE_RELEASE_ERROR](stateClone, error);
mutations[types.RECEIVE_RELEASE_ERROR](state, error);
expect(stateClone.isFetchingRelease).toEqual(false);
expect(state.isFetchingRelease).toEqual(false);
expect(stateClone.release).toBeUndefined();
expect(state.release).toBeUndefined();
expect(stateClone.fetchError).toEqual(error);
expect(state.fetchError).toEqual(error);
});
});
describe(types.UPDATE_RELEASE_TITLE, () => {
it("updates the release's title", () => {
stateClone.release = releaseClone;
state.release = releaseClone;
const newTitle = 'The new release title';
mutations[types.UPDATE_RELEASE_TITLE](stateClone, newTitle);
mutations[types.UPDATE_RELEASE_TITLE](state, newTitle);
expect(stateClone.release.name).toEqual(newTitle);
expect(state.release.name).toEqual(newTitle);
});
});
describe(types.UPDATE_RELEASE_NOTES, () => {
it("updates the release's notes", () => {
stateClone.release = releaseClone;
state.release = releaseClone;
const newNotes = 'The new release notes';
mutations[types.UPDATE_RELEASE_NOTES](stateClone, newNotes);
mutations[types.UPDATE_RELEASE_NOTES](state, newNotes);
expect(stateClone.release.description).toEqual(newNotes);
expect(state.release.description).toEqual(newNotes);
});
});
describe(types.REQUEST_UPDATE_RELEASE, () => {
it('set state.isUpdatingRelease to true', () => {
mutations[types.REQUEST_UPDATE_RELEASE](stateClone);
mutations[types.REQUEST_UPDATE_RELEASE](state);
expect(stateClone.isUpdatingRelease).toEqual(true);
expect(state.isUpdatingRelease).toEqual(true);
});
});
describe(types.RECEIVE_UPDATE_RELEASE_SUCCESS, () => {
it('handles a successful response from the server', () => {
mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](stateClone, releaseClone);
mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state, releaseClone);
expect(stateClone.updateError).toEqual(undefined);
expect(state.updateError).toEqual(undefined);
expect(stateClone.isUpdatingRelease).toEqual(false);
expect(state.isUpdatingRelease).toEqual(false);
});
});
describe(types.RECEIVE_UPDATE_RELEASE_ERROR, () => {
it('handles an unsuccessful response from the server', () => {
const error = { message: 'An error occurred!' };
mutations[types.RECEIVE_UPDATE_RELEASE_ERROR](stateClone, error);
mutations[types.RECEIVE_UPDATE_RELEASE_ERROR](state, error);
expect(stateClone.isUpdatingRelease).toEqual(false);
expect(state.isUpdatingRelease).toEqual(false);
expect(stateClone.updateError).toEqual(error);
expect(state.updateError).toEqual(error);
});
});
});
import Vue from 'vue';
import * as jqueryMatchers from 'custom-jquery-matchers';
import $ from 'jquery';
import { config as testUtilsConfig } from '@vue/test-utils';
import Translate from '~/vue_shared/translate';
import { initializeTestTimeout } from './helpers/timeout';
......@@ -9,11 +8,9 @@ import { setupManualMocks } from './mocks/mocks_helper';
import customMatchers from './matchers';
import './helpers/dom_shims';
// Expose jQuery so specs using jQuery plugins can be imported nicely.
// Here is an issue to explore better alternatives:
// https://gitlab.com/gitlab-org/gitlab/issues/12448
window.jQuery = $;
import './helpers/jquery';
import '~/commons/jquery';
import '~/commons/bootstrap';
process.on('unhandledRejection', global.promiseRejectionHandler);
......
......@@ -72,19 +72,6 @@ describe('File row component', () => {
});
});
it('is marked as viewed if clicked', () => {
createComponent({
file: {
...file(),
type: 'blob',
fileHash: '#123456789',
},
level: 0,
viewedFiles: ['#123456789'],
});
expect(wrapper.classes()).toContain('is-viewed');
});
it('indents row based on level', () => {
createComponent({
file: file('t4'),
......
......@@ -1542,16 +1542,54 @@ describe Gitlab::Database::MigrationHelpers do
end
describe '#create_or_update_plan_limit' do
it 'creates or updates plan limits' do
class self::Plan < ActiveRecord::Base
self.table_name = 'plans'
end
class self::PlanLimits < ActiveRecord::Base
self.table_name = 'plan_limits'
end
it 'properly escapes names' do
expect(model).to receive(:execute).with <<~SQL
INSERT INTO plan_limits (plan_id, "project_hooks")
VALUES
((SELECT id FROM plans WHERE name = 'free' LIMIT 1), '10')
SELECT id, '10' FROM plans WHERE name = 'free' LIMIT 1
ON CONFLICT (plan_id) DO UPDATE SET "project_hooks" = EXCLUDED."project_hooks";
SQL
model.create_or_update_plan_limit('project_hooks', 'free', 10)
end
context 'when plan does not exist' do
it 'does not create any plan limits' do
expect { model.create_or_update_plan_limit('project_hooks', 'plan_name', 10) }
.not_to change { self.class::PlanLimits.count }
end
end
context 'when plan does exist' do
let!(:plan) { self.class::Plan.create!(name: 'plan_name') }
context 'when limit does not exist' do
it 'inserts a new plan limits' do
expect { model.create_or_update_plan_limit('project_hooks', 'plan_name', 10) }
.to change { self.class::PlanLimits.count }.by(1)
expect(self.class::PlanLimits.pluck(:project_hooks)).to contain_exactly(10)
end
end
context 'when limit does exist' do
let!(:plan_limit) { self.class::PlanLimits.create!(plan_id: plan.id) }
it 'updates an existing plan limits' do
expect { model.create_or_update_plan_limit('project_hooks', 'plan_name', 999) }
.not_to change { self.class::PlanLimits.count }
expect(plan_limit.reload.project_hooks).to eq(999)
end
end
end
end
describe '#with_lock_retries' do
......
This diff is collapsed.
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::X509::Signature do
let(:issuer_attributes) do
{
subject_key_identifier: X509Helpers::User1.issuer_subject_key_identifier,
subject: X509Helpers::User1.certificate_issuer,
crl_url: X509Helpers::User1.certificate_crl
}
end
context 'commit signature' do
let(:certificate_attributes) do
{
subject_key_identifier: X509Helpers::User1.certificate_subject_key_identifier,
subject: X509Helpers::User1.certificate_subject,
email: X509Helpers::User1.certificate_email,
serial_number: X509Helpers::User1.certificate_serial
}
end
context 'verified signature' do
context 'with trusted certificate store' do
before do
store = OpenSSL::X509::Store.new
certificate = OpenSSL::X509::Certificate.new(X509Helpers::User1.trust_cert)
store.add_cert(certificate)
allow(OpenSSL::X509::Store).to receive(:new).and_return(store)
end
it 'returns a verified signature if email does match' do
signature = described_class.new(
X509Helpers::User1.signed_commit_signature,
X509Helpers::User1.signed_commit_base_data,
X509Helpers::User1.certificate_email,
X509Helpers::User1.signed_commit_time
)
expect(signature.x509_certificate).to have_attributes(certificate_attributes)
expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
expect(signature.verified_signature).to be_truthy
expect(signature.verification_status).to eq(:verified)
end
it 'returns an unverified signature if email does not match' do
signature = described_class.new(
X509Helpers::User1.signed_commit_signature,
X509Helpers::User1.signed_commit_base_data,
"gitlab@example.com",
X509Helpers::User1.signed_commit_time
)
expect(signature.x509_certificate).to have_attributes(certificate_attributes)
expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
expect(signature.verified_signature).to be_truthy
expect(signature.verification_status).to eq(:unverified)
end
it 'returns an unverified signature if email does match and time is wrong' do
signature = described_class.new(
X509Helpers::User1.signed_commit_signature,
X509Helpers::User1.signed_commit_base_data,
X509Helpers::User1.certificate_email,
Time.new(2020, 2, 22)
)
expect(signature.x509_certificate).to have_attributes(certificate_attributes)
expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
expect(signature.verified_signature).to be_falsey
expect(signature.verification_status).to eq(:unverified)
end
it 'returns an unverified signature if certificate is revoked' do
signature = described_class.new(
X509Helpers::User1.signed_commit_signature,
X509Helpers::User1.signed_commit_base_data,
X509Helpers::User1.certificate_email,
X509Helpers::User1.signed_commit_time
)
expect(signature.verification_status).to eq(:verified)
signature.x509_certificate.revoked!
expect(signature.verification_status).to eq(:unverified)
end
end
context 'without trusted certificate within store' do
before do
store = OpenSSL::X509::Store.new
allow(OpenSSL::X509::Store).to receive(:new)
.and_return(
store
)
end
it 'returns an unverified signature' do
signature = described_class.new(
X509Helpers::User1.signed_commit_signature,
X509Helpers::User1.signed_commit_base_data,
X509Helpers::User1.certificate_email,
X509Helpers::User1.signed_commit_time
)
expect(signature.x509_certificate).to have_attributes(certificate_attributes)
expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
expect(signature.verified_signature).to be_falsey
expect(signature.verification_status).to eq(:unverified)
end
end
end
context 'invalid signature' do
it 'returns nil' do
signature = described_class.new(
X509Helpers::User1.signed_commit_signature.tr('A', 'B'),
X509Helpers::User1.signed_commit_base_data,
X509Helpers::User1.certificate_email,
X509Helpers::User1.signed_commit_time
)
expect(signature.x509_certificate).to be_nil
expect(signature.verified_signature).to be_falsey
expect(signature.verification_status).to eq(:unverified)
end
end
context 'invalid commit message' do
it 'returns nil' do
signature = described_class.new(
X509Helpers::User1.signed_commit_signature,
'x',
X509Helpers::User1.certificate_email,
X509Helpers::User1.signed_commit_time
)
expect(signature.x509_certificate).to be_nil
expect(signature.verified_signature).to be_falsey
expect(signature.verification_status).to eq(:unverified)
end
end
end
context 'certificate_crl' do
describe 'valid crlDistributionPoints' do
before do
allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension).and_call_original
allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension)
.with('crlDistributionPoints')
.and_return("\nFull Name:\n URI:http://ch.siemens.com/pki?ZZZZZZA2.crl\n URI:ldap://cl.siemens.net/CN=ZZZZZZA2,L=PKI?certificateRevocationList\n URI:ldap://cl.siemens.com/CN=ZZZZZZA2,o=Trustcenter?certificateRevocationList\n")
end
it 'creates an issuer' do
signature = described_class.new(
X509Helpers::User1.signed_commit_signature,
X509Helpers::User1.signed_commit_base_data,
X509Helpers::User1.certificate_email,
X509Helpers::User1.signed_commit_time
)
expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
end
end
describe 'valid crlDistributionPoints providing multiple http URIs' do
before do
allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension).and_call_original
allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension)
.with('crlDistributionPoints')
.and_return("\nFull Name:\n URI:http://cdp1.pca.dfn.de/dfn-ca-global-g2/pub/crl/cacrl.crl\n\nFull Name:\n URI:http://cdp2.pca.dfn.de/dfn-ca-global-g2/pub/crl/cacrl.crl\n")
end
it 'extracts the first URI' do
signature = described_class.new(
X509Helpers::User1.signed_commit_signature,
X509Helpers::User1.signed_commit_base_data,
X509Helpers::User1.certificate_email,
X509Helpers::User1.signed_commit_time
)
expect(signature.x509_certificate.x509_issuer.crl_url).to eq("http://cdp1.pca.dfn.de/dfn-ca-global-g2/pub/crl/cacrl.crl")
end
end
end
context 'email' do
describe 'subjectAltName with email, othername' do
before do
allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension).and_call_original
allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension)
.with('subjectAltName')
.and_return("email:gitlab@example.com, othername:<unsupported>")
end
it 'extracts email' do
signature = described_class.new(
X509Helpers::User1.signed_commit_signature,
X509Helpers::User1.signed_commit_base_data,
'gitlab@example.com',
X509Helpers::User1.signed_commit_time
)
expect(signature.x509_certificate.email).to eq("gitlab@example.com")
end
end
describe 'subjectAltName with othername, email' do
before do
allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension).and_call_original
allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension)
.with('subjectAltName')
.and_return("othername:<unsupported>, email:gitlab@example.com")
end
it 'extracts email' do
signature = described_class.new(
X509Helpers::User1.signed_commit_signature,
X509Helpers::User1.signed_commit_base_data,
'gitlab@example.com',
X509Helpers::User1.signed_commit_time
)
expect(signature.x509_certificate.email).to eq("gitlab@example.com")
end
end
end
end
......@@ -46,7 +46,7 @@ describe API::PipelineSchedules do
get api("/projects/#{project.id}/pipeline_schedules", developer)
end.count
create_pipeline_schedules(10)
create_pipeline_schedules(5)
expect do
get api("/projects/#{project.id}/pipeline_schedules", developer)
......
......@@ -169,6 +169,10 @@ module X509Helpers
SIGNEDDATA
end
def signed_commit_time
Time.at(1561027326)
end
def certificate_crl
'http://ch.siemens.com/pki?ZZZZZZA2.crl'
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