Commit d44305eb authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into 30634-protected-pipeline

* upstream/master: (56 commits)
  Resolve "When changing project visibility setting, change other dropdowns automatically"
  change headings to improve SEO
  backports changed import logic from pull mirroring feature into CE
  Fix header component spec
  Add a Rake task to aid in rotating otp_key_base
  Remove unnecessary variable
  Add changelog entry
  Allow users to be hard-deleted from the admin user show page
  Allow users to be hard-removed from the admin users list page
  Support hard deletion in Admin::UsersController#destroy
  Add changelog entry
  Extract and memoize `user_access`
  Remove GitAccessStatus (no longer needed)
  Refactor construction of response
  Refactor to remove a special case
  Fix would-be regression
  Clarify error messages
  Refactor to let GitAccess errors bubble up
  Refactor to let `GitAccess` check protocol config
  Specify new Git-LFS-over-HTTP behavior
  ...
parents 47b93fd7 4a811fbc
......@@ -56,6 +56,8 @@
if (job.import_status === 'finished') {
job_item.removeClass("active").addClass("success");
return status_field.html('<span><i class="fa fa-check"></i> done</span>');
} else if (job.import_status === 'scheduled') {
return status_field.html("<i class='fa fa-spinner fa-spin'></i> scheduled");
} else if (job.import_status === 'started') {
return status_field.html("<i class='fa fa-spinner fa-spin'></i> started");
} else {
......
var locales = locales || {}; locales['zh_CN'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (China) (https://www.transifex.com/gitlab-zh/teams/75177/zh_CN/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_CN","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_CN","domain":"app","plural_forms":"nplurals=1; plural=0;"},"ByAuthor|by":["作者:"],"Commit":["提交"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["周期分析概述了项目从想法到产品实现的各阶段所需的时间。"],"CycleAnalyticsStage|Code":["编码"],"CycleAnalyticsStage|Issue":["议题"],"CycleAnalyticsStage|Plan":["计划"],"CycleAnalyticsStage|Production":["生产"],"CycleAnalyticsStage|Review":["评审"],"CycleAnalyticsStage|Staging":["预发布"],"CycleAnalyticsStage|Test":["测试"],"Deploy":["部署"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["从创建议题到部署至生产环境"],"From merge request merge until deploy to production":["从合并请求被合并后到部署至生产环境"],"Introducing Cycle Analytics":["周期分析简介"],"Last %d day":["最后 %d 天"],"Limited to showing %d event at most":["最多显示 %d 个事件"],"Median":["中位数"],"New Issue":["新议题"],"Not available":["数据不足"],"Not enough data":["数据不足"],"OpenedNDaysAgo|Opened":["开始于"],"Pipeline Health":["流水线健康指标"],"ProjectLifecycle|Stage":["项目生命周期"],"Read more":["了解更多"],"Related Commits":["相关的提交"],"Related Deployed Jobs":["相关的部署作业"],"Related Issues":["相关的议题"],"Related Jobs":["相关的作业"],"Related Merge Requests":["相关的合并请求"],"Related Merged Requests":["相关已合并的合并请求"],"Showing %d event":["显示 %d 个事件"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["编码阶段概述了从第一次提交到创建合并请求的时间。创建第一个合并请求后,数据将自动添加到此处。"],"The collection of events added to the data gathered for that stage.":["与该阶段相关的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["议题阶段概述了从创建议题到将议题设置里程碑或将议题添加到议题看板的时间。开始创建议题以查看此阶段的数据。"],"The phase of the development lifecycle.":["项目生命周期中的各个阶段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["计划阶段概述了从议题添加到日程后到推送首次提交的时间。当首次推送提交后,数据将自动添加到此处。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生产阶段概述了从创建一个议题到将代码部署到生产环境的总时间。当完成想法到部署生产的循环,数据将自动添加到此处。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["评审阶段概述了从创建合并请求到被合并的时间。当创建第一个合并请求后,数据将自动添加到此处。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["预发布阶段概述了从合并请求被合并到部署至生产环境的总时间。首次部署到生产环境后,数据将自动添加到此处。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["测试阶段概述了GitLab CI为相关合并请求运行每个流水线所需的时间。当第一个流水线运行完成后,数据将自动添加到此处。"],"The time taken by each data entry gathered by that stage.":["该阶段每条数据所花的时间"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位数是一个数列中最中间的值。例如在 3、5、9 之间,中位数是 5。在 3、5、7、8 之间,中位数是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["议题被列入日程表的时间"],"Time before an issue starts implementation":["开始进行编码前的时间"],"Time between merge request creation and merge/close":["从创建合并请求到被合并或关闭的时间"],"Time until first merge request":["创建第一个合并请求之前的时间"],"Time|hr":["小时"],"Time|min":["分钟"],"Time|s":[""],"Total Time":["总时间"],"Total test time for all commits/merges":["所有提交和合并的总测试时间"],"Want to see the data? Please ask an administrator for access.":["权限不足。如需查看相关数据,请向管理员申请权限。"],"We don't have enough data to show this stage.":["该阶段的数据不足,无法显示。"],"You need permission.":["您需要相关的权限。"],"day":[""]}}};
\ No newline at end of file
var locales = locales || {}; locales['zh_HK'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (Hong Kong) (https://www.transifex.com/gitlab-zh/teams/75177/zh_HK/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_HK","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_HK","domain":"app","plural_forms":"nplurals=1; plural=0;"},"ByAuthor|by":["作者:"],"Commit":["提交"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了項目從想法到產品實現的各階段所需的時間。"],"CycleAnalyticsStage|Code":["編碼"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["生產"],"CycleAnalyticsStage|Review":["評審"],"CycleAnalyticsStage|Staging":["預發布"],"CycleAnalyticsStage|Test":["測試"],"Deploy":["部署"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["從創建議題到部署到生產環境"],"From merge request merge until deploy to production":["從合併請求的合併到部署至生產環境"],"Introducing Cycle Analytics":["週期分析簡介"],"Last %d day":["最後 %d 天"],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"New Issue":["新議題"],"Not available":["不可用"],"Not enough data":["數據不足"],"OpenedNDaysAgo|Opened":["開始於"],"Pipeline Health":["流水線健康指標"],"ProjectLifecycle|Stage":["項目生命週期"],"Read more":["了解更多"],"Related Commits":["相關的提交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的合並請求"],"Showing %d event":["顯示 %d 個事件"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["編碼階段概述了從第一次提交到創建合併請求的時間。創建第壹個合並請求後,數據將自動添加到此處。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["議題階段概述了從創建議題到將議題設置裏程碑或將議題添加到議題看板的時間。創建一個議題後,數據將自動添加到此處。"],"The phase of the development lifecycle.":["項目生命週期中的各個階段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["計劃階段概述了從議題添加到日程後到推送首次提交的時間。當首次推送提交後,數據將自動添加到此處。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生產階段概述了從創建議題到將代碼部署到生產環境的時間。當完成完整的想法到部署生產,數據將自動添加到此處。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["評審階段概述了從創建合並請求到合併的時間。當創建第壹個合並請求後,數據將自動添加到此處。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["預發布階段概述了合並請求的合併到部署代碼到生產環境的總時間。當首次部署到生產環境後,數據將自動添加到此處。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["測試階段概述了GitLab CI為相關合併請求運行每個流水線所需的時間。當第壹個流水線運行完成後,數據將自動添加到此處。"],"The time taken by each data entry gathered by that stage.":["該階段每條數據所花的時間"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["開始進行編碼前的時間"],"Time between merge request creation and merge/close":["從創建合併請求到被合並或關閉的時間"],"Time until first merge request":["創建第壹個合併請求之前的時間"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":[""],"Total Time":["總時間"],"Total test time for all commits/merges":["所有提交和合併的總測試時間"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關數據,請向管理員申請權限。"],"We don't have enough data to show this stage.":["該階段的數據不足,無法顯示。"],"You need permission.":["您需要相關的權限。"],"day":[""]}}};
\ No newline at end of file
var locales = locales || {}; locales['zh_TW'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (Taiwan) (https://www.transifex.com/gitlab-zh/teams/75177/zh_TW/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_TW","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_TW","domain":"app","plural_forms":"nplurals=1; plural=0;"},"ByAuthor|by":["作者:"],"Commit":["送交"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了你的專案從想法到產品實現,各階段所需的時間。"],"CycleAnalyticsStage|Code":["程式開發"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["上線"],"CycleAnalyticsStage|Review":["複閱"],"CycleAnalyticsStage|Staging":["預備"],"CycleAnalyticsStage|Test":["測試"],"Deploy":["部署"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["從議題建立至線上部署"],"From merge request merge until deploy to production":["從請求被合併後至線上部署"],"Introducing Cycle Analytics":["週期分析簡介"],"Last %d day":["最後 %d 天"],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"New Issue":["新議題"],"Not available":["無法使用"],"Not enough data":["資料不足"],"OpenedNDaysAgo|Opened":["開始於"],"Pipeline Health":["流水線健康指標"],"ProjectLifecycle|Stage":["專案生命週期"],"Read more":["了解更多"],"Related Commits":["相關的送交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的請求"],"Showing %d event":["顯示 %d 個事件"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["程式開發階段顯示從第一次送交到建立合併請求的時間。建立第一個合併請求後,資料將自動填入。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["議題階段顯示從議題建立到設置里程碑、或將該議題加至議題看板的時間。建立第一個議題後,資料將自動填入。"],"The phase of the development lifecycle.":["專案開發生命週期的各個階段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["計劃階段顯示從議題添加到日程後至推送第一個送交的時間。當第一次推送送交後,資料將自動填入。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["上線階段顯示從建立一個議題到部署程式至線上的總時間。當完成從想法到產品實現的循環後,資料將自動填入。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["複閱階段顯示從合併請求建立後至被合併的時間。當建立第一個合併請求後,資料將自動填入。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["預備階段顯示從合併請求被合併後至部署上線的時間。當第一次部署上線後,資料將自動填入。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["測試階段顯示相關合併請求的流水線所花的時間。當第一個流水線運作完畢後,資料將自動填入。"],"The time taken by each data entry gathered by that stage.":["每筆該階段相關資料所花的時間。"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["議題等待開始實作的時間"],"Time between merge request creation and merge/close":["合併請求被合併或是關閉的時間"],"Time until first merge request":["第一個合併請求被建立前的時間"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":[""],"Total Time":["總時間"],"Total test time for all commits/merges":["所有送交和合併的總測試時間"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關資料,請向管理員申請權限。"],"We don't have enough data to show this stage.":["因該階段的資料不足而無法顯示相關資訊"],"You need permission.":["您需要相關的權限。"],"day":[""]}}};
\ No newline at end of file
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */
function highlightChanges($elm) {
$elm.addClass('highlight-changes');
setTimeout(() => $elm.removeClass('highlight-changes'), 10);
}
(function() {
this.ProjectNew = (function() {
function ProjectNew() {
this.toggleSettings = this.toggleSettings.bind(this);
this.$selects = $('.features select');
this.$repoSelects = this.$selects.filter('.js-repo-select');
this.$projectSelects = this.$selects.not('.js-repo-select');
$('.project-edit-container').on('ajax:before', (function(_this) {
return function() {
......@@ -26,6 +32,42 @@
if (!visibilityContainer) return;
const visibilitySelect = new gl.VisibilitySelect(visibilityContainer);
visibilitySelect.init();
const $visibilitySelect = $(visibilityContainer).find('select');
let projectVisibility = $visibilitySelect.val();
const PROJECT_VISIBILITY_PRIVATE = '0';
$visibilitySelect.on('change', () => {
const newProjectVisibility = $visibilitySelect.val();
if (projectVisibility !== newProjectVisibility) {
this.$projectSelects.each((idx, select) => {
const $select = $(select);
const $options = $select.find('option');
const values = $.map($options, e => e.value);
// if switched to "private", limit visibility options
if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) {
if ($select.val() !== values[0] && $select.val() !== values[1]) {
$select.val(values[1]).trigger('change');
highlightChanges($select);
}
$options.slice(2).disable();
}
// if switched from "private", increase visibility for non-disabled options
if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) {
$options.enable();
if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) {
$select.val(values[values.length - 1]).trigger('change');
highlightChanges($select);
}
}
});
projectVisibility = newProjectVisibility;
}
});
};
ProjectNew.prototype.toggleSettings = function() {
......@@ -56,8 +98,10 @@
ProjectNew.prototype.toggleRepoVisibility = function () {
var $repoAccessLevel = $('.js-repo-access-level select');
var $lfsEnabledOption = $('.js-lfs-enabled select');
var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
var prevSelectedVal = parseInt($repoAccessLevel.val(), 10);
this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
.nextAll()
......@@ -71,29 +115,40 @@
var $this = $(this);
var repoSelectVal = parseInt($this.val(), 10);
$this.find('option').show();
$this.find('option').enable();
if (selectedVal < repoSelectVal) {
$this.val(selectedVal);
if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) {
$this.val(selectedVal).trigger('change');
highlightChanges($this);
}
$this.find("option[value='" + selectedVal + "']").nextAll().hide();
$this.find("option[value='" + selectedVal + "']").nextAll().disable();
});
if (selectedVal) {
this.$repoSelects.removeClass('disabled');
if ($lfsEnabledOption.length) {
$lfsEnabledOption.removeClass('disabled');
highlightChanges($lfsEnabledOption);
}
if (containerRegistry) {
containerRegistry.style.display = '';
}
} else {
this.$repoSelects.addClass('disabled');
if ($lfsEnabledOption.length) {
$lfsEnabledOption.val('false').addClass('disabled');
highlightChanges($lfsEnabledOption);
}
if (containerRegistry) {
containerRegistry.style.display = 'none';
containerRegistryCheckbox.checked = false;
}
}
prevSelectedVal = selectedVal;
}.bind(this));
};
......
......@@ -10,7 +10,7 @@ export default class ProtectedTagDropdown {
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
this.$protectedTag = this.$dropdownContainer.find('.create-new-protected-tag');
this.$protectedTag = this.$dropdownContainer.find('.js-create-new-protected-tag');
this.buildDropdown();
this.bindEvents();
......@@ -73,7 +73,7 @@ export default class ProtectedTagDropdown {
};
this.$dropdownContainer
.find('.create-new-protected-tag code')
.find('.js-create-new-protected-tag code')
.text(tagName);
}
......
......@@ -187,6 +187,7 @@ $divergence-graph-bar-bg: #ccc;
$divergence-graph-separator-bg: #ccc;
$general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232);
/*
......
......@@ -29,6 +29,20 @@
& > .form-group {
padding-left: 0;
}
select option[disabled] {
display: none;
}
}
select {
background: transparent;
transition: background 2s ease-out;
&.highlight-changes {
background: $highlight-changes-color;
transition: none;
}
}
.help-block {
......@@ -675,14 +689,16 @@ pre.light-well {
}
}
.new_protected_branch {
.new_protected_branch,
.new-protected-tag {
label {
margin-top: 6px;
font-weight: normal;
}
}
.create-new-protected-branch-button {
.create-new-protected-branch-button,
.create-new-protected-tag-button {
@include dropdown-link;
width: 100%;
......
......@@ -138,7 +138,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def destroy
DeleteUserWorker.perform_async(current_user.id, user.id)
user.delete_async(deleted_by: current_user, params: params.permit(:hard_delete))
respond_to do |format|
format.html { redirect_to admin_users_path, notice: "The user is being deleted." }
......
......@@ -106,4 +106,8 @@ module LfsRequest
def objects
@objects ||= (params[:objects] || []).to_a
end
def has_authentication_ability?(capability)
(authentication_abilities || []).include?(capability)
end
end
......@@ -128,32 +128,10 @@ class Projects::GitHttpClientController < Projects::ApplicationController
@authentication_result = Gitlab::Auth.find_for_git_client(
login, password, project: project, ip: request.ip)
return false unless @authentication_result.success?
if download_request?
authentication_has_download_access?
else
authentication_has_upload_access?
end
@authentication_result.success?
end
def ci?
authentication_result.ci?(project)
end
def authentication_has_download_access?
has_authentication_ability?(:download_code) || has_authentication_ability?(:build_download_code)
end
def authentication_has_upload_access?
has_authentication_ability?(:push_code)
end
def has_authentication_ability?(capability)
(authentication_abilities || []).include?(capability)
end
def authentication_project
authentication_result.project
end
end
class Projects::GitHttpController < Projects::GitHttpClientController
include WorkhorseRequest
before_action :access_check
rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403
rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
def info_refs
if upload_pack? && upload_pack_allowed?
log_user_activity
render_ok
elsif receive_pack? && receive_pack_allowed?
render_ok
elsif http_blocked?
render_http_not_allowed
else
render_denied
end
log_user_activity if upload_pack?
render_ok
end
# POST /foo/bar.git/git-upload-pack (git pull)
def git_upload_pack
if upload_pack? && upload_pack_allowed?
render_ok
else
render_denied
end
render_ok
end
# POST /foo/bar.git/git-receive-pack" (git push)
def git_receive_pack
if receive_pack? && receive_pack_allowed?
render_ok
else
render_denied
end
render_ok
end
private
......@@ -45,10 +34,6 @@ class Projects::GitHttpController < Projects::GitHttpClientController
git_command == 'git-upload-pack'
end
def receive_pack?
git_command == 'git-receive-pack'
end
def git_command
if action_name == 'info_refs'
params[:service]
......@@ -62,47 +47,27 @@ class Projects::GitHttpController < Projects::GitHttpClientController
render json: Gitlab::Workhorse.git_http_ok(repository, wiki?, user, action_name)
end
def render_http_not_allowed
render plain: access_check.message, status: :forbidden
def render_403(exception)
render plain: exception.message, status: :forbidden
end
def render_denied
if user && can?(user, :read_project, project)
render plain: access_denied_message, status: :forbidden
else
# Do not leak information about project existence
render_not_found
end
end
def access_denied_message
'Access denied'
def render_404(exception)
render plain: exception.message, status: :not_found
end
def upload_pack_allowed?
return false unless Gitlab.config.gitlab_shell.upload_pack
access_check.allowed? || ci?
def access
@access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities)
end
def access
@access ||= access_klass.new(user, project, 'http', authentication_abilities: authentication_abilities)
def access_actor
return user if user
return :ci if ci?
end
def access_check
# Use the magic string '_any' to indicate we do not know what the
# changes are. This is also what gitlab-shell does.
@access_check ||= access.check(git_command, '_any')
end
def http_blocked?
!access.protocol_allowed?
end
def receive_pack_allowed?
return false unless Gitlab.config.gitlab_shell.receive_pack
access_check.allowed?
access.check(git_command, '_any')
end
def access_klass
......
......@@ -14,14 +14,7 @@ class Projects::ImportsController < Projects::ApplicationController
@project.import_url = params[:project][:import_url]
if @project.save
@project.reload
if @project.import_failed?
@project.import_retry
else
@project.import_start
@project.add_import_job
end
@project.reload.import_schedule
end
redirect_to namespace_project_import_path(@project.namespace, @project)
......
......@@ -19,7 +19,7 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
def protected_ref_params
params.require(:protected_branch).permit(:name,
merge_access_levels_attributes: [:access_level, :id],
push_access_levels_attributes: [:access_level, :id])
merge_access_levels_attributes: access_level_attributes,
push_access_levels_attributes: access_level_attributes)
end
end
......@@ -44,4 +44,10 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
format.js { head :ok }
end
end
protected
def access_level_attributes
%i(access_level id)
end
end
......@@ -18,6 +18,6 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController
end
def protected_ref_params
params.require(:protected_tag).permit(:name, create_access_levels_attributes: [:access_level, :id])
params.require(:protected_tag).permit(:name, create_access_levels_attributes: access_level_attributes)
end
end
......@@ -25,7 +25,7 @@ class RegistrationsController < Devise::RegistrationsController
end
def destroy
DeleteUserWorker.perform_async(current_user.id, current_user.id)
current_user.delete_async(deleted_by: current_user)
respond_to do |format|
format.html do
......
......@@ -138,11 +138,15 @@ module ProjectsHelper
if @project.private?
level = @project.project_feature.send(field)
options.delete('Everyone with access')
highest_available_option = options.values.max if level == ProjectFeature::ENABLED
disabled_option = ProjectFeature::ENABLED
highest_available_option = ProjectFeature::PRIVATE if level == disabled_option
end
options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field))
options = options_for_select(
options,
selected: highest_available_option || @project.project_feature.public_send(field),
disabled: disabled_option
)
content_tag(
:select,
......
......@@ -13,6 +13,17 @@ module SubmoduleHelper
if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
namespace, project = $1, $2
gitlab_hosts = [Gitlab.config.gitlab.url,
Gitlab.config.gitlab_shell.ssh_path_prefix]
gitlab_hosts.each do |host|
if url.start_with?(host)
namespace, _, project = url.sub(host, '').rpartition('/')
break
end
end
namespace.sub!(/\A\//, '')
project.rstrip!
project.sub!(/\.git\z/, '')
......
......@@ -31,9 +31,9 @@ module VisibilityLevelHelper
when Gitlab::VisibilityLevel::PRIVATE
"Project access must be granted explicitly to each user."
when Gitlab::VisibilityLevel::INTERNAL
"The project can be cloned by any logged in user."
"The project can be accessed by any logged in user."
when Gitlab::VisibilityLevel::PUBLIC
"The project can be cloned without any authentication."
"The project can be accessed without any authentication."
end
end
......
......@@ -15,8 +15,7 @@ class AbuseReport < ActiveRecord::Base
alias_method :author, :reporter
def remove_user(deleted_by:)
user.block
DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true, hard_delete: true)
user.delete_async(deleted_by: deleted_by, params: { hard_delete: true })
end
def notify
......
......@@ -199,7 +199,7 @@ class ApplicationSetting < ActiveRecord::Base
ApplicationSetting.define_attribute_methods
end
def self.defaults_ce
def self.defaults
{
after_sign_up_text: nil,
akismet_enabled: false,
......@@ -250,10 +250,6 @@ class ApplicationSetting < ActiveRecord::Base
}
end
def self.defaults
defaults_ce
end
def self.create_from_defaults
create(defaults)
end
......
......@@ -8,32 +8,44 @@ module ProtectedRef
validates :project, presence: true
delegate :matching, :matches?, :wildcard?, to: :ref_matcher
end
def commit
project.commit(self.name)
end
class_methods do
def protected_ref_access_levels(*types)
types.each do |type|
has_many :"#{type}_access_levels", dependent: :destroy
validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." }
def self.protected_ref_accessible_to?(ref, user, action:)
accepts_nested_attributes_for :"#{type}_access_levels", allow_destroy: true
end
end
def protected_ref_accessible_to?(ref, user, action:)
access_levels_for_ref(ref, action: action).any? do |access_level|
access_level.check_access(user)
end
end
def self.developers_can?(action, ref)
def developers_can?(action, ref)
access_levels_for_ref(ref, action: action).any? do |access_level|
access_level.access_level == Gitlab::Access::DEVELOPER
end
end
def self.access_levels_for_ref(ref, action:)
def access_levels_for_ref(ref, action:)
self.matching(ref).map(&:"#{action}_access_levels").flatten
end
def self.matching(ref_name, protected_refs: nil)
def matching(ref_name, protected_refs: nil)
ProtectedRefMatcher.matching(self, ref_name, protected_refs: protected_refs)
end
end
def commit
project.commit(self.name)
end
private
def ref_matcher
......
......@@ -222,6 +222,16 @@ class Group < Namespace
User.where(id: members_with_parents.select(:user_id))
end
def max_member_access_for_user(user)
return GroupMember::OWNER if user.admin?
members_with_parents.
where(user_id: user).
reorder(access_level: :desc).
first&.
access_level || GroupMember::NO_ACCESS
end
def mattermost_team_params
max_length = 59
......
......@@ -200,6 +200,10 @@ class Member < ActiveRecord::Base
source_type
end
def access_field
access_level
end
def invite?
self.invite_token.present?
end
......
......@@ -25,10 +25,6 @@ class GroupMember < Member
source
end
def access_field
access_level
end
# Because source_type is `Namespace`...
def real_source_type
'Group'
......
......@@ -79,10 +79,6 @@ class ProjectMember < Member
end
end
def access_field
access_level
end
def project
source
end
......
......@@ -165,7 +165,7 @@ class Project < ActiveRecord::Base
has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy, as: :source
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_one :import_data, dependent: :delete, class_name: "ProjectImportData"
has_one :project_feature, dependent: :destroy
has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
has_many :container_repositories, dependent: :destroy
......@@ -298,8 +298,16 @@ class Project < ActiveRecord::Base
scope :excluding_project, ->(project) { where.not(id: project) }
state_machine :import_status, initial: :none do
event :import_schedule do
transition [:none, :finished, :failed] => :scheduled
end
event :force_import_start do
transition [:none, :finished, :failed] => :started
end
event :import_start do
transition [:none, :finished] => :started
transition scheduled: :started
end
event :import_finish do
......@@ -307,18 +315,23 @@ class Project < ActiveRecord::Base
end
event :import_fail do
transition started: :failed
transition [:scheduled, :started] => :failed
end
event :import_retry do
transition failed: :started
end
state :scheduled
state :started
state :finished
state :failed
after_transition any => :finished, do: :reset_cache_and_import_attrs
after_transition [:none, :finished, :failed] => :scheduled do |project, _|
project.run_after_commit { add_import_job }
end
after_transition started: :finished, do: :reset_cache_and_import_attrs
end
class << self
......@@ -532,9 +545,17 @@ class Project < ActiveRecord::Base
end
def import_in_progress?
import_started? || import_scheduled?
end
def import_started?
import? && import_status == 'started'
end
def import_scheduled?
import_status == 'scheduled'
end
def import_failed?
import_status == 'failed'
end
......
......@@ -2,14 +2,7 @@ class ProtectedBranch < ActiveRecord::Base
include Gitlab::ShellAdapter
include ProtectedRef
has_many :merge_access_levels, dependent: :destroy
has_many :push_access_levels, dependent: :destroy
validates :merge_access_levels, length: { is: 1, message: "are restricted to a single instance per protected branch." }
validates :push_access_levels, length: { is: 1, message: "are restricted to a single instance per protected branch." }
accepts_nested_attributes_for :push_access_levels
accepts_nested_attributes_for :merge_access_levels
protected_ref_access_levels :merge, :push
# Check if branch name is marked as protected in the system
def self.protected?(project, ref_name)
......
......@@ -2,11 +2,7 @@ class ProtectedTag < ActiveRecord::Base
include Gitlab::ShellAdapter
include ProtectedRef
has_many :create_access_levels, dependent: :destroy
validates :create_access_levels, length: { is: 1, message: "are restricted to a single instance per protected tag." }
accepts_nested_attributes_for :create_access_levels
protected_ref_access_levels :create
def self.protected?(project, ref_name)
self.matching(ref_name, protected_refs: project.protected_tags).present?
......
......@@ -4,8 +4,7 @@ class SpamLog < ActiveRecord::Base
validates :user, presence: true
def remove_user(deleted_by:)
user.block
DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true, hard_delete: true)
user.delete_async(deleted_by: deleted_by, params: { hard_delete: true })
end
def text
......
......@@ -809,6 +809,11 @@ class User < ActiveRecord::Base
system_hook_service.execute_hooks_for(self, :destroy)
end
def delete_async(deleted_by:, params: {})
block if params[:hard_delete]
DeleteUserWorker.perform_async(deleted_by.id, id, params)
end
def notification_service
NotificationService.new
end
......
......@@ -4,22 +4,25 @@ class GroupPolicy < BasePolicy
return unless @user
globally_viewable = @subject.public? || (@subject.internal? && !@user.external?)
member = @subject.users_with_parents.include?(@user)
owner = @user.admin? || @subject.has_owner?(@user)
master = owner || @subject.has_master?(@user)
access_level = @subject.max_member_access_for_user(@user)
owner = access_level >= GroupMember::OWNER
master = access_level >= GroupMember::MASTER
reporter = access_level >= GroupMember::REPORTER
can_read = false
can_read ||= globally_viewable
can_read ||= member
can_read ||= @user.admin?
can_read ||= access_level >= GroupMember::GUEST
can_read ||= GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
can! :read_group if can_read
if reporter
can! :admin_label
end
# Only group masters and group owners can create new projects
if master
can! :create_projects
can! :admin_milestones
can! :admin_label
end
# Only group owner and administrators can admin group
......@@ -31,7 +34,7 @@ class GroupPolicy < BasePolicy
can! :create_subgroup if @user.can_create_group
end
if globally_viewable && @subject.request_access_enabled && !member
if globally_viewable && @subject.request_access_enabled && access_level == GroupMember::NO_ACCESS
can! :request_access
end
end
......
......@@ -48,15 +48,14 @@ module Projects
save_project_and_import_data(import_data)
@project.import_start if @project.import?
after_create_actions if @project.persisted?
if @project.errors.empty?
@project.add_import_job if @project.import?
@project.import_schedule if @project.import?
else
fail(error: @project.errors.full_messages.join(', '))
end
@project
rescue ActiveRecord::RecordInvalid => e
message = "Unable to save #{e.record.type}: #{e.record.errors.full_messages.join(", ")} "
......
......@@ -16,7 +16,7 @@ module Users
def record_activity
Gitlab::UserActivities.record(@author.id)
Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username}")
Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username})")
end
end
end
......@@ -6,12 +6,27 @@ module Users
@current_user = current_user
end
# Synchronously destroys +user+
#
# The operation will fail if the user is the sole owner of any groups. To
# force the groups to be destroyed, pass `delete_solo_owned_groups: true` in
# +options+.
#
# The user's contributions will be migrated to a global ghost user. To
# force the contributions to be destroyed, pass `hard_delete: true` in
# +options+.
#
# `hard_delete: true` implies `delete_solo_owned_groups: true`. To perform
# a hard deletion without destroying solo-owned groups, pass
# `delete_solo_owned_groups: false, hard_delete: true` in +options+.
def execute(user, options = {})
delete_solo_owned_groups = options.fetch(:delete_solo_owned_groups, options[:hard_delete])
unless Ability.allowed?(current_user, :destroy_user, user)
raise Gitlab::Access::AccessDeniedError, "#{current_user} tried to destroy user #{user}!"
end
if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present?
if !delete_solo_owned_groups && user.solo_owned_groups.present?
user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user'
return user
end
......
......@@ -34,9 +34,15 @@
- if user.access_locked?
%li
= link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
- if user.can_be_removed? && can?(current_user, :destroy_user, user)
- if can?(current_user, :destroy_user, user)
%li.divider
- if user.can_be_removed?
%li
= link_to 'Remove user', admin_user_path(user),
data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" },
method: :delete
%li
= link_to 'Delete user', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" },
class: 'btn btn-remove btn-block',
method: :delete
= link_to 'Remove user and contributions', admin_user_path(user, hard_delete: true),
data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and comments authored by this user, and groups owned solely by them, will also be removed! Are you sure?" },
class: 'btn btn-remove btn-block',
method: :delete
......@@ -177,7 +177,7 @@
%p Deleting a user has the following effects:
= render 'users/deletion_guidance', user: @user
%br
= link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove"
= link_to 'Remove user', admin_user_path(@user), data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove"
- else
- if @user.solo_owned_groups.present?
%p
......@@ -188,3 +188,22 @@
- else
%p
You don't have access to delete this user.
.panel.panel-danger
.panel-heading
Remove user and contributions
.panel-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
= link_to 'Remove user and contributions', admin_user_path(@user, hard_delete: true), data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove"
- else
%p
You don't have access to delete this user.
......@@ -42,7 +42,7 @@
.col-md-9
.label-light
= label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level
= link_to "(?)", help_page_path("public_access/public_access")
= link_to icon('question-circle'), help_page_path("public_access/public_access")
%span.help-block
.col-md-3.visibility-select-container
= render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
......@@ -92,14 +92,14 @@
.form-group
= render 'shared/allow_request_access', form: f
- if Gitlab.config.lfs.enabled && current_user.admin?
.row
.row.js-lfs-enabled
.col-md-9
= f.label :lfs_enabled, 'LFS', class: 'label-light'
%span.help-block
Git Large File Storage
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
.col-md-3
= f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control', data: { field: 'lfs_enabled' }
= f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select', data: { field: 'lfs_enabled' }
- if Gitlab.config.registry.enabled
......
= form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'js-new-protected-tag' } do |f|
= form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'new-protected-tag js-new-protected-tag' } do |f|
.panel.panel-default
.panel-heading
%h3.panel-title
......
......@@ -2,7 +2,7 @@
= dropdown_tag('Select tag or create wildcard',
options: { toggle_class: 'js-protected-tag-select js-filter-submit wide git-revision-dropdown-toggle',
filter: true, dropdown_class: "dropdown-menu-selectable capitalize-header git-revision-dropdown", placeholder: "Search protected tag",
filter: true, dropdown_class: "dropdown-menu-selectable capitalize-header git-revision-dropdown", placeholder: "Search protected tags",
footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_tag_name],
......@@ -10,6 +10,6 @@
%ul.dropdown-footer-list
%li
= link_to '#', title: "New Protected Tag", class: "create-new-protected-tag" do
%button{ class: "create-new-protected-tag-button js-create-new-protected-tag", title: "New Protected Tag" }
Create wildcard
%code
......@@ -4,13 +4,14 @@
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
Protected tags
Protected Tags
%p.prepend-top-20
By default, Protected tags are designed to:
By default, protected tags are designed to:
%ul
%li Prevent tag creation by everybody except Masters
%li Prevent <strong>anyone</strong> from updating the tag
%li Prevent <strong>anyone</strong> from deleting the tag
%p.append-bottom-0 Read more about #{link_to "protected tags", help_page_path("user/project/protected_tags"), class: "underlined-link"}.
.col-lg-9
- if can? current_user, :admin_project, @project
= render 'projects/protected_tags/create_protected_tag'
......
......@@ -19,4 +19,4 @@
- if can_admin_project
%td
= link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_tag], data: { confirm: 'tag will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning'
= link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_tag], data: { confirm: 'Tag will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning'
.panel.panel-default.protected-tags-list.js-protected-tags-list
.panel.panel-default.protected-tags-list
- if @protected_tags.empty?
.panel-heading
%h3.panel-title
......@@ -13,6 +13,8 @@
%col{ width: "25%" }
%col{ width: "25%" }
%col{ width: "50%" }
- if can_admin_project
%col
%thead
%tr
%th Protected tag (#{@protected_tags.size})
......
......@@ -5,7 +5,7 @@
%h4.prepend-top-0.ref-name
= @protected_ref.name
.col-lg-9
.col-lg-9.edit_protected_tag
%h5 Matching Tags
- if @matching_refs.present?
.table-responsive
......
- noteable = @sent_notification.noteable
- noteable_type = @sent_notification.noteable_type.humanize(capitalize: false)
- noteable_type = @sent_notification.noteable_type.titleize.downcase
- noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
- page_title "Unsubscribe", noteable_text, @sent_notification.noteable_type.humanize.pluralize, @sent_notification.project.name_with_namespace
- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.name_with_namespace
%h3.page-title
Unsubscribe from #{noteable_type} #{noteable_text}
Unsubscribe from #{noteable_type}
%p
= succeed '?' do
Are you sure you want to unsubscribe from #{noteable_type}
Are you sure you want to unsubscribe from the #{noteable_type}:
= link_to noteable_text, url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable])
%p
......
......@@ -7,7 +7,7 @@
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
- cache_key = project_list_cache_key(project)
- updated_tooltip = time_ago_with_tooltip(project.updated_at)
- updated_tooltip = time_ago_with_tooltip(project.last_activity_at)
%li.project-row{ class: css_class }
= cache(cache_key) do
......
class RepositoryForkWorker
ForkError = Class.new(StandardError)
include Sidekiq::Worker
include Gitlab::ShellAdapter
include DedicatedSidekiqQueue
......@@ -8,29 +10,31 @@ class RepositoryForkWorker
source_path: source_path,
target_path: target_path)
project = Project.find_by_id(project_id)
unless project.present?
logger.error("Project #{project_id} no longer exists!")
return
end
project = Project.find(project_id)
project.import_start
result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_path,
project.repository_storage_path, target_path)
unless result
logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}")
project.mark_import_as_failed('The project could not be forked.')
return
end
raise ForkError, "Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}" unless result
project.repository.after_import
unless project.valid_repo?
logger.error("Project #{project_id} had an invalid repository after fork")
project.mark_import_as_failed('The forked repository is invalid.')
return
end
raise ForkError, "Project #{project_id} had an invalid repository after fork" unless project.valid_repo?
project.import_finish
rescue ForkError => ex
fail_fork(project, ex.message)
raise
rescue => ex
return unless project
fail_fork(project, ex.message)
raise ForkError, "#{ex.class} #{ex.message}"
end
private
def fail_fork(project, message)
Rails.logger.error(message)
project.mark_import_as_failed(message)
end
end
class RepositoryImportWorker
ImportError = Class.new(StandardError)
include Sidekiq::Worker
include DedicatedSidekiqQueue
......@@ -10,6 +12,8 @@ class RepositoryImportWorker
@project = Project.find(project_id)
@current_user = @project.creator
project.import_start
Gitlab::Metrics.add_event(:import_repository,
import_url: @project.import_url,
path: @project.path_with_namespace)
......@@ -17,13 +21,23 @@ class RepositoryImportWorker
project.update_columns(import_jid: self.jid, import_error: nil)
result = Projects::ImportService.new(project, current_user).execute
if result[:status] == :error
project.mark_import_as_failed(result[:message])
return
end
raise ImportError, result[:message] if result[:status] == :error
project.repository.after_import
project.import_finish
rescue ImportError => ex
fail_import(project, ex.message)
raise
rescue => ex
return unless project
fail_import(project, ex.message)
raise ImportError, "#{ex.class} #{ex.message}"
end
private
def fail_import(project, message)
project.mark_import_as_failed(message)
end
end
---
title: Automatically adjust project settings to match changes in project visibility
merge_request: 11831
author:
---
title: Allow users to be hard-deleted from the admin panel
merge_request: 11874
author:
---
title: Add a Rake task to aid in rotating otp_key_base
merge_request: 11881
author:
---
title: Allow group reporters to manage group labels
merge_request:
author:
---
title: Add Chinese translation of Cycle Analytics Page to I18N
merge_request: 11644
author:Huang Tao
---
title: Fix submodule link to then project under subgroup
merge_request: 11906
author:
---
title: Fixed style on unsubscribe page
merge_request:
author: Gustav Ernberg
---
title: Fix Git-over-HTTP error statuses and improve error messages
merge_request: 11398
author:
# Analyze project code quality with Code Climate CLI
This example shows how to run [Code Climate CLI][cli] on your code by using\
This example shows how to run [Code Climate CLI][cli] on your code by using
GitLab CI and Docker.
First, you need GitLab Runner with [docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor).
First, you need GitLab Runner with [docker-in-docker executor][dind].
Once you setup the Runner add new job to `.gitlab-ci.yml`:
Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `codeclimate`:
```yaml
codeclimate:
......@@ -25,4 +25,10 @@ codeclimate:
This will create a `codeclimate` job in your CI pipeline and will allow you to
download and analyze the report artifact in JSON format.
For GitLab [Enterprise Edition Starter][ee] users, this information can be automatically
extracted and shown right in the merge request widget. [Learn more on code quality
diffs in merge requests](http://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.md).
[cli]: https://github.com/codeclimate/codeclimate
[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor
[ee]: https://about.gitlab.com/gitlab-ee/
......@@ -207,7 +207,9 @@ its class in an annotation.
>**Note:**
The Ingress alone doesn't expose GitLab externally. You need to have a Ingress controller setup to do that.
Setting up an Ingress controller can be done by installing the `nginx-ingress` helm chart. But be sure
to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md)
to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md).
>**Note:**
If you would like to use the Registry, you will also need to ensure your Ingress supports a [sufficiently large request size](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size).
#### Preserving Source IPs
......
......@@ -141,7 +141,7 @@ Once you [have configured](#configuration) GitLab Runner in your `values.yml` fi
run the following:
```bash
helm install --namepace <NAMEPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE> gitlab/gitlab-runner
helm install --namespace <NAMESPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE> gitlab/gitlab-runner
```
- `<NAMESPACE>` is the Kubernetes namespace where you want to install the GitLab Runner.
......@@ -153,7 +153,7 @@ helm install --namepace <NAMEPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE>
Once your GitLab Runner Chart is installed, configuration changes and chart updates should we done using `helm upgrade`
```bash
helm upgrade --namepace <NAMEPACE> -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab-runner
helm upgrade --namespace <NAMESPACE> -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab-runner
```
Where:
......
......@@ -71,6 +71,85 @@ sudo gitlab-rake gitlab:two_factor:disable_for_all_users
bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
```
## Rotate Two-factor Authentication (2FA) encryption key
GitLab stores the secret data enabling 2FA to work in an encrypted database
column. The encryption key for this data is known as `otp_key_base`, and is
stored in `config/secrets.yml`.
If that file is leaked, but the individual 2FA secrets have not, it's possible
to re-encrypt those secrets with a new encryption key. This allows you to change
the leaked key without forcing all users to change their 2FA details.
First, look up the old key. This is in the `config/secrets.yml` file, but
**make sure you're working with the production section**. The line you're
interested in will look like this:
```yaml
production:
otp_key_base: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
```
Next, generate a new secret:
```
# omnibus-gitlab
sudo gitlab-rake secret
# installation from source
bundle exec rake secret RAILS_ENV=production
```
Now you need to stop the GitLab server, back up the existing secrets file and
update the database:
```
# omnibus-gitlab
sudo gitlab-ctl stop
sudo cp config/secrets.yml config/secrets.yml.bak
sudo gitlab-rake gitlab:two_factor:rotate_key:apply filename=backup.csv old_key=<old key> new_key=<new key>
# installation from source
sudo /etc/init.d/gitlab stop
cp config/secrets.yml config/secrets.yml.bak
bundle exec rake gitlab:two_factor:rotate_key:apply filename=backup.csv old_key=<old key> new_key=<new key> RAILS_ENV=production
```
The `<old key>` value can be read from `config/secrets.yml`; `<new key>` was
generated earlier. The **encrypted** values for the user 2FA secrets will be
written to the specified `filename` - you can use this to rollback in case of
error.
Finally, change `config/secrets.yml` to set `otp_key_base` to `<new key>` and
restart. Again, make sure you're operating in the **production** section.
```
# omnibus-gitlab
sudo gitlab-ctl start
# installation from source
sudo /etc/init.d/gitlab start
```
If there are any problems (perhaps using the wrong value for `old_key`), you can
restore your backup of `config/secrets.yml` and rollback the changes:
```
# omnibus-gitlab
sudo gitlab-ctl stop
sudo gitlab-rake gitlab:two_factor:rotate_key:rollback filename=backup.csv
sudo cp config/secrets.yml.bak config/secrets.yml
sudo gitlab-ctl start
# installation from source
sudo /etc/init.d/gitlab start
bundle exec rake gitlab:two_factor:rotate_key:rollback filename=backup.csv RAILS_ENV=production
cp config/secrets.yml.bak config/secrets.yml
sudo /etc/init.d/gitlab start
```
## Clear authentication tokens for all users. Important! Data loss!
Clear authentication tokens for all users in the GitLab database. This
......
......@@ -126,7 +126,7 @@ which visibility level you select on project settings.
## GitLab CI
GitLab CI permissions rely on the role the user has in GitLab. There are four
permission levels it total:
permission levels in total:
- admin
- master
......
......@@ -25,7 +25,8 @@ Instead of being deleted, these records will be moved to a system-wide
When a user is deleted from an abuse report or spam log, these associated
records are not ghosted and will be removed, along with any groups the user
is a sole owner of. Administrators can also request this behaviour when
deleting users from the [API](../../../api/users.md#user-deletion)
deleting users from the [API](../../../api/users.md#user-deletion) or the
admin area.
[ce-7393]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7393
[ce-10273]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10273
......
# GitLab Issues Documentation
# Issues documentation
The GitLab Issue Tracker is an advanced and complete tool
for tracking the evolution of a new idea or the process
......@@ -41,13 +41,13 @@ The image bellow illustrates how an issue looks like:
Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md).
## New Issue
## New issue
Read through the [documentation on creating issues](create_new_issue.md).
## Closing issues
Read through the distinct ways to [close issues](closing_issues.md) on GitLab.
Learn distinct ways to [close issues](closing_issues.md) in GitLab.
## Create a merge request from an issue
......@@ -84,7 +84,7 @@ Learn more about them on the [issue templates documentation](../../project/descr
Learn more about [crosslinking](crosslinking_issues.md) issues and merge requests.
### GitLab Issue Board
### Issue Board
The [GitLab Issue Board](https://about.gitlab.com/features/issueboard/) is a way to
enhance your workflow by organizing and prioritizing issues in GitLab.
......
......@@ -42,6 +42,22 @@ module API
@project, @wiki = Gitlab::RepoPath.parse(params[:project])
end
end
# Project id to pass between components that don't share/don't have
# access to the same filesystem mounts
def gl_repository
Gitlab::GlRepository.gl_repository(project, wiki?)
end
# Return the repository full path so that gitlab-shell has it when
# handling ssh commands
def repository_path
if wiki?
project.wiki.repository.path_to_repo
else
project.repository.path_to_repo
end
end
end
end
end
......@@ -32,31 +32,23 @@ module API
actor.update_last_used_at if actor.is_a?(Key)
access_checker = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
access_status = access_checker
access_checker_klass = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
access_checker = access_checker_klass
.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
.check(params[:action], params[:changes])
response = { status: access_status.status, message: access_status.message }
if access_status.status
log_user_activity(actor)
# Project id to pass between components that don't share/don't have
# access to the same filesystem mounts
response[:gl_repository] = Gitlab::GlRepository.gl_repository(project, wiki?)
# Return the repository full path so that gitlab-shell has it when
# handling ssh commands
response[:repository_path] =
if wiki?
project.wiki.repository.path_to_repo
else
project.repository.path_to_repo
end
begin
access_checker.check(params[:action], params[:changes])
rescue Gitlab::GitAccess::UnauthorizedError, Gitlab::GitAccess::NotFoundError => e
return { status: false, message: e.message }
end
response
log_user_activity(actor)
{
status: true,
gl_repository: gl_repository,
repository_path: repository_path
}
end
post "/lfs_authenticate" do
......
......@@ -293,7 +293,7 @@ module API
user = User.find_by(id: params[:id])
not_found!('User') unless user
DeleteUserWorker.perform_async(current_user.id, user.id, hard_delete: params[:hard_delete])
user.delete_async(deleted_by: current_user, params: params)
end
desc 'Block a user. Available only for admins.'
......
module Gitlab
module Checks
class ChangeAccess
ERROR_MESSAGES = {
push_code: 'You are not allowed to push code to this project.',
delete_default_branch: 'The default branch of a project cannot be deleted.',
force_push_protected_branch: 'You are not allowed to force push code to a protected branch on this project.',
non_master_delete_protected_branch: 'You are not allowed to delete protected branches from this project. Only a project master or owner can delete a protected branch.',
non_web_delete_protected_branch: 'You can only delete protected branches using the web interface.',
merge_protected_branch: 'You are not allowed to merge code into protected branches on this project.',
push_protected_branch: 'You are not allowed to push code to protected branches on this project.',
change_existing_tags: 'You are not allowed to change existing tags on this project.',
update_protected_tag: 'Protected tags cannot be updated.',
delete_protected_tag: 'Protected tags cannot be deleted.',
create_protected_tag: 'You are not allowed to create this tag as it is protected.'
}.freeze
attr_reader :user_access, :project, :skip_authorization, :protocol
def initialize(
......@@ -17,22 +31,20 @@ module Gitlab
end
def exec
return GitAccessStatus.new(true) if skip_authorization
return true if skip_authorization
error = push_checks || branch_checks || tag_checks
push_checks
branch_checks
tag_checks
if error
GitAccessStatus.new(false, error)
else
GitAccessStatus.new(true)
end
true
end
protected
def push_checks
if user_access.cannot_do_action?(:push_code)
"You are not allowed to push code to this project."
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code]
end
end
......@@ -40,7 +52,7 @@ module Gitlab
return unless @branch_name
if deletion? && @branch_name == project.default_branch
return "The default branch of a project cannot be deleted."
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch]
end
protected_branch_checks
......@@ -50,7 +62,7 @@ module Gitlab
return unless ProtectedBranch.protected?(project, @branch_name)
if forced_push?
return "You are not allowed to force push code to a protected branch on this project."
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch]
end
if deletion?
......@@ -62,22 +74,22 @@ module Gitlab
def protected_branch_deletion_checks
unless user_access.can_delete_branch?(@branch_name)
return 'You are not allowed to delete protected branches from this project. Only a project master or owner can delete a protected branch.'
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch]
end
unless protocol == 'web'
'You can only delete protected branches using the web interface.'
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch]
end
end
def protected_branch_push_checks
if matching_merge_request?
unless user_access.can_merge_to_branch?(@branch_name) || user_access.can_push_to_branch?(@branch_name)
"You are not allowed to merge code into protected branches on this project."
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch]
end
else
unless user_access.can_push_to_branch?(@branch_name)
"You are not allowed to push code to protected branches on this project."
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_protected_branch]
end
end
end
......@@ -86,7 +98,7 @@ module Gitlab
return unless @tag_name
if tag_exists? && user_access.cannot_do_action?(:admin_project)
return "You are not allowed to change existing tags on this project."
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags]
end
protected_tag_checks
......@@ -95,11 +107,11 @@ module Gitlab
def protected_tag_checks
return unless ProtectedTag.protected?(project, @tag_name)
return "Protected tags cannot be updated." if update?
return "Protected tags cannot be deleted." if deletion?
raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update?
raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion?
unless user_access.can_create_tag?(@tag_name)
return "You are not allowed to create this tag as it is protected."
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag]
end
end
......
module Gitlab
# For backwards compatibility, generic CI (which is a build without a user) is
# allowed to :build_download_code without any other checks.
class CiAccess
def can_do_action?(action)
action == :build_download_code
end
end
end
......@@ -3,33 +3,39 @@
module Gitlab
class GitAccess
UnauthorizedError = Class.new(StandardError)
NotFoundError = Class.new(StandardError)
ERROR_MESSAGES = {
upload: 'You are not allowed to upload code for this project.',
download: 'You are not allowed to download code from this project.',
deploy_key_upload:
'This deploy key does not have write access to this project.',
no_repo: 'A repository for this project does not exist yet.'
no_repo: 'A repository for this project does not exist yet.',
project_not_found: 'The project you were looking for could not be found.',
account_blocked: 'Your account has been blocked.',
command_not_allowed: "The command you're trying to execute is not allowed.",
upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.',
receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.'
}.freeze
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze
PUSH_COMMANDS = %w{ git-receive-pack }.freeze
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
attr_reader :actor, :project, :protocol, :authentication_abilities
def initialize(actor, project, protocol, authentication_abilities:)
@actor = actor
@project = project
@protocol = protocol
@authentication_abilities = authentication_abilities
@user_access = UserAccess.new(user, project: project)
end
def check(cmd, changes)
check_protocol!
check_active_user!
check_project_accessibility!
check_command_disabled!(cmd)
check_command_existence!(cmd)
check_repository_existence!
......@@ -40,9 +46,7 @@ module Gitlab
check_push_access!(changes)
end
build_status_object(true)
rescue UnauthorizedError => ex
build_status_object(false, ex.message)
true
end
def guest_can_download_code?
......@@ -73,19 +77,39 @@ module Gitlab
return if deploy_key?
if user && !user_access.allowed?
raise UnauthorizedError, "Your account has been blocked."
raise UnauthorizedError, ERROR_MESSAGES[:account_blocked]
end
end
def check_project_accessibility!
if project.blank? || !can_read_project?
raise UnauthorizedError, 'The project you were looking for could not be found.'
raise NotFoundError, ERROR_MESSAGES[:project_not_found]
end
end
def check_command_disabled!(cmd)
if upload_pack?(cmd)
check_upload_pack_disabled!
elsif receive_pack?(cmd)
check_receive_pack_disabled!
end
end
def check_upload_pack_disabled!
if http? && upload_pack_disabled_over_http?
raise UnauthorizedError, ERROR_MESSAGES[:upload_pack_disabled_over_http]
end
end
def check_receive_pack_disabled!
if http? && receive_pack_disabled_over_http?
raise UnauthorizedError, ERROR_MESSAGES[:receive_pack_disabled_over_http]
end
end
def check_command_existence!(cmd)
unless ALL_COMMANDS.include?(cmd)
raise UnauthorizedError, "The command you're trying to execute is not allowed."
raise UnauthorizedError, ERROR_MESSAGES[:command_not_allowed]
end
end
......@@ -138,11 +162,9 @@ module Gitlab
# Iterate over all changes to find if user allowed all of them to be applied
changes_list.each do |change|
status = check_single_change_access(change)
unless status.allowed?
# If user does not have access to make at least one change - cancel all push
raise UnauthorizedError, status.message
end
# If user does not have access to make at least one change, cancel all
# push by allowing the exception to bubble up
check_single_change_access(change)
end
end
......@@ -168,14 +190,40 @@ module Gitlab
actor.is_a?(DeployKey)
end
def ci?
actor == :ci
end
def can_read_project?
if deploy_key
if deploy_key?
deploy_key.has_access_to?(project)
elsif user
user.can?(:read_project, project)
elsif ci?
true # allow CI (build without a user) for backwards compatibility
end || Guest.can?(:read_project, project)
end
def http?
protocol == 'http'
end
def upload_pack?(command)
command == 'git-upload-pack'
end
def receive_pack?(command)
command == 'git-receive-pack'
end
def upload_pack_disabled_over_http?
!Gitlab.config.gitlab_shell.upload_pack
end
def receive_pack_disabled_over_http?
!Gitlab.config.gitlab_shell.receive_pack
end
protected
def user
......@@ -185,15 +233,19 @@ module Gitlab
case actor
when User
actor
when DeployKey
nil
when Key
actor.user
actor.user unless actor.is_a?(DeployKey)
when :ci
nil
end
end
def build_status_object(status, message = '')
Gitlab::GitAccessStatus.new(status, message)
def user_access
@user_access ||= if ci?
CiAccess.new
else
UserAccess.new(user, project: project)
end
end
end
end
module Gitlab
class GitAccessStatus
attr_accessor :status, :message
alias_method :allowed?, :status
def initialize(status, message = '')
@status = status
@message = message
end
def to_json(opts = nil)
{ status: @status, message: @message }.to_json(opts)
end
end
end
module Gitlab
class GitAccessWiki < GitAccess
ERROR_MESSAGES = {
write_to_wiki: "You are not allowed to write to this project's wiki."
}.freeze
def guest_can_download_code?
Guest.can?(:download_wiki_code, project)
end
......@@ -9,11 +13,11 @@ module Gitlab
end
def check_single_change_access(change)
if user_access.can_do_action?(:create_wiki)
build_status_object(true)
else
build_status_object(false, "You are not allowed to write to this project's wiki.")
unless user_access.can_do_action?(:create_wiki)
raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki]
end
true
end
end
end
......@@ -5,7 +5,10 @@ module Gitlab
AVAILABLE_LANGUAGES = {
'en' => 'English',
'es' => 'Español',
'de' => 'Deutsch'
'de' => 'Deutsch',
'zh_CN' => '简体中文',
'zh_HK' => '繁體中文(香港)',
'zh_TW' => '繁體中文(臺灣)'
}.freeze
def available_locales
......
module Gitlab
# The +otp_key_base+ param is used to encrypt the User#otp_secret attribute.
#
# When +otp_key_base+ is changed, it invalidates the current encrypted values
# of User#otp_secret. This class can be used to decrypt all the values with
# the old key, encrypt them with the new key, and and update the database
# with the new values.
#
# For persistence between runs, a CSV file is used with the following columns:
#
# user_id, old_value, new_value
#
# Only the encrypted values are stored in this file.
#
# As users may have their 2FA settings changed at any time, this is only
# guaranteed to be safe if run offline.
class OtpKeyRotator
HEADERS = %w[user_id old_value new_value].freeze
attr_reader :filename
# Create a new rotator. +filename+ is used to store values by +calculate!+,
# and to update the database with new and old values in +apply!+ and
# +rollback!+, respectively.
def initialize(filename)
@filename = filename
end
def rotate!(old_key:, new_key:)
old_key ||= Gitlab::Application.secrets.otp_key_base
raise ArgumentError.new("Old key is the same as the new key") if old_key == new_key
raise ArgumentError.new("New key is too short! Must be 256 bits") if new_key.size < 64
write_csv do |csv|
ActiveRecord::Base.transaction do
User.with_two_factor.in_batches do |relation|
rows = relation.pluck(:id, :encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt)
rows.each do |row|
user = %i[id ciphertext iv salt].zip(row).to_h
new_value = reencrypt(user, old_key, new_key)
User.where(id: user[:id]).update_all(encrypted_otp_secret: new_value)
csv << [user[:id], user[:ciphertext], new_value]
end
end
end
end
end
def rollback!
ActiveRecord::Base.transaction do
CSV.foreach(filename, headers: HEADERS, return_headers: false) do |row|
User.where(id: row['user_id']).update_all(encrypted_otp_secret: row['old_value'])
end
end
end
private
attr_reader :old_key, :new_key
def otp_secret_settings
@otp_secret_settings ||= User.encrypted_attributes[:otp_secret]
end
def reencrypt(user, old_key, new_key)
original = user[:ciphertext].unpack("m").join
opts = {
iv: user[:iv].unpack("m").join,
salt: user[:salt].unpack("m").join,
algorithm: otp_secret_settings[:algorithm],
insecure_mode: otp_secret_settings[:insecure_mode]
}
decrypted = Encryptor.decrypt(original, opts.merge(key: old_key))
encrypted = Encryptor.encrypt(decrypted, opts.merge(key: new_key))
[encrypted].pack("m")
end
def write_csv(&blk)
File.open(filename, "w") do |file|
yield CSV.new(file, headers: HEADERS, write_headers: false)
end
end
end
end
......@@ -19,5 +19,21 @@ namespace :gitlab do
puts "There are currently no users with 2FA enabled.".color(:yellow)
end
end
namespace :rotate_key do
def rotator
@rotator ||= Gitlab::OtpKeyRotator.new(ENV['filename'])
end
desc "Encrypt user OTP secrets with a new encryption key"
task apply: :environment do |t, args|
rotator.rotate!(old_key: ENV['old_key'], new_key: ENV['new_key'])
end
desc "Rollback to secrets encrypted with the old encryption key"
task rollback: :environment do
rotator.rollback!
end
end
end
end
......@@ -37,7 +37,7 @@ class GithubImport
end
def import!
@project.import_start
@project.force_import_start
timings = Benchmark.measure do
Github::Import.new(@project, @options).execute
......
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the gitlab package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-04 19:24-0500\n"
"PO-Revision-Date: 2017-05-04 19:24-0500\n"
"Last-Translator: HuangTao <htve@outlook.com>, 2017\n"
"Language-Team: Chinese (China) (https://www.transifex.com/gitlab-zh/teams/75177/zh_CN/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_CN\n"
"Plural-Forms: nplurals=1; plural=0;\n"
msgid "ByAuthor|by"
msgstr "作者:"
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "提交"
msgid ""
"Cycle Analytics gives an overview of how much time it takes to go from idea "
"to production in your project."
msgstr "周期分析概述了项目从想法到产品实现的各阶段所需的时间。"
msgid "CycleAnalyticsStage|Code"
msgstr "编码"
msgid "CycleAnalyticsStage|Issue"
msgstr "议题"
msgid "CycleAnalyticsStage|Plan"
msgstr "计划"
msgid "CycleAnalyticsStage|Production"
msgstr "生产"
msgid "CycleAnalyticsStage|Review"
msgstr "评审"
msgid "CycleAnalyticsStage|Staging"
msgstr "预发布"
msgid "CycleAnalyticsStage|Test"
msgstr "测试"
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "部署"
msgid "FirstPushedBy|First"
msgstr "首次推送"
msgid "FirstPushedBy|pushed by"
msgstr "推送者:"
msgid "From issue creation until deploy to production"
msgstr "从创建议题到部署至生产环境"
msgid "From merge request merge until deploy to production"
msgstr "从合并请求被合并后到部署至生产环境"
msgid "Introducing Cycle Analytics"
msgstr "周期分析简介"
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "最后 %d 天"
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "最多显示 %d 个事件"
msgid "Median"
msgstr "中位数"
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "新议题"
msgid "Not available"
msgstr "数据不足"
msgid "Not enough data"
msgstr "数据不足"
msgid "OpenedNDaysAgo|Opened"
msgstr "开始于"
msgid "Pipeline Health"
msgstr "流水线健康指标"
msgid "ProjectLifecycle|Stage"
msgstr "项目生命周期"
msgid "Read more"
msgstr "了解更多"
msgid "Related Commits"
msgstr "相关的提交"
msgid "Related Deployed Jobs"
msgstr "相关的部署作业"
msgid "Related Issues"
msgstr "相关的议题"
msgid "Related Jobs"
msgstr "相关的作业"
msgid "Related Merge Requests"
msgstr "相关的合并请求"
msgid "Related Merged Requests"
msgstr "相关已合并的合并请求"
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "显示 %d 个事件"
msgid ""
"The coding stage shows the time from the first commit to creating the merge "
"request. The data will automatically be added here once you create your "
"first merge request."
msgstr "编码阶段概述了从第一次提交到创建合并请求的时间。创建第一个合并请求后,数据将自动添加到此处。"
msgid "The collection of events added to the data gathered for that stage."
msgstr "与该阶段相关的事件。"
msgid ""
"The issue stage shows the time it takes from creating an issue to assigning "
"the issue to a milestone, or add the issue to a list on your Issue Board. "
"Begin creating issues to see data for this stage."
msgstr "议题阶段概述了从创建议题到将议题设置里程碑或将议题添加到议题看板的时间。开始创建议题以查看此阶段的数据。"
msgid "The phase of the development lifecycle."
msgstr "项目生命周期中的各个阶段。"
msgid ""
"The planning stage shows the time from the previous step to pushing your "
"first commit. This time will be added automatically once you push your first"
" commit."
msgstr "计划阶段概述了从议题添加到日程后到推送首次提交的时间。当首次推送提交后,数据将自动添加到此处。"
msgid ""
"The production stage shows the total time it takes between creating an issue"
" and deploying the code to production. The data will be automatically added "
"once you have completed the full idea to production cycle."
msgstr "生产阶段概述了从创建一个议题到将代码部署到生产环境的总时间。当完成想法到部署生产的循环,数据将自动添加到此处。"
msgid ""
"The review stage shows the time from creating the merge request to merging "
"it. The data will automatically be added after you merge your first merge "
"request."
msgstr "评审阶段概述了从创建合并请求到被合并的时间。当创建第一个合并请求后,数据将自动添加到此处。"
msgid ""
"The staging stage shows the time between merging the MR and deploying code "
"to the production environment. The data will be automatically added once you"
" deploy to production for the first time."
msgstr "预发布阶段概述了从合并请求被合并到部署至生产环境的总时间。首次部署到生产环境后,数据将自动添加到此处。"
msgid ""
"The testing stage shows the time GitLab CI takes to run every pipeline for "
"the related merge request. The data will automatically be added after your "
"first pipeline finishes running."
msgstr "测试阶段概述了GitLab CI为相关合并请求运行每个流水线所需的时间。当第一个流水线运行完成后,数据将自动添加到此处。"
msgid "The time taken by each data entry gathered by that stage."
msgstr "该阶段每条数据所花的时间"
msgid ""
"The value lying at the midpoint of a series of observed values. E.g., "
"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 "
"= 6."
msgstr "中位数是一个数列中最中间的值。例如在 3、5、9 之间,中位数是 5。在 3、5、7、8 之间,中位数是 (5 + 7)/ 2 = 6。"
msgid "Time before an issue gets scheduled"
msgstr "议题被列入日程表的时间"
msgid "Time before an issue starts implementation"
msgstr "开始进行编码前的时间"
msgid "Time between merge request creation and merge/close"
msgstr "从创建合并请求到被合并或关闭的时间"
msgid "Time until first merge request"
msgstr "创建第一个合并请求之前的时间"
msgid "Time|hr"
msgid_plural "Time|hrs"
msgstr[0] "小时"
msgid "Time|min"
msgid_plural "Time|mins"
msgstr[0] "分钟"
msgid "Time|s"
msgstr "秒"
msgid "Total Time"
msgstr "总时间"
msgid "Total test time for all commits/merges"
msgstr "所有提交和合并的总测试时间"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "权限不足。如需查看相关数据,请向管理员申请权限。"
msgid "We don't have enough data to show this stage."
msgstr "该阶段的数据不足,无法显示。"
msgid "You need permission."
msgstr "您需要相关的权限。"
msgid "day"
msgid_plural "days"
msgstr[0] "天"
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the gitlab package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-04 19:24-0500\n"
"PO-Revision-Date: 2017-05-04 19:24-0500\n"
"Last-Translator: HuangTao <htve@outlook.com>, 2017\n"
"Language-Team: Chinese (Hong Kong) (https://www.transifex.com/gitlab-zh/teams/75177/zh_HK/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_HK\n"
"Plural-Forms: nplurals=1; plural=0;\n"
msgid "ByAuthor|by"
msgstr "作者:"
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "提交"
msgid ""
"Cycle Analytics gives an overview of how much time it takes to go from idea "
"to production in your project."
msgstr "週期分析概述了項目從想法到產品實現的各階段所需的時間。"
msgid "CycleAnalyticsStage|Code"
msgstr "編碼"
msgid "CycleAnalyticsStage|Issue"
msgstr "議題"
msgid "CycleAnalyticsStage|Plan"
msgstr "計劃"
msgid "CycleAnalyticsStage|Production"
msgstr "生產"
msgid "CycleAnalyticsStage|Review"
msgstr "評審"
msgid "CycleAnalyticsStage|Staging"
msgstr "預發布"
msgid "CycleAnalyticsStage|Test"
msgstr "測試"
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "部署"
msgid "FirstPushedBy|First"
msgstr "首次推送"
msgid "FirstPushedBy|pushed by"
msgstr "推送者:"
msgid "From issue creation until deploy to production"
msgstr "從創建議題到部署到生產環境"
msgid "From merge request merge until deploy to production"
msgstr "從合併請求的合併到部署至生產環境"
msgid "Introducing Cycle Analytics"
msgstr "週期分析簡介"
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "最後 %d 天"
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "最多顯示 %d 個事件"
msgid "Median"
msgstr "中位數"
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "新議題"
msgid "Not available"
msgstr "不可用"
msgid "Not enough data"
msgstr "數據不足"
msgid "OpenedNDaysAgo|Opened"
msgstr "開始於"
msgid "Pipeline Health"
msgstr "流水線健康指標"
msgid "ProjectLifecycle|Stage"
msgstr "項目生命週期"
msgid "Read more"
msgstr "了解更多"
msgid "Related Commits"
msgstr "相關的提交"
msgid "Related Deployed Jobs"
msgstr "相關的部署作業"
msgid "Related Issues"
msgstr "相關的議題"
msgid "Related Jobs"
msgstr "相關的作業"
msgid "Related Merge Requests"
msgstr "相關的合併請求"
msgid "Related Merged Requests"
msgstr "相關已合併的合並請求"
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "顯示 %d 個事件"
msgid ""
"The coding stage shows the time from the first commit to creating the merge "
"request. The data will automatically be added here once you create your "
"first merge request."
msgstr "編碼階段概述了從第一次提交到創建合併請求的時間。創建第壹個合並請求後,數據將自動添加到此處。"
msgid "The collection of events added to the data gathered for that stage."
msgstr "與該階段相關的事件。"
msgid ""
"The issue stage shows the time it takes from creating an issue to assigning "
"the issue to a milestone, or add the issue to a list on your Issue Board. "
"Begin creating issues to see data for this stage."
msgstr "議題階段概述了從創建議題到將議題設置裏程碑或將議題添加到議題看板的時間。創建一個議題後,數據將自動添加到此處。"
msgid "The phase of the development lifecycle."
msgstr "項目生命週期中的各個階段。"
msgid ""
"The planning stage shows the time from the previous step to pushing your "
"first commit. This time will be added automatically once you push your first"
" commit."
msgstr "計劃階段概述了從議題添加到日程後到推送首次提交的時間。當首次推送提交後,數據將自動添加到此處。"
msgid ""
"The production stage shows the total time it takes between creating an issue"
" and deploying the code to production. The data will be automatically added "
"once you have completed the full idea to production cycle."
msgstr "生產階段概述了從創建議題到將代碼部署到生產環境的時間。當完成完整的想法到部署生產,數據將自動添加到此處。"
msgid ""
"The review stage shows the time from creating the merge request to merging "
"it. The data will automatically be added after you merge your first merge "
"request."
msgstr "評審階段概述了從創建合並請求到合併的時間。當創建第壹個合並請求後,數據將自動添加到此處。"
msgid ""
"The staging stage shows the time between merging the MR and deploying code "
"to the production environment. The data will be automatically added once you"
" deploy to production for the first time."
msgstr "預發布階段概述了合並請求的合併到部署代碼到生產環境的總時間。當首次部署到生產環境後,數據將自動添加到此處。"
msgid ""
"The testing stage shows the time GitLab CI takes to run every pipeline for "
"the related merge request. The data will automatically be added after your "
"first pipeline finishes running."
msgstr "測試階段概述了GitLab CI為相關合併請求運行每個流水線所需的時間。當第壹個流水線運行完成後,數據將自動添加到此處。"
msgid "The time taken by each data entry gathered by that stage."
msgstr "該階段每條數據所花的時間"
msgid ""
"The value lying at the midpoint of a series of observed values. E.g., "
"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 "
"= 6."
msgstr "中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"
msgid "Time before an issue gets scheduled"
msgstr "議題被列入日程表的時間"
msgid "Time before an issue starts implementation"
msgstr "開始進行編碼前的時間"
msgid "Time between merge request creation and merge/close"
msgstr "從創建合併請求到被合並或關閉的時間"
msgid "Time until first merge request"
msgstr "創建第壹個合併請求之前的時間"
msgid "Time|hr"
msgid_plural "Time|hrs"
msgstr[0] "小時"
msgid "Time|min"
msgid_plural "Time|mins"
msgstr[0] "分鐘"
msgid "Time|s"
msgstr "秒"
msgid "Total Time"
msgstr "總時間"
msgid "Total test time for all commits/merges"
msgstr "所有提交和合併的總測試時間"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "權限不足。如需查看相關數據,請向管理員申請權限。"
msgid "We don't have enough data to show this stage."
msgstr "該階段的數據不足,無法顯示。"
msgid "You need permission."
msgstr "您需要相關的權限。"
msgid "day"
msgid_plural "days"
msgstr[0] "天"
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the gitlab package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-04 19:24-0500\n"
"PO-Revision-Date: 2017-05-04 19:24-0500\n"
"Last-Translator: HuangTao <htve@outlook.com>, 2017\n"
"Language-Team: Chinese (Taiwan) (https://www.transifex.com/gitlab-zh/teams/75177/zh_TW/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_TW\n"
"Plural-Forms: nplurals=1; plural=0;\n"
msgid "ByAuthor|by"
msgstr "作者:"
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "送交"
msgid ""
"Cycle Analytics gives an overview of how much time it takes to go from idea "
"to production in your project."
msgstr "週期分析概述了你的專案從想法到產品實現,各階段所需的時間。"
msgid "CycleAnalyticsStage|Code"
msgstr "程式開發"
msgid "CycleAnalyticsStage|Issue"
msgstr "議題"
msgid "CycleAnalyticsStage|Plan"
msgstr "計劃"
msgid "CycleAnalyticsStage|Production"
msgstr "上線"
msgid "CycleAnalyticsStage|Review"
msgstr "複閱"
msgid "CycleAnalyticsStage|Staging"
msgstr "預備"
msgid "CycleAnalyticsStage|Test"
msgstr "測試"
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "部署"
msgid "FirstPushedBy|First"
msgstr "首次推送"
msgid "FirstPushedBy|pushed by"
msgstr "推送者:"
msgid "From issue creation until deploy to production"
msgstr "從議題建立至線上部署"
msgid "From merge request merge until deploy to production"
msgstr "從請求被合併後至線上部署"
msgid "Introducing Cycle Analytics"
msgstr "週期分析簡介"
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "最後 %d 天"
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "最多顯示 %d 個事件"
msgid "Median"
msgstr "中位數"
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "新議題"
msgid "Not available"
msgstr "無法使用"
msgid "Not enough data"
msgstr "資料不足"
msgid "OpenedNDaysAgo|Opened"
msgstr "開始於"
msgid "Pipeline Health"
msgstr "流水線健康指標"
msgid "ProjectLifecycle|Stage"
msgstr "專案生命週期"
msgid "Read more"
msgstr "了解更多"
msgid "Related Commits"
msgstr "相關的送交"
msgid "Related Deployed Jobs"
msgstr "相關的部署作業"
msgid "Related Issues"
msgstr "相關的議題"
msgid "Related Jobs"
msgstr "相關的作業"
msgid "Related Merge Requests"
msgstr "相關的合併請求"
msgid "Related Merged Requests"
msgstr "相關已合併的請求"
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "顯示 %d 個事件"
msgid ""
"The coding stage shows the time from the first commit to creating the merge "
"request. The data will automatically be added here once you create your "
"first merge request."
msgstr "程式開發階段顯示從第一次送交到建立合併請求的時間。建立第一個合併請求後,資料將自動填入。"
msgid "The collection of events added to the data gathered for that stage."
msgstr "與該階段相關的事件。"
msgid ""
"The issue stage shows the time it takes from creating an issue to assigning "
"the issue to a milestone, or add the issue to a list on your Issue Board. "
"Begin creating issues to see data for this stage."
msgstr "議題階段顯示從議題建立到設置里程碑、或將該議題加至議題看板的時間。建立第一個議題後,資料將自動填入。"
msgid "The phase of the development lifecycle."
msgstr "專案開發生命週期的各個階段。"
msgid ""
"The planning stage shows the time from the previous step to pushing your "
"first commit. This time will be added automatically once you push your first"
" commit."
msgstr "計劃階段顯示從議題添加到日程後至推送第一個送交的時間。當第一次推送送交後,資料將自動填入。"
msgid ""
"The production stage shows the total time it takes between creating an issue"
" and deploying the code to production. The data will be automatically added "
"once you have completed the full idea to production cycle."
msgstr "上線階段顯示從建立一個議題到部署程式至線上的總時間。當完成從想法到產品實現的循環後,資料將自動填入。"
msgid ""
"The review stage shows the time from creating the merge request to merging "
"it. The data will automatically be added after you merge your first merge "
"request."
msgstr "複閱階段顯示從合併請求建立後至被合併的時間。當建立第一個合併請求後,資料將自動填入。"
msgid ""
"The staging stage shows the time between merging the MR and deploying code "
"to the production environment. The data will be automatically added once you"
" deploy to production for the first time."
msgstr "預備階段顯示從合併請求被合併後至部署上線的時間。當第一次部署上線後,資料將自動填入。"
msgid ""
"The testing stage shows the time GitLab CI takes to run every pipeline for "
"the related merge request. The data will automatically be added after your "
"first pipeline finishes running."
msgstr "測試階段顯示相關合併請求的流水線所花的時間。當第一個流水線運作完畢後,資料將自動填入。"
msgid "The time taken by each data entry gathered by that stage."
msgstr "每筆該階段相關資料所花的時間。"
msgid ""
"The value lying at the midpoint of a series of observed values. E.g., "
"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 "
"= 6."
msgstr "中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"
msgid "Time before an issue gets scheduled"
msgstr "議題被列入日程表的時間"
msgid "Time before an issue starts implementation"
msgstr "議題等待開始實作的時間"
msgid "Time between merge request creation and merge/close"
msgstr "合併請求被合併或是關閉的時間"
msgid "Time until first merge request"
msgstr "第一個合併請求被建立前的時間"
msgid "Time|hr"
msgid_plural "Time|hrs"
msgstr[0] "小時"
msgid "Time|min"
msgid_plural "Time|mins"
msgstr[0] "分鐘"
msgid "Time|s"
msgstr "秒"
msgid "Total Time"
msgstr "總時間"
msgid "Total test time for all commits/merges"
msgstr "所有送交和合併的總測試時間"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "權限不足。如需查看相關資料,請向管理員申請權限。"
msgid "We don't have enough data to show this stage."
msgstr "因該階段的資料不足而無法顯示相關資訊"
msgid "You need permission."
msgstr "您需要相關的權限。"
msgid "day"
msgid_plural "days"
msgstr[0] "天"
......@@ -10,15 +10,26 @@ describe Admin::UsersController do
describe 'DELETE #user with projects' do
let(:project) { create(:empty_project, namespace: user.namespace) }
let!(:issue) { create(:issue, author: user) }
before do
project.team << [user, :developer]
end
it 'deletes user' do
it 'deletes user and ghosts their contributions' do
delete :destroy, id: user.username, format: :json
expect(response).to have_http_status(200)
expect(User.exists?(user.id)).to be_falsy
expect(issue.reload.author).to be_ghost
end
it 'deletes the user and their contributions when hard delete is specified' do
delete :destroy, id: user.username, hard_delete: true, format: :json
expect(response).to have_http_status(200)
expect { User.find(user.id) }.to raise_exception(ActiveRecord::RecordNotFound)
expect(User.exists?(user.id)).to be_falsy
expect(Issue.exists?(issue.id)).to be_falsy
end
end
......
......@@ -77,7 +77,7 @@ describe RegistrationsController do
end
it 'schedules the user for destruction' do
expect(DeleteUserWorker).to receive(:perform_async).with(user.id, user.id)
expect(DeleteUserWorker).to receive(:perform_async).with(user.id, user.id, {})
post(:destroy)
......
......@@ -7,5 +7,9 @@ FactoryGirl.define do
link.forked_from_project.reload
link.forked_to_project.reload
end
trait :forked_to_empty_project do
association :forked_to_project, factory: :empty_project
end
end
end
......@@ -26,6 +26,22 @@ FactoryGirl.define do
visibility_level Gitlab::VisibilityLevel::PRIVATE
end
trait :import_scheduled do
import_status :scheduled
end
trait :import_started do
import_status :started
end
trait :import_finished do
import_status :finished
end
trait :import_failed do
import_status :failed
end
trait :archived do
archived true
end
......
......@@ -22,7 +22,8 @@ describe "Admin::Users", feature: true do
expect(page).to have_content(user.email)
expect(page).to have_content(user.name)
expect(page).to have_link('Block', href: block_admin_user_path(user))
expect(page).to have_link('Delete', href: admin_user_path(user))
expect(page).to have_link('Remove user', href: admin_user_path(user))
expect(page).to have_link('Remove user and contributions', href: admin_user_path(user, hard_delete: true))
end
describe 'Two-factor Authentication filters' do
......@@ -116,6 +117,9 @@ describe "Admin::Users", feature: true do
expect(page).to have_content(user.email)
expect(page).to have_content(user.name)
expect(page).to have_link('Block user', href: block_admin_user_path(user))
expect(page).to have_link('Remove user', href: admin_user_path(user))
expect(page).to have_link('Remove user and contributions', href: admin_user_path(user, hard_delete: true))
end
describe 'Impersonation' do
......
......@@ -15,6 +15,15 @@ RSpec.describe 'Dashboard Projects', feature: true do
expect(page).to have_content('awesome stuff')
end
it 'shows the last_activity_at attribute as the update date' do
now = Time.now
project.update_column(:last_activity_at, now)
visit dashboard_projects_path
expect(page).to have_xpath("//time[@datetime='#{now.getutc.iso8601}']")
end
context 'when on Starred projects tab' do
it 'shows only starred projects' do
user.toggle_star(project2)
......
......@@ -68,9 +68,14 @@ feature 'Diffs URL', js: true, feature: true do
let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, target_project: project, author: author_user) }
let(:changelog_id) { Digest::SHA1.hexdigest("CHANGELOG") }
before do
forked_project.repository.after_import
end
context 'as author' do
it 'shows direct edit link' do
login_as(author_user)
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
# Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
......@@ -81,6 +86,7 @@ feature 'Diffs URL', js: true, feature: true do
context 'as user who needs to fork' do
it 'shows fork/cancel confirmation' do
login_as(user)
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
# Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
......
......@@ -14,7 +14,7 @@ feature 'Visibility settings', feature: true, js: true do
visibility_select_container = find('.js-visibility-select')
expect(visibility_select_container.find('.visibility-select').value).to eq project.visibility_level.to_s
expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.'
expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.'
end
scenario 'project visibility description updates on change' do
......@@ -41,7 +41,7 @@ feature 'Visibility settings', feature: true, js: true do
expect(visibility_select_container).not_to have_select '.visibility-select'
expect(visibility_select_container).to have_content 'Public'
expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.'
expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.'
end
end
end
......@@ -24,8 +24,8 @@ describe 'Unsubscribe links', feature: true do
visit body_link
expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last)
expect(page).to have_text(%(Unsubscribe from issue #{issue.title} (#{issue.to_reference})))
expect(page).to have_text(%(Are you sure you want to unsubscribe from issue #{issue.title} (#{issue.to_reference})?))
expect(page).to have_text(%(Unsubscribe from issue))
expect(page).to have_text(%(Are you sure you want to unsubscribe from the issue: #{issue.title} (#{issue.to_reference})?))
expect(issue.subscribed?(recipient, project)).to be_truthy
click_link 'Unsubscribe'
......
......@@ -257,7 +257,7 @@ describe ProjectsHelper do
result = helper.project_feature_access_select(:issues_access_level)
expect(result).to include("Disabled")
expect(result).to include("Only team members")
expect(result).not_to include("Everyone with access")
expect(result).to have_selector('option[disabled]', text: "Everyone with access")
end
end
......@@ -272,7 +272,7 @@ describe ProjectsHelper do
expect(result).to include("Disabled")
expect(result).to include("Only team members")
expect(result).not_to include("Everyone with access")
expect(result).to have_selector('option[disabled]', text: "Everyone with access")
expect(result).to have_selector('option[selected]', text: "Only team members")
end
end
......
......@@ -52,6 +52,14 @@ describe SubmoduleHelper do
stub_url(['http://', config.host, '/gitlab/root/gitlab-org/gitlab-ce.git'].join(''))
expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
end
it 'works with subgroups' do
allow(Gitlab.config.gitlab).to receive(:port).and_return(80) # set this just to be sure
allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root')
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
stub_url(['http://', config.host, '/gitlab/root/gitlab-org/sub/gitlab-ce.git'].join(''))
expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org/sub', 'gitlab-ce'), namespace_project_tree_path('gitlab-org/sub', 'gitlab-ce', 'hash')])
end
end
context 'submodule on github.com' do
......
......@@ -37,7 +37,7 @@ describe VisibilityLevelHelper do
it "describes public projects" do
expect(project_visibility_level_description(Gitlab::VisibilityLevel::PUBLIC))
.to eq "The project can be cloned without any authentication."
.to eq "The project can be accessed without any authentication."
end
end
......
......@@ -10,6 +10,9 @@ describe('Pipeline details header', () => {
beforeEach(() => {
HeaderComponent = Vue.extend(headerComponent);
const threeWeeksAgo = new Date();
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
props = {
pipeline: {
details: {
......@@ -22,7 +25,7 @@ describe('Pipeline details header', () => {
},
},
id: 123,
created_at: '2017-05-08T14:57:39.781Z',
created_at: threeWeeksAgo.toISOString(),
user: {
web_url: 'path',
name: 'Foo',
......
......@@ -23,29 +23,27 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
before { project.add_developer(user) }
context 'without failed checks' do
it "doesn't return any error" do
expect(subject.status).to be(true)
it "doesn't raise an error" do
expect { subject }.not_to raise_error
end
end
context 'when the user is not allowed to push code' do
it 'returns an error' do
it 'raises an error' do
expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
expect(subject.status).to be(false)
expect(subject.message).to eq('You are not allowed to push code to this project.')
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.')
end
end
context 'tags check' do
let(:ref) { 'refs/tags/v1.0.0' }
it 'returns an error if the user is not allowed to update tags' do
it 'raises an error if the user is not allowed to update tags' do
allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true)
expect(user_access).to receive(:can_do_action?).with(:admin_project).and_return(false)
expect(subject.status).to be(false)
expect(subject.message).to eq('You are not allowed to change existing tags on this project.')
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to change existing tags on this project.')
end
context 'with protected tag' do
......@@ -59,8 +57,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
let(:newrev) { '0000000000000000000000000000000000000000' }
it 'is prevented' do
expect(subject.status).to be(false)
expect(subject.message).to include('cannot be deleted')
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be deleted/)
end
end
......@@ -69,8 +66,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
it 'is prevented' do
expect(subject.status).to be(false)
expect(subject.message).to include('cannot be updated')
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be updated/)
end
end
end
......@@ -81,15 +77,14 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
let(:ref) { 'refs/tags/v9.1.0' }
it 'prevents creation below access level' do
expect(subject.status).to be(false)
expect(subject.message).to include('allowed to create this tag as it is protected')
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /allowed to create this tag as it is protected/)
end
context 'when user has access' do
let!(:protected_tag) { create(:protected_tag, :developers_can_create, project: project, name: 'v*') }
it 'allows tag creation' do
expect(subject.status).to be(true)
expect { subject }.not_to raise_error
end
end
end
......@@ -101,9 +96,8 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
let(:newrev) { '0000000000000000000000000000000000000000' }
let(:ref) { 'refs/heads/master' }
it 'returns an error' do
expect(subject.status).to be(false)
expect(subject.message).to eq('The default branch of a project cannot be deleted.')
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'The default branch of a project cannot be deleted.')
end
end
......@@ -113,27 +107,24 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
allow(ProtectedBranch).to receive(:protected?).with(project, 'feature').and_return(true)
end
it 'returns an error if the user is not allowed to do forced pushes to protected branches' do
it 'raises an error if the user is not allowed to do forced pushes to protected branches' do
expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true)
expect(subject.status).to be(false)
expect(subject.message).to eq('You are not allowed to force push code to a protected branch on this project.')
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to force push code to a protected branch on this project.')
end
it 'returns an error if the user is not allowed to merge to protected branches' do
it 'raises an error if the user is not allowed to merge to protected branches' do
expect_any_instance_of(Gitlab::Checks::MatchingMergeRequest).to receive(:match?).and_return(true)
expect(user_access).to receive(:can_merge_to_branch?).and_return(false)
expect(user_access).to receive(:can_push_to_branch?).and_return(false)
expect(subject.status).to be(false)
expect(subject.message).to eq('You are not allowed to merge code into protected branches on this project.')
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to merge code into protected branches on this project.')
end
it 'returns an error if the user is not allowed to push to protected branches' do
it 'raises an error if the user is not allowed to push to protected branches' do
expect(user_access).to receive(:can_push_to_branch?).and_return(false)
expect(subject.status).to be(false)
expect(subject.message).to eq('You are not allowed to push code to protected branches on this project.')
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to protected branches on this project.')
end
context 'branch deletion' do
......@@ -141,9 +132,8 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
let(:ref) { 'refs/heads/feature' }
context 'if the user is not allowed to delete protected branches' do
it 'returns an error' do
expect(subject.status).to be(false)
expect(subject.message).to eq('You are not allowed to delete protected branches from this project. Only a project master or owner can delete a protected branch.')
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to delete protected branches from this project. Only a project master or owner can delete a protected branch.')
end
end
......@@ -156,14 +146,13 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
let(:protocol) { 'web' }
it 'allows branch deletion' do
expect(subject.status).to be(true)
expect { subject }.not_to raise_error
end
end
context 'over SSH or HTTP' do
it 'returns an error' do
expect(subject.status).to be(false)
expect(subject.message).to eq('You can only delete protected branches using the web interface.')
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only delete protected branches using the web interface.')
end
end
end
......
require 'spec_helper'
describe Gitlab::CiAccess, lib: true do
let(:access) { Gitlab::CiAccess.new }
describe '#can_do_action?' do
context 'when action is :build_download_code' do
it { expect(access.can_do_action?(:build_download_code)).to be_truthy }
end
context 'when action is not :build_download_code' do
it { expect(access.can_do_action?(:download_code)).to be_falsey }
end
end
end
require 'spec_helper'
describe Gitlab::GitAccess, lib: true do
let(:access) { Gitlab::GitAccess.new(actor, project, 'ssh', authentication_abilities: authentication_abilities) }
let(:pull_access_check) { access.check('git-upload-pack', '_any') }
let(:push_access_check) { access.check('git-receive-pack', '_any') }
let(:access) { Gitlab::GitAccess.new(actor, project, protocol, authentication_abilities: authentication_abilities) }
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:actor) { user }
let(:protocol) { 'ssh' }
let(:authentication_abilities) do
[
:read_project,
......@@ -15,49 +18,188 @@ describe Gitlab::GitAccess, lib: true do
describe '#check with single protocols allowed' do
def disable_protocol(protocol)
settings = ::ApplicationSetting.create_from_defaults
settings.update_attribute(:enabled_git_access_protocol, protocol)
allow(Gitlab::ProtocolAccess).to receive(:allowed?).with(protocol).and_return(false)
end
context 'ssh disabled' do
before do
disable_protocol('ssh')
@acc = Gitlab::GitAccess.new(actor, project, 'ssh', authentication_abilities: authentication_abilities)
end
it 'blocks ssh git push' do
expect(@acc.check('git-receive-pack', '_any').allowed?).to be_falsey
expect { push_access_check }.to raise_unauthorized('Git access over SSH is not allowed')
end
it 'blocks ssh git pull' do
expect(@acc.check('git-upload-pack', '_any').allowed?).to be_falsey
expect { pull_access_check }.to raise_unauthorized('Git access over SSH is not allowed')
end
end
context 'http disabled' do
let(:protocol) { 'http' }
before do
disable_protocol('http')
@acc = Gitlab::GitAccess.new(actor, project, 'http', authentication_abilities: authentication_abilities)
end
it 'blocks http push' do
expect(@acc.check('git-receive-pack', '_any').allowed?).to be_falsey
expect { push_access_check }.to raise_unauthorized('Git access over HTTP is not allowed')
end
it 'blocks http git pull' do
expect(@acc.check('git-upload-pack', '_any').allowed?).to be_falsey
expect { pull_access_check }.to raise_unauthorized('Git access over HTTP is not allowed')
end
end
end
describe '#check_download_access!' do
subject { access.check('git-upload-pack', '_any') }
describe '#check_project_accessibility!' do
context 'when the project exists' do
context 'when actor exists' do
context 'when actor is a DeployKey' do
let(:deploy_key) { create(:deploy_key, user: user, can_push: true) }
let(:actor) { deploy_key }
context 'when the DeployKey has access to the project' do
before { deploy_key.projects << project }
it 'allows pull access' do
expect { pull_access_check }.not_to raise_error
end
it 'allows push access' do
expect { push_access_check }.not_to raise_error
end
end
context 'when the Deploykey does not have access to the project' do
it 'blocks pulls with "not found"' do
expect { pull_access_check }.to raise_not_found('The project you were looking for could not be found.')
end
it 'blocks pushes with "not found"' do
expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.')
end
end
end
context 'when actor is a User' do
context 'when the User can read the project' do
before { project.team << [user, :master] }
it 'allows pull access' do
expect { pull_access_check }.not_to raise_error
end
it 'allows push access' do
expect { push_access_check }.not_to raise_error
end
end
context 'when the User cannot read the project' do
it 'blocks pulls with "not found"' do
expect { pull_access_check }.to raise_not_found('The project you were looking for could not be found.')
end
it 'blocks pushes with "not found"' do
expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.')
end
end
end
# For backwards compatibility
context 'when actor is :ci' do
let(:actor) { :ci }
let(:authentication_abilities) { build_authentication_abilities }
it 'allows pull access' do
expect { pull_access_check }.not_to raise_error
end
it 'does not block pushes with "not found"' do
expect { push_access_check }.to raise_unauthorized('You are not allowed to upload code for this project.')
end
end
end
context 'when actor is nil' do
let(:actor) { nil }
context 'when guests can read the project' do
let(:project) { create(:project, :repository, :public) }
it 'allows pull access' do
expect { pull_access_check }.not_to raise_error
end
it 'does not block pushes with "not found"' do
expect { push_access_check }.to raise_unauthorized('You are not allowed to upload code for this project.')
end
end
context 'when guests cannot read the project' do
it 'blocks pulls with "not found"' do
expect { pull_access_check }.to raise_not_found('The project you were looking for could not be found.')
end
it 'blocks pushes with "not found"' do
expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.')
end
end
end
end
context 'when the project is nil' do
let(:project) { nil }
it 'blocks any command with "not found"' do
expect { pull_access_check }.to raise_not_found('The project you were looking for could not be found.')
expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.')
end
end
end
describe '#check_command_disabled!' do
before { project.team << [user, :master] }
context 'over http' do
let(:protocol) { 'http' }
context 'when the git-upload-pack command is disabled in config' do
before do
allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
end
context 'when calling git-upload-pack' do
it { expect { pull_access_check }.to raise_unauthorized('Pulling over HTTP is not allowed.') }
end
context 'when calling git-receive-pack' do
it { expect { push_access_check }.not_to raise_error }
end
end
context 'when the git-receive-pack command is disabled in config' do
before do
allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
end
context 'when calling git-receive-pack' do
it { expect { push_access_check }.to raise_unauthorized('Pushing over HTTP is not allowed.') }
end
context 'when calling git-upload-pack' do
it { expect { pull_access_check }.not_to raise_error }
end
end
end
end
describe '#check_download_access!' do
describe 'master permissions' do
before { project.team << [user, :master] }
context 'pull code' do
it { expect(subject.allowed?).to be_truthy }
it { expect { pull_access_check }.not_to raise_error }
end
end
......@@ -65,8 +207,7 @@ describe Gitlab::GitAccess, lib: true do
before { project.team << [user, :guest] }
context 'pull code' do
it { expect(subject.allowed?).to be_falsey }
it { expect(subject.message).to match(/You are not allowed to download code/) }
it { expect { pull_access_check }.to raise_unauthorized('You are not allowed to download code from this project.') }
end
end
......@@ -77,24 +218,22 @@ describe Gitlab::GitAccess, lib: true do
end
context 'pull code' do
it { expect(subject.allowed?).to be_falsey }
it { expect(subject.message).to match(/Your account has been blocked/) }
it { expect { pull_access_check }.to raise_unauthorized('Your account has been blocked.') }
end
end
describe 'without access to project' do
context 'pull code' do
it { expect(subject.allowed?).to be_falsey }
it { expect { pull_access_check }.to raise_not_found('The project you were looking for could not be found.') }
end
context 'when project is public' do
let(:public_project) { create(:project, :public, :repository) }
let(:guest_access) { Gitlab::GitAccess.new(nil, public_project, 'web', authentication_abilities: []) }
subject { guest_access.check('git-upload-pack', '_any') }
let(:access) { Gitlab::GitAccess.new(nil, public_project, 'web', authentication_abilities: []) }
context 'when repository is enabled' do
it 'give access to download code' do
expect(subject.allowed?).to be_truthy
expect { pull_access_check }.not_to raise_error
end
end
......@@ -102,8 +241,7 @@ describe Gitlab::GitAccess, lib: true do
it 'does not give access to download code' do
public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
expect(subject.allowed?).to be_falsey
expect(subject.message).to match(/You are not allowed to download code/)
expect { pull_access_check }.to raise_unauthorized('You are not allowed to download code from this project.')
end
end
end
......@@ -117,26 +255,26 @@ describe Gitlab::GitAccess, lib: true do
context 'when project is authorized' do
before { key.projects << project }
it { expect(subject).to be_allowed }
it { expect { pull_access_check }.not_to raise_error }
end
context 'when unauthorized' do
context 'from public project' do
let(:project) { create(:project, :public, :repository) }
it { expect(subject).to be_allowed }
it { expect { pull_access_check }.not_to raise_error }
end
context 'from internal project' do
let(:project) { create(:project, :internal, :repository) }
it { expect(subject).not_to be_allowed }
it { expect { pull_access_check }.to raise_not_found('The project you were looking for could not be found.') }
end
context 'from private project' do
let(:project) { create(:project, :private, :repository) }
it { expect(subject).not_to be_allowed }
it { expect { pull_access_check }.to raise_not_found('The project you were looking for could not be found.') }
end
end
end
......@@ -149,7 +287,7 @@ describe Gitlab::GitAccess, lib: true do
let(:project) { create(:project, :repository, namespace: user.namespace) }
context 'pull code' do
it { expect(subject).to be_allowed }
it { expect { pull_access_check }.not_to raise_error }
end
end
......@@ -157,7 +295,7 @@ describe Gitlab::GitAccess, lib: true do
before { project.team << [user, :reporter] }
context 'pull code' do
it { expect(subject).to be_allowed }
it { expect { pull_access_check }.not_to raise_error }
end
end
......@@ -168,16 +306,24 @@ describe Gitlab::GitAccess, lib: true do
before { project.team << [user, :reporter] }
context 'pull code' do
it { expect(subject).to be_allowed }
it { expect { pull_access_check }.not_to raise_error }
end
end
context 'when is not member of the project' do
context 'pull code' do
it { expect(subject).not_to be_allowed }
it { expect { pull_access_check }.to raise_unauthorized('You are not allowed to download code from this project.') }
end
end
end
describe 'generic CI (build without a user)' do
let(:actor) { :ci }
context 'pull code' do
it { expect { pull_access_check }.not_to raise_error }
end
end
end
end
......@@ -365,42 +511,32 @@ describe Gitlab::GitAccess, lib: true do
end
end
shared_examples 'pushing code' do |can|
subject { access.check('git-receive-pack', '_any') }
describe 'build authentication abilities' do
let(:authentication_abilities) { build_authentication_abilities }
context 'when project is authorized' do
before { authorize }
before { project.team << [user, :reporter] }
it { expect(subject).public_send(can, be_allowed) }
it { expect { push_access_check }.to raise_unauthorized('You are not allowed to upload code for this project.') }
end
context 'when unauthorized' do
context 'to public project' do
let(:project) { create(:project, :public, :repository) }
it { expect(subject).not_to be_allowed }
it { expect { push_access_check }.to raise_unauthorized('You are not allowed to upload code for this project.') }
end
context 'to internal project' do
let(:project) { create(:project, :internal, :repository) }
it { expect(subject).not_to be_allowed }
it { expect { push_access_check }.to raise_unauthorized('You are not allowed to upload code for this project.') }
end
context 'to private project' do
let(:project) { create(:project, :private, :repository) }
it { expect(subject).not_to be_allowed }
end
end
end
describe 'build authentication abilities' do
let(:authentication_abilities) { build_authentication_abilities }
it_behaves_like 'pushing code', :not_to do
def authorize
project.team << [user, :reporter]
it { expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.') }
end
end
end
......@@ -412,9 +548,29 @@ describe Gitlab::GitAccess, lib: true do
context 'when deploy_key can push' do
let(:can_push) { true }
it_behaves_like 'pushing code', :to do
def authorize
key.projects << project
context 'when project is authorized' do
before { key.projects << project }
it { expect { push_access_check }.not_to raise_error }
end
context 'when unauthorized' do
context 'to public project' do
let(:project) { create(:project, :public, :repository) }
it { expect { push_access_check }.to raise_unauthorized('This deploy key does not have write access to this project.') }
end
context 'to internal project' do
let(:project) { create(:project, :internal, :repository) }
it { expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.') }
end
context 'to private project' do
let(:project) { create(:project, :private, :repository) }
it { expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.') }
end
end
end
......@@ -422,9 +578,29 @@ describe Gitlab::GitAccess, lib: true do
context 'when deploy_key cannot push' do
let(:can_push) { false }
it_behaves_like 'pushing code', :not_to do
def authorize
key.projects << project
context 'when project is authorized' do
before { key.projects << project }
it { expect { push_access_check }.to raise_unauthorized('This deploy key does not have write access to this project.') }
end
context 'when unauthorized' do
context 'to public project' do
let(:project) { create(:project, :public, :repository) }
it { expect { push_access_check }.to raise_unauthorized('This deploy key does not have write access to this project.') }
end
context 'to internal project' do
let(:project) { create(:project, :internal, :repository) }
it { expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.') }
end
context 'to private project' do
let(:project) { create(:project, :private, :repository) }
it { expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.') }
end
end
end
......@@ -432,6 +608,14 @@ describe Gitlab::GitAccess, lib: true do
private
def raise_unauthorized(message)
raise_error(Gitlab::GitAccess::UnauthorizedError, message)
end
def raise_not_found(message)
raise_error(Gitlab::GitAccess::NotFoundError, message)
end
def build_authentication_abilities
[
:read_project,
......
......@@ -20,7 +20,7 @@ describe Gitlab::GitAccessWiki, lib: true do
subject { access.check('git-receive-pack', changes) }
it { expect(subject.allowed?).to be_truthy }
it { expect { subject }.not_to raise_error }
end
def changes
......@@ -36,7 +36,7 @@ describe Gitlab::GitAccessWiki, lib: true do
context 'when wiki feature is enabled' do
it 'give access to download wiki code' do
expect(subject.allowed?).to be_truthy
expect { subject }.not_to raise_error
end
end
......@@ -44,8 +44,7 @@ describe Gitlab::GitAccessWiki, lib: true do
it 'does not give access to download wiki code' do
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
expect(subject.allowed?).to be_falsey
expect(subject.message).to match(/You are not allowed to download code/)
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to download code from this project.')
end
end
end
......
......@@ -144,7 +144,9 @@ merge_access_levels:
push_access_levels:
- protected_branch
create_access_levels:
- user
- protected_tag
- group
container_repositories:
- project
- name
......
require 'spec_helper'
describe Gitlab::OtpKeyRotator do
let(:file) { Tempfile.new("otp-key-rotator-test") }
let(:filename) { file.path }
let(:old_key) { Gitlab::Application.secrets.otp_key_base }
let(:new_key) { "00" * 32 }
let!(:users) { create_list(:user, 5, :two_factor) }
after do
file.close
file.unlink
end
def data
CSV.read(filename)
end
def build_row(user, applied = false)
[user.id.to_s, encrypt_otp(user, old_key), encrypt_otp(user, new_key)]
end
def encrypt_otp(user, key)
opts = {
value: user.otp_secret,
iv: user.encrypted_otp_secret_iv.unpack("m").join,
salt: user.encrypted_otp_secret_salt.unpack("m").join,
algorithm: 'aes-256-cbc',
insecure_mode: true,
key: key
}
[Encryptor.encrypt(opts)].pack("m")
end
subject(:rotator) { described_class.new(filename) }
describe '#rotate!' do
subject(:rotation) { rotator.rotate!(old_key: old_key, new_key: new_key) }
it 'stores the calculated values in a spreadsheet' do
rotation
expect(data).to match_array(users.map {|u| build_row(u) })
end
context 'new key is too short' do
let(:new_key) { "00" * 31 }
it { expect { rotation }.to raise_error(ArgumentError) }
end
context 'new key is the same as the old key' do
let(:new_key) { old_key }
it { expect { rotation }.to raise_error(ArgumentError) }
end
end
describe '#rollback!' do
it 'updates rows to the old value' do
file.puts("#{users[0].id},old,new")
file.close
rotator.rollback!
expect(users[0].reload.encrypted_otp_secret).to eq('old')
expect(users[1].reload.encrypted_otp_secret).not_to eq('old')
end
end
end
......@@ -28,9 +28,7 @@ RSpec.describe AbuseReport, type: :model do
end
it 'lets a worker delete the user' do
expect(DeleteUserWorker).to receive(:perform_async).with(user.id, subject.user.id,
delete_solo_owned_groups: true,
hard_delete: true)
expect(DeleteUserWorker).to receive(:perform_async).with(user.id, subject.user.id, hard_delete: true)
subject.remove_user(deleted_by: user)
end
......
......@@ -50,7 +50,7 @@ describe Project, models: true do
it { is_expected.to have_one(:external_wiki_service).dependent(:destroy) }
it { is_expected.to have_one(:project_feature).dependent(:destroy) }
it { is_expected.to have_one(:statistics).class_name('ProjectStatistics').dependent(:delete) }
it { is_expected.to have_one(:import_data).class_name('ProjectImportData').dependent(:destroy) }
it { is_expected.to have_one(:import_data).class_name('ProjectImportData').dependent(:delete) }
it { is_expected.to have_one(:last_event).class_name('Event') }
it { is_expected.to have_one(:forked_from_project).through(:forked_project_link) }
it { is_expected.to have_many(:commit_statuses) }
......@@ -1446,16 +1446,13 @@ describe Project, models: true do
end
describe 'Project import job' do
let(:project) { create(:empty_project) }
let(:mirror) { false }
let(:project) { create(:empty_project, import_url: generate(:url)) }
before do
allow_any_instance_of(Gitlab::Shell).to receive(:import_repository)
.with(project.repository_storage_path, project.path_with_namespace, project.import_url)
.and_return(true)
allow(project).to receive(:repository_exists?).and_return(true)
expect_any_instance_of(Repository).to receive(:after_import)
.and_call_original
end
......@@ -1463,8 +1460,7 @@ describe Project, models: true do
it 'imports a project' do
expect_any_instance_of(RepositoryImportWorker).to receive(:perform).and_call_original
project.import_start
project.add_import_job
project.import_schedule
expect(project.reload.import_status).to eq('finished')
end
......@@ -1551,7 +1547,7 @@ describe Project, models: true do
describe '#add_import_job' do
context 'forked' do
let(:forked_project_link) { create(:forked_project_link) }
let(:forked_project_link) { create(:forked_project_link, :forked_to_empty_project) }
let(:forked_from_project) { forked_project_link.forked_from_project }
let(:project) { forked_project_link.forked_to_project }
......@@ -1565,9 +1561,9 @@ describe Project, models: true do
end
context 'not forked' do
let(:project) { create(:empty_project) }
it 'schedules a RepositoryImportWorker job' do
project = create(:empty_project, import_url: generate(:url))
expect(RepositoryImportWorker).to receive(:perform_async).with(project.id)
project.add_import_job
......
......@@ -9,11 +9,12 @@ describe GroupPolicy, models: true do
let(:admin) { create(:admin) }
let(:group) { create(:group) }
let(:reporter_permissions) { [:admin_label] }
let(:master_permissions) do
[
:create_projects,
:admin_milestones,
:admin_label
:admin_milestones
]
end
......@@ -42,6 +43,7 @@ describe GroupPolicy, models: true do
it do
is_expected.to include(:read_group)
is_expected.not_to include(*reporter_permissions)
is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
......@@ -52,6 +54,7 @@ describe GroupPolicy, models: true do
it do
is_expected.to include(:read_group)
is_expected.not_to include(*reporter_permissions)
is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
......@@ -62,6 +65,7 @@ describe GroupPolicy, models: true do
it do
is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
......@@ -72,6 +76,7 @@ describe GroupPolicy, models: true do
it do
is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
......@@ -82,6 +87,7 @@ describe GroupPolicy, models: true do
it do
is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
......@@ -92,6 +98,7 @@ describe GroupPolicy, models: true do
it do
is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.to include(*master_permissions)
is_expected.to include(*owner_permissions)
end
......@@ -102,14 +109,27 @@ describe GroupPolicy, models: true do
it do
is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.to include(*master_permissions)
is_expected.to include(*owner_permissions)
end
end
describe 'private nested group inherit permissions', :nested_groups do
describe 'private nested group use the highest access level from the group and inherited permissions', :nested_groups do
let(:nested_group) { create(:group, :private, parent: group) }
before do
nested_group.add_guest(guest)
nested_group.add_guest(reporter)
nested_group.add_guest(developer)
nested_group.add_guest(master)
group.owners.destroy_all
group.add_guest(owner)
nested_group.add_owner(owner)
end
subject { described_class.abilities(current_user, nested_group).to_set }
context 'with no user' do
......@@ -117,6 +137,7 @@ describe GroupPolicy, models: true do
it do
is_expected.not_to include(:read_group)
is_expected.not_to include(*reporter_permissions)
is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
......@@ -127,6 +148,7 @@ describe GroupPolicy, models: true do
it do
is_expected.to include(:read_group)
is_expected.not_to include(*reporter_permissions)
is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
......@@ -137,6 +159,7 @@ describe GroupPolicy, models: true do
it do
is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
......@@ -147,6 +170,7 @@ describe GroupPolicy, models: true do
it do
is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
......@@ -157,6 +181,7 @@ describe GroupPolicy, models: true do
it do
is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
......@@ -167,6 +192,7 @@ describe GroupPolicy, models: true do
it do
is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.to include(*master_permissions)
is_expected.to include(*owner_permissions)
end
......
......@@ -5,76 +5,217 @@ describe 'Git HTTP requests', lib: true do
include WorkhorseHelpers
include UserActivitiesHelpers
it "gives WWW-Authenticate hints" do
clone_get('doesnt/exist.git')
shared_examples 'pulls require Basic HTTP Authentication' do
context "when no credentials are provided" do
it "responds to downloads with status 401 Unauthorized (no project existence information leak)" do
download(path) do |response|
expect(response).to have_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
end
end
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
context "when only username is provided" do
it "responds to downloads with status 401 Unauthorized" do
download(path, user: user.username) do |response|
expect(response).to have_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
end
end
describe "User with no identities" do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, path: 'project.git-project') }
context "when username and password are provided" do
context "when authentication fails" do
it "responds to downloads with status 401 Unauthorized" do
download(path, user: user.username, password: "wrong-password") do |response|
expect(response).to have_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
end
end
context "when the project doesn't exist" do
context "when no authentication is provided" do
it "responds with status 401 (no project existence information leak)" do
download('doesnt/exist.git') do |response|
expect(response).to have_http_status(401)
context "when authentication succeeds" do
it "does not respond to downloads with status 401 Unauthorized" do
download(path, user: user.username, password: user.password) do |response|
expect(response).not_to have_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to be_nil
end
end
end
end
end
context "when username and password are provided" do
context "when authentication fails" do
it "responds with status 401" do
download('doesnt/exist.git', user: user.username, password: "nope") do |response|
expect(response).to have_http_status(401)
end
shared_examples 'pushes require Basic HTTP Authentication' do
context "when no credentials are provided" do
it "responds to uploads with status 401 Unauthorized (no project existence information leak)" do
upload(path) do |response|
expect(response).to have_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
end
end
context "when only username is provided" do
it "responds to uploads with status 401 Unauthorized" do
upload(path, user: user.username) do |response|
expect(response).to have_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
end
end
context "when username and password are provided" do
context "when authentication fails" do
it "responds to uploads with status 401 Unauthorized" do
upload(path, user: user.username, password: "wrong-password") do |response|
expect(response).to have_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
end
end
context "when authentication succeeds" do
it "responds with status 404" do
download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
expect(response).to have_http_status(404)
end
context "when authentication succeeds" do
it "does not respond to uploads with status 401 Unauthorized" do
upload(path, user: user.username, password: user.password) do |response|
expect(response).not_to have_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to be_nil
end
end
end
end
end
context "when the Wiki for a project exists" do
it "responds with the right project" do
wiki = ProjectWiki.new(project)
project.update_attribute(:visibility_level, Project::PUBLIC)
shared_examples_for 'pulls are allowed' do
it do
download(path, env) do |response|
expect(response).to have_http_status(:ok)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
end
download("/#{wiki.repository.path_with_namespace}.git") do |response|
json_body = ActiveSupport::JSON.decode(response.body)
shared_examples_for 'pushes are allowed' do
it do
upload(path, env) do |response|
expect(response).to have_http_status(:ok)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
end
expect(response).to have_http_status(200)
expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
describe "User with no identities" do
let(:user) { create(:user) }
context "when the project doesn't exist" do
let(:path) { 'doesnt/exist.git' }
it_behaves_like 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication'
context 'when authenticated' do
it 'rejects downloads and uploads with 404 Not Found' do
download_or_upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(:not_found)
end
end
end
end
context "when requesting the Wiki" do
let(:wiki) { ProjectWiki.new(project) }
let(:path) { "/#{wiki.repository.path_with_namespace}.git" }
context "when the project is public" do
let(:project) { create(:project, :repository, :public, :wiki_enabled) }
it_behaves_like 'pushes require Basic HTTP Authentication'
context 'when unauthenticated' do
let(:env) { {} }
context 'but the repo is disabled' do
let(:project) { create(:project, :repository_disabled, :wiki_enabled) }
let(:wiki) { ProjectWiki.new(project) }
let(:path) { "/#{wiki.repository.path_with_namespace}.git" }
it_behaves_like 'pulls are allowed'
before do
project.team << [user, :developer]
it "responds to pulls with the wiki's repo" do
download(path) do |response|
json_body = ActiveSupport::JSON.decode(response.body)
expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace)
end
end
end
it 'allows clones' do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(200)
context 'when authenticated' do
let(:env) { { user: user.username, password: user.password } }
context 'and as a developer on the team' do
before do
project.team << [user, :developer]
end
context 'but the repo is disabled' do
let(:project) { create(:project, :repository, :public, :repository_disabled, :wiki_enabled) }
it_behaves_like 'pulls are allowed'
it_behaves_like 'pushes are allowed'
end
end
context 'and not on the team' do
it_behaves_like 'pulls are allowed'
it 'rejects pushes with 403 Forbidden' do
upload(path, env) do |response|
expect(response).to have_http_status(:forbidden)
expect(response.body).to eq(git_access_wiki_error(:write_to_wiki))
end
end
end
end
end
it 'allows pushes' do
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(200)
context "when the project is private" do
let(:project) { create(:project, :repository, :private, :wiki_enabled) }
it_behaves_like 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication'
context 'when authenticated' do
context 'and as a developer on the team' do
before do
project.team << [user, :developer]
end
context 'but the repo is disabled' do
let(:project) { create(:project, :repository, :private, :repository_disabled, :wiki_enabled) }
it 'allows clones' do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(:ok)
end
end
it 'pushes are allowed' do
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(:ok)
end
end
end
end
context 'and not on the team' do
it 'rejects clones with 404 Not Found' do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(:not_found)
expect(response.body).to eq(git_access_error(:project_not_found))
end
end
it 'rejects pushes with 404 Not Found' do
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(:not_found)
expect(response.body).to eq(git_access_error(:project_not_found))
end
end
end
end
end
......@@ -84,49 +225,60 @@ describe 'Git HTTP requests', lib: true do
let(:path) { "#{project.path_with_namespace}.git" }
context "when the project is public" do
before do
project.update_attribute(:visibility_level, Project::PUBLIC)
end
let(:project) { create(:project, :repository, :public) }
it "downloads get status 200" do
download(path, {}) do |response|
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
it_behaves_like 'pushes require Basic HTTP Authentication'
it "uploads get status 401" do
upload(path, {}) do |response|
expect(response).to have_http_status(401)
end
context 'when not authenticated' do
let(:env) { {} }
it_behaves_like 'pulls are allowed'
end
context "with correct credentials" do
context "when authenticated" do
let(:env) { { user: user.username, password: user.password } }
it "uploads get status 403" do
upload(path, env) do |response|
expect(response).to have_http_status(403)
context 'as a developer on the team' do
before do
project.team << [user, :developer]
end
end
context 'but git-receive-pack is disabled' do
it "responds with status 404" do
allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
it_behaves_like 'pulls are allowed'
it_behaves_like 'pushes are allowed'
upload(path, env) do |response|
expect(response).to have_http_status(403)
context 'but git-receive-pack over HTTP is disabled in config' do
before do
allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
end
it 'rejects pushes with 403 Forbidden' do
upload(path, env) do |response|
expect(response).to have_http_status(:forbidden)
expect(response.body).to eq(git_access_error(:receive_pack_disabled_over_http))
end
end
end
context 'but git-upload-pack over HTTP is disabled in config' do
it "rejects pushes with 403 Forbidden" do
allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
download(path, env) do |response|
expect(response).to have_http_status(:forbidden)
expect(response.body).to eq(git_access_error(:upload_pack_disabled_over_http))
end
end
end
end
end
context 'but git-upload-pack is disabled' do
it "responds with status 404" do
allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
context 'and not a member of the team' do
it_behaves_like 'pulls are allowed'
download(path, {}) do |response|
expect(response).to have_http_status(404)
it 'rejects pushes with 403 Forbidden' do
upload(path, env) do |response|
expect(response).to have_http_status(:forbidden)
expect(response.body).to eq(change_access_error(:push_code))
end
end
end
end
......@@ -141,66 +293,41 @@ describe 'Git HTTP requests', lib: true do
context 'when the repo is public' do
context 'but the repo is disabled' do
it 'does not allow to clone the repo' do
project = create(:project, :public, :repository_disabled)
let(:project) { create(:project, :public, :repository, :repository_disabled) }
let(:path) { "#{project.path_with_namespace}.git" }
let(:env) { {} }
download("#{project.path_with_namespace}.git", {}) do |response|
expect(response).to have_http_status(:unauthorized)
end
end
it_behaves_like 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication'
end
context 'but the repo is enabled' do
it 'allows to clone the repo' do
project = create(:project, :public, :repository_enabled)
let(:project) { create(:project, :public, :repository, :repository_enabled) }
let(:path) { "#{project.path_with_namespace}.git" }
let(:env) { {} }
download("#{project.path_with_namespace}.git", {}) do |response|
expect(response).to have_http_status(:ok)
end
end
it_behaves_like 'pulls are allowed'
end
context 'but only project members are allowed' do
it 'does not allow to clone the repo' do
project = create(:project, :public, :repository_private)
let(:project) { create(:project, :public, :repository, :repository_private) }
download("#{project.path_with_namespace}.git", {}) do |response|
expect(response).to have_http_status(:unauthorized)
end
end
it_behaves_like 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication'
end
end
end
context "when the project is private" do
before do
project.update_attribute(:visibility_level, Project::PRIVATE)
end
context "when no authentication is provided" do
it "responds with status 401 to downloads" do
download(path, {}) do |response|
expect(response).to have_http_status(401)
end
end
let(:project) { create(:project, :repository, :private) }
it "responds with status 401 to uploads" do
upload(path, {}) do |response|
expect(response).to have_http_status(401)
end
end
end
it_behaves_like 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication'
context "when username and password are provided" do
let(:env) { { user: user.username, password: 'nope' } }
context "when authentication fails" do
it "responds with status 401" do
download(path, env) do |response|
expect(response).to have_http_status(401)
end
end
context "when the user is IP banned" do
it "responds with status 401" do
expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
......@@ -208,7 +335,7 @@ describe 'Git HTTP requests', lib: true do
clone_get(path, env)
expect(response).to have_http_status(401)
expect(response).to have_http_status(:unauthorized)
end
end
end
......@@ -222,37 +349,39 @@ describe 'Git HTTP requests', lib: true do
end
context "when the user is blocked" do
it "responds with status 401" do
it "rejects pulls with 401 Unauthorized" do
user.block
project.team << [user, :master]
download(path, env) do |response|
expect(response).to have_http_status(401)
expect(response).to have_http_status(:unauthorized)
end
end
it "responds with status 401 for unknown projects (no project existence information leak)" do
it "rejects pulls with 401 Unauthorized for unknown projects (no project existence information leak)" do
user.block
download('doesnt/exist.git', env) do |response|
expect(response).to have_http_status(401)
expect(response).to have_http_status(:unauthorized)
end
end
end
context "when the user isn't blocked" do
it "downloads get status 200" do
expect(Rack::Attack::Allow2Ban).to receive(:reset)
clone_get(path, env)
it "resets the IP in Rack Attack on download" do
expect(Rack::Attack::Allow2Ban).to receive(:reset).twice
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
download(path, env) do
expect(response).to have_http_status(:ok)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
it "uploads get status 200" do
upload(path, env) do |response|
expect(response).to have_http_status(200)
it "resets the IP in Rack Attack on upload" do
expect(Rack::Attack::Allow2Ban).to receive(:reset).twice
upload(path, env) do
expect(response).to have_http_status(:ok)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
......@@ -272,56 +401,43 @@ describe 'Git HTTP requests', lib: true do
@token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api")
end
it "downloads get status 200" do
clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
it "uploads get status 200" do
push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
let(:path) { "#{project.path_with_namespace}.git" }
let(:env) { { user: 'oauth2', password: @token.token } }
expect(response).to have_http_status(200)
end
it_behaves_like 'pulls are allowed'
it_behaves_like 'pushes are allowed'
end
context 'when user has 2FA enabled' do
let(:user) { create(:user, :two_factor) }
let(:access_token) { create(:personal_access_token, user: user) }
let(:path) { "#{project.path_with_namespace}.git" }
before do
project.team << [user, :master]
end
context 'when username and password are provided' do
it 'rejects the clone attempt' do
download("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
expect(response).to have_http_status(401)
it 'rejects pulls with 2FA error message' do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(:unauthorized)
expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
end
end
it 'rejects the push attempt' do
upload("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
expect(response).to have_http_status(401)
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(:unauthorized)
expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
end
end
end
context 'when username and personal access token are provided' do
it 'allows clones' do
download("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
expect(response).to have_http_status(200)
end
end
let(:env) { { user: user.username, password: access_token.token } }
it 'allows pushes' do
upload("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
expect(response).to have_http_status(200)
end
end
it_behaves_like 'pulls are allowed'
it_behaves_like 'pushes are allowed'
end
end
......@@ -357,15 +473,15 @@ describe 'Git HTTP requests', lib: true do
end
context "when the user doesn't have access to the project" do
it "downloads get status 404" do
it "pulls get status 404" do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(404)
expect(response).to have_http_status(:not_found)
end
end
it "uploads get status 404" do
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(404)
expect(response).to have_http_status(:not_found)
end
end
end
......@@ -373,28 +489,41 @@ describe 'Git HTTP requests', lib: true do
end
context "when a gitlab ci token is provided" do
let(:project) { create(:project, :repository) }
let(:build) { create(:ci_build, :running) }
let(:project) { build.project }
let(:other_project) { create(:empty_project) }
context 'when build created by system is authenticated' do
it "downloads get status 200" do
clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
it "uploads get status 401 (no project existence information leak)" do
push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
before do
build.update!(project: project) # can't associate it on factory create
end
expect(response).to have_http_status(401)
context 'when build created by system is authenticated' do
let(:path) { "#{project.path_with_namespace}.git" }
let(:env) { { user: 'gitlab-ci-token', password: build.token } }
it_behaves_like 'pulls are allowed'
# A non-401 here is not an information leak since the system is
# "authenticated" as CI using the correct token. It does not have
# push access, so pushes should be rejected as forbidden, and giving
# a reason is fine.
#
# We know for sure it is not an information leak since pulls using
# the build token must be allowed.
it "rejects pushes with 403 Forbidden" do
push_get(path, env)
expect(response).to have_http_status(:forbidden)
expect(response.body).to eq(git_access_error(:upload))
end
it "downloads from other project get status 404" do
clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
# We are "authenticated" as CI using a valid token here. But we are
# not authorized to see any other project, so return "not found".
it "rejects pulls for other project with 404 Not Found" do
clone_get("#{other_project.path_with_namespace}.git", env)
expect(response).to have_http_status(404)
expect(response).to have_http_status(:not_found)
expect(response.body).to eq(git_access_error(:project_not_found))
end
end
......@@ -405,31 +534,27 @@ describe 'Git HTTP requests', lib: true do
end
shared_examples 'can download code only' do
it 'downloads get status 200' do
allow_any_instance_of(Repository).
to receive(:exists?).and_return(true)
clone_get "#{project.path_with_namespace}.git",
user: 'gitlab-ci-token', password: build.token
let(:path) { "#{project.path_with_namespace}.git" }
let(:env) { { user: 'gitlab-ci-token', password: build.token } }
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
it_behaves_like 'pulls are allowed'
it 'downloads from non-existing repository and gets 403' do
allow_any_instance_of(Repository).
to receive(:exists?).and_return(false)
context 'when the repo does not exist' do
let(:project) { create(:empty_project) }
clone_get "#{project.path_with_namespace}.git",
user: 'gitlab-ci-token', password: build.token
it 'rejects pulls with 403 Forbidden' do
clone_get path, env
expect(response).to have_http_status(403)
expect(response).to have_http_status(:forbidden)
expect(response.body).to eq(git_access_error(:no_repo))
end
end
it 'uploads get status 403' do
push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
it 'rejects pushes with 403 Forbidden' do
push_get path, env
expect(response).to have_http_status(401)
expect(response).to have_http_status(:forbidden)
expect(response.body).to eq(git_access_error(:upload))
end
end
......@@ -441,7 +566,7 @@ describe 'Git HTTP requests', lib: true do
it 'downloads from other project get status 403' do
clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
expect(response).to have_http_status(403)
expect(response).to have_http_status(:forbidden)
end
end
......@@ -453,91 +578,93 @@ describe 'Git HTTP requests', lib: true do
it 'downloads from other project get status 404' do
clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
expect(response).to have_http_status(404)
expect(response).to have_http_status(:not_found)
end
end
end
end
end
end
context "when the project path doesn't end in .git" do
context "GET info/refs" do
let(:path) { "/#{project.path_with_namespace}/info/refs" }
context "when the project path doesn't end in .git" do
let(:project) { create(:project, :repository, :public, path: 'project.git-project') }
context "GET info/refs" do
let(:path) { "/#{project.path_with_namespace}/info/refs" }
context "when no params are added" do
before { get path }
context "when no params are added" do
before { get path }
it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs")
it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs")
end
end
end
context "when the upload-pack service is requested" do
let(:params) { { service: 'git-upload-pack' } }
before { get path, params }
context "when the upload-pack service is requested" do
let(:params) { { service: 'git-upload-pack' } }
before { get path, params }
it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
end
end
end
context "when the receive-pack service is requested" do
let(:params) { { service: 'git-receive-pack' } }
before { get path, params }
context "when the receive-pack service is requested" do
let(:params) { { service: 'git-receive-pack' } }
before { get path, params }
it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
end
end
end
context "when the params are anything else" do
let(:params) { { service: 'git-implode-pack' } }
before { get path, params }
context "when the params are anything else" do
let(:params) { { service: 'git-implode-pack' } }
before { get path, params }
it "redirects to the sign-in page" do
expect(response).to redirect_to(new_user_session_path)
it "redirects to the sign-in page" do
expect(response).to redirect_to(new_user_session_path)
end
end
end
end
context "POST git-upload-pack" do
it "fails to find a route" do
expect { clone_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
context "POST git-upload-pack" do
it "fails to find a route" do
expect { clone_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
end
end
end
context "POST git-receive-pack" do
it "failes to find a route" do
expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
context "POST git-receive-pack" do
it "failes to find a route" do
expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
end
end
end
end
context "retrieving an info/refs file" do
before { project.update_attribute(:visibility_level, Project::PUBLIC) }
context "retrieving an info/refs file" do
let(:project) { create(:project, :repository, :public) }
context "when the file exists" do
before do
# Provide a dummy file in its place
allow_any_instance_of(Repository).to receive(:blob_at).and_call_original
allow_any_instance_of(Repository).to receive(:blob_at).with('b83d6e391c22777fca1ed3012fce84f633d7fed0', 'info/refs') do
Gitlab::Git::Blob.find(project.repository, 'master', 'bar/branch-test.txt')
end
context "when the file exists" do
before do
# Provide a dummy file in its place
allow_any_instance_of(Repository).to receive(:blob_at).and_call_original
allow_any_instance_of(Repository).to receive(:blob_at).with('b83d6e391c22777fca1ed3012fce84f633d7fed0', 'info/refs') do
Gitlab::Git::Blob.find(project.repository, 'master', 'bar/branch-test.txt')
get "/#{project.path_with_namespace}/blob/master/info/refs"
end
get "/#{project.path_with_namespace}/blob/master/info/refs"
it "returns the file" do
expect(response).to have_http_status(:ok)
end
end
it "returns the file" do
expect(response).to have_http_status(200)
end
end
context "when the file does not exist" do
before { get "/#{project.path_with_namespace}/blob/master/info/refs" }
context "when the file does not exist" do
before { get "/#{project.path_with_namespace}/blob/master/info/refs" }
it "returns not found" do
expect(response).to have_http_status(404)
it "returns not found" do
expect(response).to have_http_status(:not_found)
end
end
end
end
......@@ -546,6 +673,7 @@ describe 'Git HTTP requests', lib: true do
describe "User with LDAP identity" do
let(:user) { create(:omniauth_user, extern_uid: dn) }
let(:dn) { 'uid=john,ou=people,dc=example,dc=com' }
let(:path) { 'doesnt/exist.git' }
before do
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
......@@ -553,44 +681,36 @@ describe 'Git HTTP requests', lib: true do
allow(Gitlab::LDAP::Authentication).to receive(:login).with(user.username, user.password).and_return(user)
end
context "when authentication fails" do
context "when no authentication is provided" do
it "responds with status 401" do
download('doesnt/exist.git') do |response|
expect(response).to have_http_status(401)
end
end
end
context "when username and invalid password are provided" do
it "responds with status 401" do
download('doesnt/exist.git', user: user.username, password: "nope") do |response|
expect(response).to have_http_status(401)
end
end
end
end
it_behaves_like 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication'
context "when authentication succeeds" do
context "when the project doesn't exist" do
it "responds with status 404" do
download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
expect(response).to have_http_status(404)
it "responds with status 404 Not Found" do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(:not_found)
end
end
end
context "when the project exists" do
let(:project) { create(:project, path: 'project.git-project') }
let(:project) { create(:project, :repository) }
let(:path) { "#{project.full_path}.git" }
let(:env) { { user: user.username, password: user.password } }
before do
project.team << [user, :master]
end
context 'and the user is on the team' do
before do
project.team << [user, :master]
end
it "responds with status 200" do
clone_get(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(200)
it "responds with status 200" do
clone_get(path, env) do |response|
expect(response).to have_http_status(200)
end
end
it_behaves_like 'pulls are allowed'
it_behaves_like 'pushes are allowed'
end
end
end
......
......@@ -759,8 +759,8 @@ describe 'Git LFS API and storage' do
context 'tries to push to own project' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'responds with 401' do
expect(response).to have_http_status(401)
it 'responds with 403 (not 404 because project is public)' do
expect(response).to have_http_status(403)
end
end
......@@ -769,8 +769,9 @@ describe 'Git LFS API and storage' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'responds with 401' do
expect(response).to have_http_status(401)
# I'm not sure what this tests that is different from the previous test
it 'responds with 403 (not 404 because project is public)' do
expect(response).to have_http_status(403)
end
end
end
......@@ -778,8 +779,8 @@ describe 'Git LFS API and storage' do
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it 'responds with 401' do
expect(response).to have_http_status(401)
it 'responds with 403 (not 404 because project is public)' do
expect(response).to have_http_status(403)
end
end
end
......@@ -979,8 +980,8 @@ describe 'Git LFS API and storage' do
put_authorize
end
it 'responds with 401' do
expect(response).to have_http_status(401)
it 'responds with 403 (not 404 because the build user can read the project)' do
expect(response).to have_http_status(403)
end
end
......@@ -993,8 +994,8 @@ describe 'Git LFS API and storage' do
put_authorize
end
it 'responds with 401' do
expect(response).to have_http_status(401)
it 'responds with 404 (do not leak non-public project existence)' do
expect(response).to have_http_status(404)
end
end
end
......@@ -1006,8 +1007,8 @@ describe 'Git LFS API and storage' do
put_authorize
end
it 'responds with 401' do
expect(response).to have_http_status(401)
it 'responds with 404 (do not leak non-public project existence)' do
expect(response).to have_http_status(404)
end
end
end
......@@ -1079,8 +1080,8 @@ describe 'Git LFS API and storage' do
context 'tries to push to own project' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'responds with 401' do
expect(response).to have_http_status(401)
it 'responds with 403 (not 404 because project is public)' do
expect(response).to have_http_status(403)
end
end
......@@ -1089,8 +1090,9 @@ describe 'Git LFS API and storage' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'responds with 401' do
expect(response).to have_http_status(401)
# I'm not sure what this tests that is different from the previous test
it 'responds with 403 (not 404 because project is public)' do
expect(response).to have_http_status(403)
end
end
end
......@@ -1098,8 +1100,8 @@ describe 'Git LFS API and storage' do
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it 'responds with 401' do
expect(response).to have_http_status(401)
it 'responds with 403 (not 404 because project is public)' do
expect(response).to have_http_status(403)
end
end
end
......
......@@ -161,15 +161,13 @@ describe Projects::CreateService, '#execute', services: true do
end
context 'when a bad service template is created' do
before do
create(:service, type: 'DroneCiService', project: nil, template: true, active: true)
end
it 'reports an error in the imported project' do
opts[:import_url] = 'http://www.gitlab.com/gitlab-org/gitlab-ce'
create(:service, type: 'DroneCiService', project: nil, template: true, active: true)
project = create_project(user, opts)
expect(project.errors.full_messages_for(:base).first).to match /Unable to save project. Error: Unable to save DroneCiService/
expect(project.errors.full_messages_for(:base).first).to match(/Unable to save project. Error: Unable to save DroneCiService/)
expect(project.services.count).to eq 0
end
end
......
......@@ -35,9 +35,14 @@ module GitHttpHelpers
yield response
end
def download_or_upload(*args, &block)
download(*args, &block)
upload(*args, &block)
end
def auth_env(user, password, spnego_request_token)
env = workhorse_internal_api_request_header
if user && password
if user
env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password)
elsif spnego_request_token
env['HTTP_AUTHORIZATION'] = "Negotiate #{::Base64.strict_encode64('opaque_request_token')}"
......@@ -45,4 +50,19 @@ module GitHttpHelpers
env
end
def git_access_error(error_key)
message = Gitlab::GitAccess::ERROR_MESSAGES[error_key]
message || raise("GitAccess error message key '#{error_key}' not found")
end
def git_access_wiki_error(error_key)
message = Gitlab::GitAccessWiki::ERROR_MESSAGES[error_key]
message || raise("GitAccessWiki error message key '#{error_key}' not found")
end
def change_access_error(error_key)
message = Gitlab::Checks::ChangeAccess::ERROR_MESSAGES[error_key]
message || raise("ChangeAccess error message key '#{error_key}' not found")
end
end
require 'spec_helper'
describe RepositoryForkWorker do
let(:project) { create(:project, :repository) }
let(:project) { create(:project, :repository, :import_scheduled) }
let(:fork_project) { create(:project, :repository, forked_from_project: project) }
let(:shell) { Gitlab::Shell.new }
......@@ -46,15 +46,27 @@ describe RepositoryForkWorker do
end
it "handles bad fork" do
source_path = project.full_path
target_path = fork_project.namespace.full_path
error_message = "Unable to fork project #{project.id} for repository #{source_path} -> #{target_path}"
expect(shell).to receive(:fork_repository).and_return(false)
expect(subject.logger).to receive(:error)
expect do
subject.perform(project.id, '/test/path', source_path, target_path)
end.to raise_error(RepositoryForkWorker::ForkError, error_message)
end
subject.perform(
project.id,
'/test/path',
project.full_path,
fork_project.namespace.full_path)
it 'handles unexpected error' do
source_path = project.full_path
target_path = fork_project.namespace.full_path
allow_any_instance_of(Gitlab::Shell).to receive(:fork_repository).and_raise(RuntimeError)
expect do
subject.perform(project.id, '/test/path', source_path, target_path)
end.to raise_error(RepositoryForkWorker::ForkError)
expect(project.reload.import_status).to eq('failed')
end
end
end
require 'spec_helper'
describe RepositoryImportWorker do
let(:project) { create(:empty_project) }
let(:project) { create(:empty_project, :import_scheduled) }
subject { described_class.new }
......@@ -21,15 +21,26 @@ describe RepositoryImportWorker do
context 'when the import has failed' do
it 'hide the credentials that were used in the import URL' do
error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
expect_any_instance_of(Projects::ImportService).to receive(:execute).
and_return({ status: :error, message: error })
allow(subject).to receive(:jid).and_return('123')
subject.perform(project.id)
expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error })
allow(subject).to receive(:jid).and_return('123')
expect(project.reload.import_error).to include("https://*****:*****@test.com/root/repoC.git/")
expect do
subject.perform(project.id)
end.to raise_error(RepositoryImportWorker::ImportError, error)
expect(project.reload.import_jid).not_to be_nil
end
end
context 'with unexpected error' do
it 'marks import as failed' do
allow_any_instance_of(Projects::ImportService).to receive(:execute).and_raise(RuntimeError)
expect do
subject.perform(project.id)
end.to raise_error(RepositoryImportWorker::ImportError)
expect(project.reload.import_status).to eq('failed')
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