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 @@ ...@@ -56,6 +56,8 @@
if (job.import_status === 'finished') { if (job.import_status === 'finished') {
job_item.removeClass("active").addClass("success"); job_item.removeClass("active").addClass("success");
return status_field.html('<span><i class="fa fa-check"></i> done</span>'); 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') { } else if (job.import_status === 'started') {
return status_field.html("<i class='fa fa-spinner fa-spin'></i> started"); return status_field.html("<i class='fa fa-spinner fa-spin'></i> started");
} else { } 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 */ /* 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() { (function() {
this.ProjectNew = (function() { this.ProjectNew = (function() {
function ProjectNew() { function ProjectNew() {
this.toggleSettings = this.toggleSettings.bind(this); this.toggleSettings = this.toggleSettings.bind(this);
this.$selects = $('.features select'); this.$selects = $('.features select');
this.$repoSelects = this.$selects.filter('.js-repo-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) { $('.project-edit-container').on('ajax:before', (function(_this) {
return function() { return function() {
...@@ -26,6 +32,42 @@ ...@@ -26,6 +32,42 @@
if (!visibilityContainer) return; if (!visibilityContainer) return;
const visibilitySelect = new gl.VisibilitySelect(visibilityContainer); const visibilitySelect = new gl.VisibilitySelect(visibilityContainer);
visibilitySelect.init(); 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() { ProjectNew.prototype.toggleSettings = function() {
...@@ -56,8 +98,10 @@ ...@@ -56,8 +98,10 @@
ProjectNew.prototype.toggleRepoVisibility = function () { ProjectNew.prototype.toggleRepoVisibility = function () {
var $repoAccessLevel = $('.js-repo-access-level select'); var $repoAccessLevel = $('.js-repo-access-level select');
var $lfsEnabledOption = $('.js-lfs-enabled select');
var containerRegistry = document.querySelectorAll('.js-container-registry')[0]; var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled'); var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
var prevSelectedVal = parseInt($repoAccessLevel.val(), 10);
this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']") this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
.nextAll() .nextAll()
...@@ -71,29 +115,40 @@ ...@@ -71,29 +115,40 @@
var $this = $(this); var $this = $(this);
var repoSelectVal = parseInt($this.val(), 10); var repoSelectVal = parseInt($this.val(), 10);
$this.find('option').show(); $this.find('option').enable();
if (selectedVal < repoSelectVal) { if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) {
$this.val(selectedVal); $this.val(selectedVal).trigger('change');
highlightChanges($this);
} }
$this.find("option[value='" + selectedVal + "']").nextAll().hide(); $this.find("option[value='" + selectedVal + "']").nextAll().disable();
}); });
if (selectedVal) { if (selectedVal) {
this.$repoSelects.removeClass('disabled'); this.$repoSelects.removeClass('disabled');
if ($lfsEnabledOption.length) {
$lfsEnabledOption.removeClass('disabled');
highlightChanges($lfsEnabledOption);
}
if (containerRegistry) { if (containerRegistry) {
containerRegistry.style.display = ''; containerRegistry.style.display = '';
} }
} else { } else {
this.$repoSelects.addClass('disabled'); this.$repoSelects.addClass('disabled');
if ($lfsEnabledOption.length) {
$lfsEnabledOption.val('false').addClass('disabled');
highlightChanges($lfsEnabledOption);
}
if (containerRegistry) { if (containerRegistry) {
containerRegistry.style.display = 'none'; containerRegistry.style.display = 'none';
containerRegistryCheckbox.checked = false; containerRegistryCheckbox.checked = false;
} }
} }
prevSelectedVal = selectedVal;
}.bind(this)); }.bind(this));
}; };
......
...@@ -10,7 +10,7 @@ export default class ProtectedTagDropdown { ...@@ -10,7 +10,7 @@ export default class ProtectedTagDropdown {
this.$dropdown = options.$dropdown; this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent(); this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); 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.buildDropdown();
this.bindEvents(); this.bindEvents();
...@@ -73,7 +73,7 @@ export default class ProtectedTagDropdown { ...@@ -73,7 +73,7 @@ export default class ProtectedTagDropdown {
}; };
this.$dropdownContainer this.$dropdownContainer
.find('.create-new-protected-tag code') .find('.js-create-new-protected-tag code')
.text(tagName); .text(tagName);
} }
......
...@@ -187,6 +187,7 @@ $divergence-graph-bar-bg: #ccc; ...@@ -187,6 +187,7 @@ $divergence-graph-bar-bg: #ccc;
$divergence-graph-separator-bg: #ccc; $divergence-graph-separator-bg: #ccc;
$general-hover-transition-duration: 100ms; $general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear; $general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232);
/* /*
......
...@@ -29,6 +29,20 @@ ...@@ -29,6 +29,20 @@
& > .form-group { & > .form-group {
padding-left: 0; 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 { .help-block {
...@@ -675,14 +689,16 @@ pre.light-well { ...@@ -675,14 +689,16 @@ pre.light-well {
} }
} }
.new_protected_branch { .new_protected_branch,
.new-protected-tag {
label { label {
margin-top: 6px; margin-top: 6px;
font-weight: normal; font-weight: normal;
} }
} }
.create-new-protected-branch-button { .create-new-protected-branch-button,
.create-new-protected-tag-button {
@include dropdown-link; @include dropdown-link;
width: 100%; width: 100%;
......
...@@ -138,7 +138,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -138,7 +138,7 @@ class Admin::UsersController < Admin::ApplicationController
end end
def destroy 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| respond_to do |format|
format.html { redirect_to admin_users_path, notice: "The user is being deleted." } format.html { redirect_to admin_users_path, notice: "The user is being deleted." }
......
...@@ -106,4 +106,8 @@ module LfsRequest ...@@ -106,4 +106,8 @@ module LfsRequest
def objects def objects
@objects ||= (params[:objects] || []).to_a @objects ||= (params[:objects] || []).to_a
end end
def has_authentication_ability?(capability)
(authentication_abilities || []).include?(capability)
end
end end
...@@ -128,32 +128,10 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -128,32 +128,10 @@ class Projects::GitHttpClientController < Projects::ApplicationController
@authentication_result = Gitlab::Auth.find_for_git_client( @authentication_result = Gitlab::Auth.find_for_git_client(
login, password, project: project, ip: request.ip) login, password, project: project, ip: request.ip)
return false unless @authentication_result.success? @authentication_result.success?
if download_request?
authentication_has_download_access?
else
authentication_has_upload_access?
end
end end
def ci? def ci?
authentication_result.ci?(project) authentication_result.ci?(project)
end 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 end
class Projects::GitHttpController < Projects::GitHttpClientController class Projects::GitHttpController < Projects::GitHttpClientController
include WorkhorseRequest 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-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
def info_refs def info_refs
if upload_pack? && upload_pack_allowed? log_user_activity if upload_pack?
log_user_activity
render_ok render_ok
elsif receive_pack? && receive_pack_allowed?
render_ok
elsif http_blocked?
render_http_not_allowed
else
render_denied
end
end end
# POST /foo/bar.git/git-upload-pack (git pull) # POST /foo/bar.git/git-upload-pack (git pull)
def git_upload_pack def git_upload_pack
if upload_pack? && upload_pack_allowed?
render_ok render_ok
else
render_denied
end
end end
# POST /foo/bar.git/git-receive-pack" (git push) # POST /foo/bar.git/git-receive-pack" (git push)
def git_receive_pack def git_receive_pack
if receive_pack? && receive_pack_allowed?
render_ok render_ok
else
render_denied
end
end end
private private
...@@ -45,10 +34,6 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -45,10 +34,6 @@ class Projects::GitHttpController < Projects::GitHttpClientController
git_command == 'git-upload-pack' git_command == 'git-upload-pack'
end end
def receive_pack?
git_command == 'git-receive-pack'
end
def git_command def git_command
if action_name == 'info_refs' if action_name == 'info_refs'
params[:service] params[:service]
...@@ -62,47 +47,27 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -62,47 +47,27 @@ class Projects::GitHttpController < Projects::GitHttpClientController
render json: Gitlab::Workhorse.git_http_ok(repository, wiki?, user, action_name) render json: Gitlab::Workhorse.git_http_ok(repository, wiki?, user, action_name)
end end
def render_http_not_allowed def render_403(exception)
render plain: access_check.message, status: :forbidden render plain: exception.message, status: :forbidden
end end
def render_denied def render_404(exception)
if user && can?(user, :read_project, project) render plain: exception.message, status: :not_found
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'
end end
def upload_pack_allowed? def access
return false unless Gitlab.config.gitlab_shell.upload_pack @access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities)
access_check.allowed? || ci?
end end
def access def access_actor
@access ||= access_klass.new(user, project, 'http', authentication_abilities: authentication_abilities) return user if user
return :ci if ci?
end end
def access_check def access_check
# Use the magic string '_any' to indicate we do not know what the # Use the magic string '_any' to indicate we do not know what the
# changes are. This is also what gitlab-shell does. # changes are. This is also what gitlab-shell does.
@access_check ||= access.check(git_command, '_any') 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?
end end
def access_klass def access_klass
......
...@@ -14,14 +14,7 @@ class Projects::ImportsController < Projects::ApplicationController ...@@ -14,14 +14,7 @@ class Projects::ImportsController < Projects::ApplicationController
@project.import_url = params[:project][:import_url] @project.import_url = params[:project][:import_url]
if @project.save if @project.save
@project.reload @project.reload.import_schedule
if @project.import_failed?
@project.import_retry
else
@project.import_start
@project.add_import_job
end
end end
redirect_to namespace_project_import_path(@project.namespace, @project) redirect_to namespace_project_import_path(@project.namespace, @project)
......
...@@ -19,7 +19,7 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController ...@@ -19,7 +19,7 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
def protected_ref_params def protected_ref_params
params.require(:protected_branch).permit(:name, params.require(:protected_branch).permit(:name,
merge_access_levels_attributes: [:access_level, :id], merge_access_levels_attributes: access_level_attributes,
push_access_levels_attributes: [:access_level, :id]) push_access_levels_attributes: access_level_attributes)
end end
end end
...@@ -44,4 +44,10 @@ class Projects::ProtectedRefsController < Projects::ApplicationController ...@@ -44,4 +44,10 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
format.js { head :ok } format.js { head :ok }
end end
end end
protected
def access_level_attributes
%i(access_level id)
end
end end
...@@ -18,6 +18,6 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController ...@@ -18,6 +18,6 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController
end end
def protected_ref_params 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
end end
...@@ -25,7 +25,7 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -25,7 +25,7 @@ class RegistrationsController < Devise::RegistrationsController
end end
def destroy def destroy
DeleteUserWorker.perform_async(current_user.id, current_user.id) current_user.delete_async(deleted_by: current_user)
respond_to do |format| respond_to do |format|
format.html do format.html do
......
...@@ -138,11 +138,15 @@ module ProjectsHelper ...@@ -138,11 +138,15 @@ module ProjectsHelper
if @project.private? if @project.private?
level = @project.project_feature.send(field) level = @project.project_feature.send(field)
options.delete('Everyone with access') disabled_option = ProjectFeature::ENABLED
highest_available_option = options.values.max if level == ProjectFeature::ENABLED highest_available_option = ProjectFeature::PRIVATE if level == disabled_option
end 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( content_tag(
:select, :select,
......
...@@ -13,6 +13,17 @@ module SubmoduleHelper ...@@ -13,6 +13,17 @@ module SubmoduleHelper
if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/ if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
namespace, project = $1, $2 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.rstrip!
project.sub!(/\.git\z/, '') project.sub!(/\.git\z/, '')
......
...@@ -31,9 +31,9 @@ module VisibilityLevelHelper ...@@ -31,9 +31,9 @@ module VisibilityLevelHelper
when Gitlab::VisibilityLevel::PRIVATE when Gitlab::VisibilityLevel::PRIVATE
"Project access must be granted explicitly to each user." "Project access must be granted explicitly to each user."
when Gitlab::VisibilityLevel::INTERNAL 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 when Gitlab::VisibilityLevel::PUBLIC
"The project can be cloned without any authentication." "The project can be accessed without any authentication."
end end
end end
......
...@@ -15,8 +15,7 @@ class AbuseReport < ActiveRecord::Base ...@@ -15,8 +15,7 @@ class AbuseReport < ActiveRecord::Base
alias_method :author, :reporter alias_method :author, :reporter
def remove_user(deleted_by:) def remove_user(deleted_by:)
user.block user.delete_async(deleted_by: deleted_by, params: { hard_delete: true })
DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true, hard_delete: true)
end end
def notify def notify
......
...@@ -199,7 +199,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -199,7 +199,7 @@ class ApplicationSetting < ActiveRecord::Base
ApplicationSetting.define_attribute_methods ApplicationSetting.define_attribute_methods
end end
def self.defaults_ce def self.defaults
{ {
after_sign_up_text: nil, after_sign_up_text: nil,
akismet_enabled: false, akismet_enabled: false,
...@@ -250,10 +250,6 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -250,10 +250,6 @@ class ApplicationSetting < ActiveRecord::Base
} }
end end
def self.defaults
defaults_ce
end
def self.create_from_defaults def self.create_from_defaults
create(defaults) create(defaults)
end end
......
...@@ -8,32 +8,44 @@ module ProtectedRef ...@@ -8,32 +8,44 @@ module ProtectedRef
validates :project, presence: true validates :project, presence: true
delegate :matching, :matches?, :wildcard?, to: :ref_matcher 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_levels_for_ref(ref, action: action).any? do |access_level|
access_level.check_access(user) access_level.check_access(user)
end end
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_levels_for_ref(ref, action: action).any? do |access_level|
access_level.access_level == Gitlab::Access::DEVELOPER access_level.access_level == Gitlab::Access::DEVELOPER
end end
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 self.matching(ref).map(&:"#{action}_access_levels").flatten
end 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) ProtectedRefMatcher.matching(self, ref_name, protected_refs: protected_refs)
end end
end end
def commit
project.commit(self.name)
end
private private
def ref_matcher def ref_matcher
......
...@@ -222,6 +222,16 @@ class Group < Namespace ...@@ -222,6 +222,16 @@ class Group < Namespace
User.where(id: members_with_parents.select(:user_id)) User.where(id: members_with_parents.select(:user_id))
end 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 def mattermost_team_params
max_length = 59 max_length = 59
......
...@@ -200,6 +200,10 @@ class Member < ActiveRecord::Base ...@@ -200,6 +200,10 @@ class Member < ActiveRecord::Base
source_type source_type
end end
def access_field
access_level
end
def invite? def invite?
self.invite_token.present? self.invite_token.present?
end end
......
...@@ -25,10 +25,6 @@ class GroupMember < Member ...@@ -25,10 +25,6 @@ class GroupMember < Member
source source
end end
def access_field
access_level
end
# Because source_type is `Namespace`... # Because source_type is `Namespace`...
def real_source_type def real_source_type
'Group' 'Group'
......
...@@ -79,10 +79,6 @@ class ProjectMember < Member ...@@ -79,10 +79,6 @@ class ProjectMember < Member
end end
end end
def access_field
access_level
end
def project def project
source source
end end
......
...@@ -165,7 +165,7 @@ class Project < ActiveRecord::Base ...@@ -165,7 +165,7 @@ class Project < ActiveRecord::Base
has_many :todos, dependent: :destroy has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy, as: :source 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 :project_feature, dependent: :destroy
has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
has_many :container_repositories, dependent: :destroy has_many :container_repositories, dependent: :destroy
...@@ -298,8 +298,16 @@ class Project < ActiveRecord::Base ...@@ -298,8 +298,16 @@ class Project < ActiveRecord::Base
scope :excluding_project, ->(project) { where.not(id: project) } scope :excluding_project, ->(project) { where.not(id: project) }
state_machine :import_status, initial: :none do 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 event :import_start do
transition [:none, :finished] => :started transition scheduled: :started
end end
event :import_finish do event :import_finish do
...@@ -307,18 +315,23 @@ class Project < ActiveRecord::Base ...@@ -307,18 +315,23 @@ class Project < ActiveRecord::Base
end end
event :import_fail do event :import_fail do
transition started: :failed transition [:scheduled, :started] => :failed
end end
event :import_retry do event :import_retry do
transition failed: :started transition failed: :started
end end
state :scheduled
state :started state :started
state :finished state :finished
state :failed 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 end
class << self class << self
...@@ -532,9 +545,17 @@ class Project < ActiveRecord::Base ...@@ -532,9 +545,17 @@ class Project < ActiveRecord::Base
end end
def import_in_progress? def import_in_progress?
import_started? || import_scheduled?
end
def import_started?
import? && import_status == 'started' import? && import_status == 'started'
end end
def import_scheduled?
import_status == 'scheduled'
end
def import_failed? def import_failed?
import_status == 'failed' import_status == 'failed'
end end
......
...@@ -2,14 +2,7 @@ class ProtectedBranch < ActiveRecord::Base ...@@ -2,14 +2,7 @@ class ProtectedBranch < ActiveRecord::Base
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include ProtectedRef include ProtectedRef
has_many :merge_access_levels, dependent: :destroy protected_ref_access_levels :merge, :push
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
# Check if branch name is marked as protected in the system # Check if branch name is marked as protected in the system
def self.protected?(project, ref_name) def self.protected?(project, ref_name)
......
...@@ -2,11 +2,7 @@ class ProtectedTag < ActiveRecord::Base ...@@ -2,11 +2,7 @@ class ProtectedTag < ActiveRecord::Base
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include ProtectedRef include ProtectedRef
has_many :create_access_levels, dependent: :destroy protected_ref_access_levels :create
validates :create_access_levels, length: { is: 1, message: "are restricted to a single instance per protected tag." }
accepts_nested_attributes_for :create_access_levels
def self.protected?(project, ref_name) def self.protected?(project, ref_name)
self.matching(ref_name, protected_refs: project.protected_tags).present? self.matching(ref_name, protected_refs: project.protected_tags).present?
......
...@@ -4,8 +4,7 @@ class SpamLog < ActiveRecord::Base ...@@ -4,8 +4,7 @@ class SpamLog < ActiveRecord::Base
validates :user, presence: true validates :user, presence: true
def remove_user(deleted_by:) def remove_user(deleted_by:)
user.block user.delete_async(deleted_by: deleted_by, params: { hard_delete: true })
DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true, hard_delete: true)
end end
def text def text
......
...@@ -809,6 +809,11 @@ class User < ActiveRecord::Base ...@@ -809,6 +809,11 @@ class User < ActiveRecord::Base
system_hook_service.execute_hooks_for(self, :destroy) system_hook_service.execute_hooks_for(self, :destroy)
end end
def delete_async(deleted_by:, params: {})
block if params[:hard_delete]
DeleteUserWorker.perform_async(deleted_by.id, id, params)
end
def notification_service def notification_service
NotificationService.new NotificationService.new
end end
......
...@@ -4,22 +4,25 @@ class GroupPolicy < BasePolicy ...@@ -4,22 +4,25 @@ class GroupPolicy < BasePolicy
return unless @user return unless @user
globally_viewable = @subject.public? || (@subject.internal? && !@user.external?) globally_viewable = @subject.public? || (@subject.internal? && !@user.external?)
member = @subject.users_with_parents.include?(@user) access_level = @subject.max_member_access_for_user(@user)
owner = @user.admin? || @subject.has_owner?(@user) owner = access_level >= GroupMember::OWNER
master = owner || @subject.has_master?(@user) master = access_level >= GroupMember::MASTER
reporter = access_level >= GroupMember::REPORTER
can_read = false can_read = false
can_read ||= globally_viewable can_read ||= globally_viewable
can_read ||= member can_read ||= access_level >= GroupMember::GUEST
can_read ||= @user.admin?
can_read ||= GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any? can_read ||= GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
can! :read_group if can_read can! :read_group if can_read
if reporter
can! :admin_label
end
# Only group masters and group owners can create new projects # Only group masters and group owners can create new projects
if master if master
can! :create_projects can! :create_projects
can! :admin_milestones can! :admin_milestones
can! :admin_label
end end
# Only group owner and administrators can admin group # Only group owner and administrators can admin group
...@@ -31,7 +34,7 @@ class GroupPolicy < BasePolicy ...@@ -31,7 +34,7 @@ class GroupPolicy < BasePolicy
can! :create_subgroup if @user.can_create_group can! :create_subgroup if @user.can_create_group
end end
if globally_viewable && @subject.request_access_enabled && !member if globally_viewable && @subject.request_access_enabled && access_level == GroupMember::NO_ACCESS
can! :request_access can! :request_access
end end
end end
......
...@@ -48,15 +48,14 @@ module Projects ...@@ -48,15 +48,14 @@ module Projects
save_project_and_import_data(import_data) save_project_and_import_data(import_data)
@project.import_start if @project.import?
after_create_actions if @project.persisted? after_create_actions if @project.persisted?
if @project.errors.empty? if @project.errors.empty?
@project.add_import_job if @project.import? @project.import_schedule if @project.import?
else else
fail(error: @project.errors.full_messages.join(', ')) fail(error: @project.errors.full_messages.join(', '))
end end
@project @project
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
message = "Unable to save #{e.record.type}: #{e.record.errors.full_messages.join(", ")} " message = "Unable to save #{e.record.type}: #{e.record.errors.full_messages.join(", ")} "
......
...@@ -16,7 +16,7 @@ module Users ...@@ -16,7 +16,7 @@ module Users
def record_activity def record_activity
Gitlab::UserActivities.record(@author.id) 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 end
end end
...@@ -6,12 +6,27 @@ module Users ...@@ -6,12 +6,27 @@ module Users
@current_user = current_user @current_user = current_user
end 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 = {}) 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) unless Ability.allowed?(current_user, :destroy_user, user)
raise Gitlab::Access::AccessDeniedError, "#{current_user} tried to destroy user #{user}!" raise Gitlab::Access::AccessDeniedError, "#{current_user} tried to destroy user #{user}!"
end 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' user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user'
return user return user
end end
......
...@@ -34,9 +34,15 @@ ...@@ -34,9 +34,15 @@
- if user.access_locked? - if user.access_locked?
%li %li
= link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } = 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 %li.divider
- if user.can_be_removed?
%li %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?" }, = link_to 'Remove user', admin_user_path(user),
data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" },
method: :delete
%li
= 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', class: 'btn btn-remove btn-block',
method: :delete method: :delete
...@@ -177,7 +177,7 @@ ...@@ -177,7 +177,7 @@
%p Deleting a user has the following effects: %p Deleting a user has the following effects:
= render 'users/deletion_guidance', user: @user = render 'users/deletion_guidance', user: @user
%br %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 - else
- if @user.solo_owned_groups.present? - if @user.solo_owned_groups.present?
%p %p
...@@ -188,3 +188,22 @@ ...@@ -188,3 +188,22 @@
- else - else
%p %p
You don't have access to delete this user. 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 @@ ...@@ -42,7 +42,7 @@
.col-md-9 .col-md-9
.label-light .label-light
= label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level = 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 %span.help-block
.col-md-3.visibility-select-container .col-md-3.visibility-select-container
= render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level) = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
...@@ -92,14 +92,14 @@ ...@@ -92,14 +92,14 @@
.form-group .form-group
= render 'shared/allow_request_access', form: f = render 'shared/allow_request_access', form: f
- if Gitlab.config.lfs.enabled && current_user.admin? - if Gitlab.config.lfs.enabled && current_user.admin?
.row .row.js-lfs-enabled
.col-md-9 .col-md-9
= f.label :lfs_enabled, 'LFS', class: 'label-light' = f.label :lfs_enabled, 'LFS', class: 'label-light'
%span.help-block %span.help-block
Git Large File Storage Git Large File Storage
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
.col-md-3 .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 - 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.panel-default
.panel-heading .panel-heading
%h3.panel-title %h3.panel-title
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
= dropdown_tag('Select tag or create wildcard', = dropdown_tag('Select tag or create wildcard',
options: { toggle_class: 'js-protected-tag-select js-filter-submit wide git-revision-dropdown-toggle', 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, footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true, data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_tag_name], selected: params[:protected_tag_name],
...@@ -10,6 +10,6 @@ ...@@ -10,6 +10,6 @@
%ul.dropdown-footer-list %ul.dropdown-footer-list
%li %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 Create wildcard
%code %code
...@@ -4,13 +4,14 @@ ...@@ -4,13 +4,14 @@
.row.prepend-top-default.append-bottom-default .row.prepend-top-default.append-bottom-default
.col-lg-3 .col-lg-3
%h4.prepend-top-0 %h4.prepend-top-0
Protected tags Protected Tags
%p.prepend-top-20 %p.prepend-top-20
By default, Protected tags are designed to: By default, protected tags are designed to:
%ul %ul
%li Prevent tag creation by everybody except Masters %li Prevent tag creation by everybody except Masters
%li Prevent <strong>anyone</strong> from updating the tag %li Prevent <strong>anyone</strong> from updating the tag
%li Prevent <strong>anyone</strong> from deleting 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 .col-lg-9
- if can? current_user, :admin_project, @project - if can? current_user, :admin_project, @project
= render 'projects/protected_tags/create_protected_tag' = render 'projects/protected_tags/create_protected_tag'
......
...@@ -19,4 +19,4 @@ ...@@ -19,4 +19,4 @@
- if can_admin_project - if can_admin_project
%td %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? - if @protected_tags.empty?
.panel-heading .panel-heading
%h3.panel-title %h3.panel-title
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
%col{ width: "25%" } %col{ width: "25%" }
%col{ width: "25%" } %col{ width: "25%" }
%col{ width: "50%" } %col{ width: "50%" }
- if can_admin_project
%col
%thead %thead
%tr %tr
%th Protected tag (#{@protected_tags.size}) %th Protected tag (#{@protected_tags.size})
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
%h4.prepend-top-0.ref-name %h4.prepend-top-0.ref-name
= @protected_ref.name = @protected_ref.name
.col-lg-9 .col-lg-9.edit_protected_tag
%h5 Matching Tags %h5 Matching Tags
- if @matching_refs.present? - if @matching_refs.present?
.table-responsive .table-responsive
......
- noteable = @sent_notification.noteable - 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})) - noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.name_with_namespace
- page_title "Unsubscribe", noteable_text, @sent_notification.noteable_type.humanize.pluralize, @sent_notification.project.name_with_namespace
%h3.page-title %h3.page-title
Unsubscribe from #{noteable_type} #{noteable_text} Unsubscribe from #{noteable_type}
%p %p
= succeed '?' do = 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]) = link_to noteable_text, url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable])
%p %p
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit - 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 - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
- cache_key = project_list_cache_key(project) - 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 } %li.project-row{ class: css_class }
= cache(cache_key) do = cache(cache_key) do
......
class RepositoryForkWorker class RepositoryForkWorker
ForkError = Class.new(StandardError)
include Sidekiq::Worker include Sidekiq::Worker
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
...@@ -8,29 +10,31 @@ class RepositoryForkWorker ...@@ -8,29 +10,31 @@ class RepositoryForkWorker
source_path: source_path, source_path: source_path,
target_path: target_path) target_path: target_path)
project = Project.find_by_id(project_id) project = Project.find(project_id)
project.import_start
unless project.present?
logger.error("Project #{project_id} no longer exists!")
return
end
result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_path, result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_path,
project.repository_storage_path, target_path) project.repository_storage_path, target_path)
unless result raise ForkError, "Unable to fork project #{project_id} for repository #{source_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
project.repository.after_import project.repository.after_import
raise ForkError, "Project #{project_id} had an invalid repository after fork" unless project.valid_repo?
unless project.valid_repo? project.import_finish
logger.error("Project #{project_id} had an invalid repository after fork") rescue ForkError => ex
project.mark_import_as_failed('The forked repository is invalid.') fail_fork(project, ex.message)
return raise
rescue => ex
return unless project
fail_fork(project, ex.message)
raise ForkError, "#{ex.class} #{ex.message}"
end end
project.import_finish private
def fail_fork(project, message)
Rails.logger.error(message)
project.mark_import_as_failed(message)
end end
end end
class RepositoryImportWorker class RepositoryImportWorker
ImportError = Class.new(StandardError)
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
...@@ -10,6 +12,8 @@ class RepositoryImportWorker ...@@ -10,6 +12,8 @@ class RepositoryImportWorker
@project = Project.find(project_id) @project = Project.find(project_id)
@current_user = @project.creator @current_user = @project.creator
project.import_start
Gitlab::Metrics.add_event(:import_repository, Gitlab::Metrics.add_event(:import_repository,
import_url: @project.import_url, import_url: @project.import_url,
path: @project.path_with_namespace) path: @project.path_with_namespace)
...@@ -17,13 +21,23 @@ class RepositoryImportWorker ...@@ -17,13 +21,23 @@ class RepositoryImportWorker
project.update_columns(import_jid: self.jid, import_error: nil) project.update_columns(import_jid: self.jid, import_error: nil)
result = Projects::ImportService.new(project, current_user).execute result = Projects::ImportService.new(project, current_user).execute
raise ImportError, result[:message] if result[:status] == :error
if result[:status] == :error
project.mark_import_as_failed(result[:message])
return
end
project.repository.after_import project.repository.after_import
project.import_finish 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
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 # 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. 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 ```yaml
codeclimate: codeclimate:
...@@ -25,4 +25,10 @@ codeclimate: ...@@ -25,4 +25,10 @@ codeclimate:
This will create a `codeclimate` job in your CI pipeline and will allow you to This will create a `codeclimate` job in your CI pipeline and will allow you to
download and analyze the report artifact in JSON format. 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 [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. ...@@ -207,7 +207,9 @@ its class in an annotation.
>**Note:** >**Note:**
The Ingress alone doesn't expose GitLab externally. You need to have a Ingress controller setup to do that. 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 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 #### Preserving Source IPs
......
...@@ -141,7 +141,7 @@ Once you [have configured](#configuration) GitLab Runner in your `values.yml` fi ...@@ -141,7 +141,7 @@ Once you [have configured](#configuration) GitLab Runner in your `values.yml` fi
run the following: run the following:
```bash ```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. - `<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> ...@@ -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` Once your GitLab Runner Chart is installed, configuration changes and chart updates should we done using `helm upgrade`
```bash ```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: Where:
......
...@@ -71,6 +71,85 @@ sudo gitlab-rake gitlab:two_factor:disable_for_all_users ...@@ -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 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. Important! Data loss!
Clear authentication tokens for all users in the GitLab database. This Clear authentication tokens for all users in the GitLab database. This
......
...@@ -126,7 +126,7 @@ which visibility level you select on project settings. ...@@ -126,7 +126,7 @@ which visibility level you select on project settings.
## GitLab CI ## GitLab CI
GitLab CI permissions rely on the role the user has in GitLab. There are four GitLab CI permissions rely on the role the user has in GitLab. There are four
permission levels it total: permission levels in total:
- admin - admin
- master - master
......
...@@ -25,7 +25,8 @@ Instead of being deleted, these records will be moved to a system-wide ...@@ -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 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 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 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-7393]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7393
[ce-10273]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10273 [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 The GitLab Issue Tracker is an advanced and complete tool
for tracking the evolution of a new idea or the process for tracking the evolution of a new idea or the process
...@@ -41,13 +41,13 @@ The image bellow illustrates how an issue looks like: ...@@ -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). 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). Read through the [documentation on creating issues](create_new_issue.md).
## Closing issues ## 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 ## Create a merge request from an issue
...@@ -84,7 +84,7 @@ Learn more about them on the [issue templates documentation](../../project/descr ...@@ -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. 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 The [GitLab Issue Board](https://about.gitlab.com/features/issueboard/) is a way to
enhance your workflow by organizing and prioritizing issues in GitLab. enhance your workflow by organizing and prioritizing issues in GitLab.
......
...@@ -42,6 +42,22 @@ module API ...@@ -42,6 +42,22 @@ module API
@project, @wiki = Gitlab::RepoPath.parse(params[:project]) @project, @wiki = Gitlab::RepoPath.parse(params[:project])
end end
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 end
end end
...@@ -32,31 +32,23 @@ module API ...@@ -32,31 +32,23 @@ module API
actor.update_last_used_at if actor.is_a?(Key) actor.update_last_used_at if actor.is_a?(Key)
access_checker = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess access_checker_klass = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
access_status = access_checker access_checker = access_checker_klass
.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities) .new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
.check(params[:action], params[:changes])
response = { status: access_status.status, message: access_status.message } begin
access_checker.check(params[:action], params[:changes])
rescue Gitlab::GitAccess::UnauthorizedError, Gitlab::GitAccess::NotFoundError => e
return { status: false, message: e.message }
end
if access_status.status
log_user_activity(actor) log_user_activity(actor)
# Project id to pass between components that don't share/don't have {
# access to the same filesystem mounts status: true,
response[:gl_repository] = Gitlab::GlRepository.gl_repository(project, wiki?) gl_repository: gl_repository,
repository_path: repository_path
# 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
end
response
end end
post "/lfs_authenticate" do post "/lfs_authenticate" do
......
...@@ -293,7 +293,7 @@ module API ...@@ -293,7 +293,7 @@ module API
user = User.find_by(id: params[:id]) user = User.find_by(id: params[:id])
not_found!('User') unless user 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 end
desc 'Block a user. Available only for admins.' desc 'Block a user. Available only for admins.'
......
module Gitlab module Gitlab
module Checks module Checks
class ChangeAccess 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 attr_reader :user_access, :project, :skip_authorization, :protocol
def initialize( def initialize(
...@@ -17,22 +31,20 @@ module Gitlab ...@@ -17,22 +31,20 @@ module Gitlab
end end
def exec 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 true
GitAccessStatus.new(false, error)
else
GitAccessStatus.new(true)
end
end end
protected protected
def push_checks def push_checks
if user_access.cannot_do_action?(:push_code) 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
end end
...@@ -40,7 +52,7 @@ module Gitlab ...@@ -40,7 +52,7 @@ module Gitlab
return unless @branch_name return unless @branch_name
if deletion? && @branch_name == project.default_branch 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 end
protected_branch_checks protected_branch_checks
...@@ -50,7 +62,7 @@ module Gitlab ...@@ -50,7 +62,7 @@ module Gitlab
return unless ProtectedBranch.protected?(project, @branch_name) return unless ProtectedBranch.protected?(project, @branch_name)
if forced_push? 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 end
if deletion? if deletion?
...@@ -62,22 +74,22 @@ module Gitlab ...@@ -62,22 +74,22 @@ module Gitlab
def protected_branch_deletion_checks def protected_branch_deletion_checks
unless user_access.can_delete_branch?(@branch_name) 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 end
unless protocol == 'web' 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
end end
def protected_branch_push_checks def protected_branch_push_checks
if matching_merge_request? if matching_merge_request?
unless user_access.can_merge_to_branch?(@branch_name) || user_access.can_push_to_branch?(@branch_name) 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 end
else else
unless user_access.can_push_to_branch?(@branch_name) 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 end
end end
...@@ -86,7 +98,7 @@ module Gitlab ...@@ -86,7 +98,7 @@ module Gitlab
return unless @tag_name return unless @tag_name
if tag_exists? && user_access.cannot_do_action?(:admin_project) 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 end
protected_tag_checks protected_tag_checks
...@@ -95,11 +107,11 @@ module Gitlab ...@@ -95,11 +107,11 @@ module Gitlab
def protected_tag_checks def protected_tag_checks
return unless ProtectedTag.protected?(project, @tag_name) return unless ProtectedTag.protected?(project, @tag_name)
return "Protected tags cannot be updated." if update? raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update?
return "Protected tags cannot be deleted." if deletion? raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion?
unless user_access.can_create_tag?(@tag_name) 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
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 @@ ...@@ -3,33 +3,39 @@
module Gitlab module Gitlab
class GitAccess class GitAccess
UnauthorizedError = Class.new(StandardError) UnauthorizedError = Class.new(StandardError)
NotFoundError = Class.new(StandardError)
ERROR_MESSAGES = { ERROR_MESSAGES = {
upload: 'You are not allowed to upload code for this project.', upload: 'You are not allowed to upload code for this project.',
download: 'You are not allowed to download code from this project.', download: 'You are not allowed to download code from this project.',
deploy_key_upload: deploy_key_upload:
'This deploy key does not have write access to this project.', '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 }.freeze
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze
PUSH_COMMANDS = %w{ git-receive-pack }.freeze PUSH_COMMANDS = %w{ git-receive-pack }.freeze
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS 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:) def initialize(actor, project, protocol, authentication_abilities:)
@actor = actor @actor = actor
@project = project @project = project
@protocol = protocol @protocol = protocol
@authentication_abilities = authentication_abilities @authentication_abilities = authentication_abilities
@user_access = UserAccess.new(user, project: project)
end end
def check(cmd, changes) def check(cmd, changes)
check_protocol! check_protocol!
check_active_user! check_active_user!
check_project_accessibility! check_project_accessibility!
check_command_disabled!(cmd)
check_command_existence!(cmd) check_command_existence!(cmd)
check_repository_existence! check_repository_existence!
...@@ -40,9 +46,7 @@ module Gitlab ...@@ -40,9 +46,7 @@ module Gitlab
check_push_access!(changes) check_push_access!(changes)
end end
build_status_object(true) true
rescue UnauthorizedError => ex
build_status_object(false, ex.message)
end end
def guest_can_download_code? def guest_can_download_code?
...@@ -73,19 +77,39 @@ module Gitlab ...@@ -73,19 +77,39 @@ module Gitlab
return if deploy_key? return if deploy_key?
if user && !user_access.allowed? if user && !user_access.allowed?
raise UnauthorizedError, "Your account has been blocked." raise UnauthorizedError, ERROR_MESSAGES[:account_blocked]
end end
end end
def check_project_accessibility! def check_project_accessibility!
if project.blank? || !can_read_project? 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
end end
def check_command_existence!(cmd) def check_command_existence!(cmd)
unless ALL_COMMANDS.include?(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
end end
...@@ -138,11 +162,9 @@ module Gitlab ...@@ -138,11 +162,9 @@ module Gitlab
# Iterate over all changes to find if user allowed all of them to be applied # Iterate over all changes to find if user allowed all of them to be applied
changes_list.each do |change| changes_list.each do |change|
status = check_single_change_access(change) # If user does not have access to make at least one change, cancel all
unless status.allowed? # push by allowing the exception to bubble up
# If user does not have access to make at least one change - cancel all push check_single_change_access(change)
raise UnauthorizedError, status.message
end
end end
end end
...@@ -168,14 +190,40 @@ module Gitlab ...@@ -168,14 +190,40 @@ module Gitlab
actor.is_a?(DeployKey) actor.is_a?(DeployKey)
end end
def ci?
actor == :ci
end
def can_read_project? def can_read_project?
if deploy_key if deploy_key?
deploy_key.has_access_to?(project) deploy_key.has_access_to?(project)
elsif user elsif user
user.can?(:read_project, project) user.can?(:read_project, project)
elsif ci?
true # allow CI (build without a user) for backwards compatibility
end || Guest.can?(:read_project, project) end || Guest.can?(:read_project, project)
end 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 protected
def user def user
...@@ -185,15 +233,19 @@ module Gitlab ...@@ -185,15 +233,19 @@ module Gitlab
case actor case actor
when User when User
actor actor
when DeployKey
nil
when Key when Key
actor.user actor.user unless actor.is_a?(DeployKey)
when :ci
nil
end end
end end
def build_status_object(status, message = '') def user_access
Gitlab::GitAccessStatus.new(status, message) @user_access ||= if ci?
CiAccess.new
else
UserAccess.new(user, project: project)
end
end end
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 module Gitlab
class GitAccessWiki < GitAccess 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? def guest_can_download_code?
Guest.can?(:download_wiki_code, project) Guest.can?(:download_wiki_code, project)
end end
...@@ -9,11 +13,11 @@ module Gitlab ...@@ -9,11 +13,11 @@ module Gitlab
end end
def check_single_change_access(change) def check_single_change_access(change)
if user_access.can_do_action?(:create_wiki) unless user_access.can_do_action?(:create_wiki)
build_status_object(true) raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki]
else
build_status_object(false, "You are not allowed to write to this project's wiki.")
end end
true
end end
end end
end end
...@@ -5,7 +5,10 @@ module Gitlab ...@@ -5,7 +5,10 @@ module Gitlab
AVAILABLE_LANGUAGES = { AVAILABLE_LANGUAGES = {
'en' => 'English', 'en' => 'English',
'es' => 'Español', 'es' => 'Español',
'de' => 'Deutsch' 'de' => 'Deutsch',
'zh_CN' => '简体中文',
'zh_HK' => '繁體中文(香港)',
'zh_TW' => '繁體中文(臺灣)'
}.freeze }.freeze
def available_locales 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 ...@@ -19,5 +19,21 @@ namespace :gitlab do
puts "There are currently no users with 2FA enabled.".color(:yellow) puts "There are currently no users with 2FA enabled.".color(:yellow)
end end
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
end end
...@@ -37,7 +37,7 @@ class GithubImport ...@@ -37,7 +37,7 @@ class GithubImport
end end
def import! def import!
@project.import_start @project.force_import_start
timings = Benchmark.measure do timings = Benchmark.measure do
Github::Import.new(@project, @options).execute 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 ...@@ -10,15 +10,26 @@ describe Admin::UsersController do
describe 'DELETE #user with projects' do describe 'DELETE #user with projects' do
let(:project) { create(:empty_project, namespace: user.namespace) } let(:project) { create(:empty_project, namespace: user.namespace) }
let!(:issue) { create(:issue, author: user) }
before do before do
project.team << [user, :developer] project.team << [user, :developer]
end end
it 'deletes user' do it 'deletes user and ghosts their contributions' do
delete :destroy, id: user.username, format: :json 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(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
end end
......
...@@ -77,7 +77,7 @@ describe RegistrationsController do ...@@ -77,7 +77,7 @@ describe RegistrationsController do
end end
it 'schedules the user for destruction' do 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) post(:destroy)
......
...@@ -7,5 +7,9 @@ FactoryGirl.define do ...@@ -7,5 +7,9 @@ FactoryGirl.define do
link.forked_from_project.reload link.forked_from_project.reload
link.forked_to_project.reload link.forked_to_project.reload
end end
trait :forked_to_empty_project do
association :forked_to_project, factory: :empty_project
end
end end
end end
...@@ -26,6 +26,22 @@ FactoryGirl.define do ...@@ -26,6 +26,22 @@ FactoryGirl.define do
visibility_level Gitlab::VisibilityLevel::PRIVATE visibility_level Gitlab::VisibilityLevel::PRIVATE
end 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 trait :archived do
archived true archived true
end end
......
...@@ -22,7 +22,8 @@ describe "Admin::Users", feature: true do ...@@ -22,7 +22,8 @@ describe "Admin::Users", feature: true do
expect(page).to have_content(user.email) expect(page).to have_content(user.email)
expect(page).to have_content(user.name) expect(page).to have_content(user.name)
expect(page).to have_link('Block', href: block_admin_user_path(user)) 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 end
describe 'Two-factor Authentication filters' do describe 'Two-factor Authentication filters' do
...@@ -116,6 +117,9 @@ describe "Admin::Users", feature: true do ...@@ -116,6 +117,9 @@ describe "Admin::Users", feature: true do
expect(page).to have_content(user.email) expect(page).to have_content(user.email)
expect(page).to have_content(user.name) 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 end
describe 'Impersonation' do describe 'Impersonation' do
......
...@@ -15,6 +15,15 @@ RSpec.describe 'Dashboard Projects', feature: true do ...@@ -15,6 +15,15 @@ RSpec.describe 'Dashboard Projects', feature: true do
expect(page).to have_content('awesome stuff') expect(page).to have_content('awesome stuff')
end 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 context 'when on Starred projects tab' do
it 'shows only starred projects' do it 'shows only starred projects' do
user.toggle_star(project2) user.toggle_star(project2)
......
...@@ -68,9 +68,14 @@ feature 'Diffs URL', js: true, feature: true do ...@@ -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(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, target_project: project, author: author_user) }
let(:changelog_id) { Digest::SHA1.hexdigest("CHANGELOG") } let(:changelog_id) { Digest::SHA1.hexdigest("CHANGELOG") }
before do
forked_project.repository.after_import
end
context 'as author' do context 'as author' do
it 'shows direct edit link' do it 'shows direct edit link' do
login_as(author_user) login_as(author_user)
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
# Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax # Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
...@@ -81,6 +86,7 @@ feature 'Diffs URL', js: true, feature: true do ...@@ -81,6 +86,7 @@ feature 'Diffs URL', js: true, feature: true do
context 'as user who needs to fork' do context 'as user who needs to fork' do
it 'shows fork/cancel confirmation' do it 'shows fork/cancel confirmation' do
login_as(user) login_as(user)
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
# Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax # Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
......
...@@ -14,7 +14,7 @@ feature 'Visibility settings', feature: true, js: true do ...@@ -14,7 +14,7 @@ feature 'Visibility settings', feature: true, js: true do
visibility_select_container = find('.js-visibility-select') 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.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 end
scenario 'project visibility description updates on change' do scenario 'project visibility description updates on change' do
...@@ -41,7 +41,7 @@ feature 'Visibility settings', feature: true, js: true 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).not_to have_select '.visibility-select'
expect(visibility_select_container).to have_content 'Public' 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 end
end end
...@@ -24,8 +24,8 @@ describe 'Unsubscribe links', feature: true do ...@@ -24,8 +24,8 @@ describe 'Unsubscribe links', feature: true do
visit body_link visit body_link
expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last) 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(%(Unsubscribe from issue))
expect(page).to have_text(%(Are you sure you want to unsubscribe from issue #{issue.title} (#{issue.to_reference})?)) 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 expect(issue.subscribed?(recipient, project)).to be_truthy
click_link 'Unsubscribe' click_link 'Unsubscribe'
......
...@@ -257,7 +257,7 @@ describe ProjectsHelper do ...@@ -257,7 +257,7 @@ describe ProjectsHelper do
result = helper.project_feature_access_select(:issues_access_level) result = helper.project_feature_access_select(:issues_access_level)
expect(result).to include("Disabled") expect(result).to include("Disabled")
expect(result).to include("Only team members") 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
end end
...@@ -272,7 +272,7 @@ describe ProjectsHelper do ...@@ -272,7 +272,7 @@ describe ProjectsHelper do
expect(result).to include("Disabled") expect(result).to include("Disabled")
expect(result).to include("Only team members") 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") expect(result).to have_selector('option[selected]', text: "Only team members")
end end
end end
......
...@@ -52,6 +52,14 @@ describe SubmoduleHelper do ...@@ -52,6 +52,14 @@ describe SubmoduleHelper do
stub_url(['http://', config.host, '/gitlab/root/gitlab-org/gitlab-ce.git'].join('')) 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')]) expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
end 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 end
context 'submodule on github.com' do context 'submodule on github.com' do
......
...@@ -37,7 +37,7 @@ describe VisibilityLevelHelper do ...@@ -37,7 +37,7 @@ describe VisibilityLevelHelper do
it "describes public projects" do it "describes public projects" do
expect(project_visibility_level_description(Gitlab::VisibilityLevel::PUBLIC)) 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
end end
......
...@@ -10,6 +10,9 @@ describe('Pipeline details header', () => { ...@@ -10,6 +10,9 @@ describe('Pipeline details header', () => {
beforeEach(() => { beforeEach(() => {
HeaderComponent = Vue.extend(headerComponent); HeaderComponent = Vue.extend(headerComponent);
const threeWeeksAgo = new Date();
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
props = { props = {
pipeline: { pipeline: {
details: { details: {
...@@ -22,7 +25,7 @@ describe('Pipeline details header', () => { ...@@ -22,7 +25,7 @@ describe('Pipeline details header', () => {
}, },
}, },
id: 123, id: 123,
created_at: '2017-05-08T14:57:39.781Z', created_at: threeWeeksAgo.toISOString(),
user: { user: {
web_url: 'path', web_url: 'path',
name: 'Foo', name: 'Foo',
......
...@@ -23,29 +23,27 @@ describe Gitlab::Checks::ChangeAccess, lib: true do ...@@ -23,29 +23,27 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
before { project.add_developer(user) } before { project.add_developer(user) }
context 'without failed checks' do context 'without failed checks' do
it "doesn't return any error" do it "doesn't raise an error" do
expect(subject.status).to be(true) expect { subject }.not_to raise_error
end end
end end
context 'when the user is not allowed to push code' do 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(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
expect(subject.status).to be(false) expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.')
expect(subject.message).to eq('You are not allowed to push code to this project.')
end end
end end
context 'tags check' do context 'tags check' do
let(:ref) { 'refs/tags/v1.0.0' } 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) 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(user_access).to receive(:can_do_action?).with(:admin_project).and_return(false)
expect(subject.status).to be(false) expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to change existing tags on this project.')
expect(subject.message).to eq('You are not allowed to change existing tags on this project.')
end end
context 'with protected tag' do context 'with protected tag' do
...@@ -59,8 +57,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do ...@@ -59,8 +57,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
let(:newrev) { '0000000000000000000000000000000000000000' } let(:newrev) { '0000000000000000000000000000000000000000' }
it 'is prevented' do it 'is prevented' do
expect(subject.status).to be(false) expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be deleted/)
expect(subject.message).to include('cannot be deleted')
end end
end end
...@@ -69,8 +66,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do ...@@ -69,8 +66,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
it 'is prevented' do it 'is prevented' do
expect(subject.status).to be(false) expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be updated/)
expect(subject.message).to include('cannot be updated')
end end
end end
end end
...@@ -81,15 +77,14 @@ describe Gitlab::Checks::ChangeAccess, lib: true do ...@@ -81,15 +77,14 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
let(:ref) { 'refs/tags/v9.1.0' } let(:ref) { 'refs/tags/v9.1.0' }
it 'prevents creation below access level' do it 'prevents creation below access level' do
expect(subject.status).to be(false) expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /allowed to create this tag as it is protected/)
expect(subject.message).to include('allowed to create this tag as it is protected')
end end
context 'when user has access' do context 'when user has access' do
let!(:protected_tag) { create(:protected_tag, :developers_can_create, project: project, name: 'v*') } let!(:protected_tag) { create(:protected_tag, :developers_can_create, project: project, name: 'v*') }
it 'allows tag creation' do it 'allows tag creation' do
expect(subject.status).to be(true) expect { subject }.not_to raise_error
end end
end end
end end
...@@ -101,9 +96,8 @@ describe Gitlab::Checks::ChangeAccess, lib: true do ...@@ -101,9 +96,8 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
let(:newrev) { '0000000000000000000000000000000000000000' } let(:newrev) { '0000000000000000000000000000000000000000' }
let(:ref) { 'refs/heads/master' } let(:ref) { 'refs/heads/master' }
it 'returns an error' do it 'raises an error' do
expect(subject.status).to be(false) expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'The default branch of a project cannot be deleted.')
expect(subject.message).to eq('The default branch of a project cannot be deleted.')
end end
end end
...@@ -113,27 +107,24 @@ describe Gitlab::Checks::ChangeAccess, lib: true do ...@@ -113,27 +107,24 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
allow(ProtectedBranch).to receive(:protected?).with(project, 'feature').and_return(true) allow(ProtectedBranch).to receive(:protected?).with(project, 'feature').and_return(true)
end 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(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true)
expect(subject.status).to be(false) expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to force push code to a protected branch on this project.')
expect(subject.message).to eq('You are not allowed to force push code to a protected branch on this project.')
end 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_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_merge_to_branch?).and_return(false)
expect(user_access).to receive(:can_push_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 }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to merge code into protected branches on this project.')
expect(subject.message).to eq('You are not allowed to merge code into protected branches on this project.')
end 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(user_access).to receive(:can_push_to_branch?).and_return(false)
expect(subject.status).to be(false) expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to protected branches on this project.')
expect(subject.message).to eq('You are not allowed to push code to protected branches on this project.')
end end
context 'branch deletion' do context 'branch deletion' do
...@@ -141,9 +132,8 @@ describe Gitlab::Checks::ChangeAccess, lib: true do ...@@ -141,9 +132,8 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
let(:ref) { 'refs/heads/feature' } let(:ref) { 'refs/heads/feature' }
context 'if the user is not allowed to delete protected branches' do context 'if the user is not allowed to delete protected branches' do
it 'returns an error' do it 'raises an error' do
expect(subject.status).to be(false) 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.')
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.')
end end
end end
...@@ -156,14 +146,13 @@ describe Gitlab::Checks::ChangeAccess, lib: true do ...@@ -156,14 +146,13 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
let(:protocol) { 'web' } let(:protocol) { 'web' }
it 'allows branch deletion' do it 'allows branch deletion' do
expect(subject.status).to be(true) expect { subject }.not_to raise_error
end end
end end
context 'over SSH or HTTP' do context 'over SSH or HTTP' do
it 'returns an error' do it 'raises an error' do
expect(subject.status).to be(false) expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only delete protected branches using the web interface.')
expect(subject.message).to eq('You can only delete protected branches using the web interface.')
end end
end 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' require 'spec_helper'
describe Gitlab::GitAccess, lib: true do 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(:project) { create(:project, :repository) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:actor) { user } let(:actor) { user }
let(:protocol) { 'ssh' }
let(:authentication_abilities) do let(:authentication_abilities) do
[ [
:read_project, :read_project,
...@@ -15,49 +18,188 @@ describe Gitlab::GitAccess, lib: true do ...@@ -15,49 +18,188 @@ describe Gitlab::GitAccess, lib: true do
describe '#check with single protocols allowed' do describe '#check with single protocols allowed' do
def disable_protocol(protocol) def disable_protocol(protocol)
settings = ::ApplicationSetting.create_from_defaults allow(Gitlab::ProtocolAccess).to receive(:allowed?).with(protocol).and_return(false)
settings.update_attribute(:enabled_git_access_protocol, protocol)
end end
context 'ssh disabled' do context 'ssh disabled' do
before do before do
disable_protocol('ssh') disable_protocol('ssh')
@acc = Gitlab::GitAccess.new(actor, project, 'ssh', authentication_abilities: authentication_abilities)
end end
it 'blocks ssh git push' do 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 end
it 'blocks ssh git pull' do 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
end end
context 'http disabled' do context 'http disabled' do
let(:protocol) { 'http' }
before do before do
disable_protocol('http') disable_protocol('http')
@acc = Gitlab::GitAccess.new(actor, project, 'http', authentication_abilities: authentication_abilities)
end end
it 'blocks http push' do 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 end
it 'blocks http git pull' do 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 end
end end
describe '#check_download_access!' do describe '#check_project_accessibility!' do
subject { access.check('git-upload-pack', '_any') } 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 describe 'master permissions' do
before { project.team << [user, :master] } before { project.team << [user, :master] }
context 'pull code' do context 'pull code' do
it { expect(subject.allowed?).to be_truthy } it { expect { pull_access_check }.not_to raise_error }
end end
end end
...@@ -65,8 +207,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -65,8 +207,7 @@ describe Gitlab::GitAccess, lib: true do
before { project.team << [user, :guest] } before { project.team << [user, :guest] }
context 'pull code' do context 'pull code' do
it { expect(subject.allowed?).to be_falsey } it { expect { pull_access_check }.to raise_unauthorized('You are not allowed to download code from this project.') }
it { expect(subject.message).to match(/You are not allowed to download code/) }
end end
end end
...@@ -77,24 +218,22 @@ describe Gitlab::GitAccess, lib: true do ...@@ -77,24 +218,22 @@ describe Gitlab::GitAccess, lib: true do
end end
context 'pull code' do context 'pull code' do
it { expect(subject.allowed?).to be_falsey } it { expect { pull_access_check }.to raise_unauthorized('Your account has been blocked.') }
it { expect(subject.message).to match(/Your account has been blocked/) }
end end
end end
describe 'without access to project' do describe 'without access to project' do
context 'pull code' 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 end
context 'when project is public' do context 'when project is public' do
let(:public_project) { create(:project, :public, :repository) } let(:public_project) { create(:project, :public, :repository) }
let(:guest_access) { Gitlab::GitAccess.new(nil, public_project, 'web', authentication_abilities: []) } let(:access) { Gitlab::GitAccess.new(nil, public_project, 'web', authentication_abilities: []) }
subject { guest_access.check('git-upload-pack', '_any') }
context 'when repository is enabled' do context 'when repository is enabled' do
it 'give access to download code' do it 'give access to download code' do
expect(subject.allowed?).to be_truthy expect { pull_access_check }.not_to raise_error
end end
end end
...@@ -102,8 +241,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -102,8 +241,7 @@ describe Gitlab::GitAccess, lib: true do
it 'does not give access to download code' do it 'does not give access to download code' do
public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
expect(subject.allowed?).to be_falsey expect { pull_access_check }.to raise_unauthorized('You are not allowed to download code from this project.')
expect(subject.message).to match(/You are not allowed to download code/)
end end
end end
end end
...@@ -117,26 +255,26 @@ describe Gitlab::GitAccess, lib: true do ...@@ -117,26 +255,26 @@ describe Gitlab::GitAccess, lib: true do
context 'when project is authorized' do context 'when project is authorized' do
before { key.projects << project } before { key.projects << project }
it { expect(subject).to be_allowed } it { expect { pull_access_check }.not_to raise_error }
end end
context 'when unauthorized' do context 'when unauthorized' do
context 'from public project' do context 'from public project' do
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
it { expect(subject).to be_allowed } it { expect { pull_access_check }.not_to raise_error }
end end
context 'from internal project' do context 'from internal project' do
let(:project) { create(:project, :internal, :repository) } 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 end
context 'from private project' do context 'from private project' do
let(:project) { create(:project, :private, :repository) } 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 end
end end
...@@ -149,7 +287,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -149,7 +287,7 @@ describe Gitlab::GitAccess, lib: true do
let(:project) { create(:project, :repository, namespace: user.namespace) } let(:project) { create(:project, :repository, namespace: user.namespace) }
context 'pull code' do context 'pull code' do
it { expect(subject).to be_allowed } it { expect { pull_access_check }.not_to raise_error }
end end
end end
...@@ -157,7 +295,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -157,7 +295,7 @@ describe Gitlab::GitAccess, lib: true do
before { project.team << [user, :reporter] } before { project.team << [user, :reporter] }
context 'pull code' do context 'pull code' do
it { expect(subject).to be_allowed } it { expect { pull_access_check }.not_to raise_error }
end end
end end
...@@ -168,16 +306,24 @@ describe Gitlab::GitAccess, lib: true do ...@@ -168,16 +306,24 @@ describe Gitlab::GitAccess, lib: true do
before { project.team << [user, :reporter] } before { project.team << [user, :reporter] }
context 'pull code' do context 'pull code' do
it { expect(subject).to be_allowed } it { expect { pull_access_check }.not_to raise_error }
end end
end end
context 'when is not member of the project' do context 'when is not member of the project' do
context 'pull code' 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 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
end end
...@@ -365,42 +511,32 @@ describe Gitlab::GitAccess, lib: true do ...@@ -365,42 +511,32 @@ describe Gitlab::GitAccess, lib: true do
end end
end end
shared_examples 'pushing code' do |can| describe 'build authentication abilities' do
subject { access.check('git-receive-pack', '_any') } let(:authentication_abilities) { build_authentication_abilities }
context 'when project is authorized' do 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 end
context 'when unauthorized' do context 'when unauthorized' do
context 'to public project' do context 'to public project' do
let(:project) { create(:project, :public, :repository) } 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 end
context 'to internal project' do context 'to internal project' do
let(:project) { create(:project, :internal, :repository) } 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 end
context 'to private project' do context 'to private project' do
let(:project) { create(:project, :private, :repository) } let(:project) { create(:project, :private, :repository) }
it { expect(subject).not_to be_allowed } it { expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.') }
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]
end end
end end
end end
...@@ -412,9 +548,29 @@ describe Gitlab::GitAccess, lib: true do ...@@ -412,9 +548,29 @@ describe Gitlab::GitAccess, lib: true do
context 'when deploy_key can push' do context 'when deploy_key can push' do
let(:can_push) { true } let(:can_push) { true }
it_behaves_like 'pushing code', :to do context 'when project is authorized' do
def authorize before { key.projects << project }
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 end
end end
...@@ -422,9 +578,29 @@ describe Gitlab::GitAccess, lib: true do ...@@ -422,9 +578,29 @@ describe Gitlab::GitAccess, lib: true do
context 'when deploy_key cannot push' do context 'when deploy_key cannot push' do
let(:can_push) { false } let(:can_push) { false }
it_behaves_like 'pushing code', :not_to do context 'when project is authorized' do
def authorize before { key.projects << project }
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 end
end end
...@@ -432,6 +608,14 @@ describe Gitlab::GitAccess, lib: true do ...@@ -432,6 +608,14 @@ describe Gitlab::GitAccess, lib: true do
private 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 def build_authentication_abilities
[ [
:read_project, :read_project,
......
...@@ -20,7 +20,7 @@ describe Gitlab::GitAccessWiki, lib: true do ...@@ -20,7 +20,7 @@ describe Gitlab::GitAccessWiki, lib: true do
subject { access.check('git-receive-pack', changes) } subject { access.check('git-receive-pack', changes) }
it { expect(subject.allowed?).to be_truthy } it { expect { subject }.not_to raise_error }
end end
def changes def changes
...@@ -36,7 +36,7 @@ describe Gitlab::GitAccessWiki, lib: true do ...@@ -36,7 +36,7 @@ describe Gitlab::GitAccessWiki, lib: true do
context 'when wiki feature is enabled' do context 'when wiki feature is enabled' do
it 'give access to download wiki code' do it 'give access to download wiki code' do
expect(subject.allowed?).to be_truthy expect { subject }.not_to raise_error
end end
end end
...@@ -44,8 +44,7 @@ describe Gitlab::GitAccessWiki, lib: true do ...@@ -44,8 +44,7 @@ describe Gitlab::GitAccessWiki, lib: true do
it 'does not give access to download wiki code' do it 'does not give access to download wiki code' do
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
expect(subject.allowed?).to be_falsey expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to download code from this project.')
expect(subject.message).to match(/You are not allowed to download code/)
end end
end end
end end
......
...@@ -144,7 +144,9 @@ merge_access_levels: ...@@ -144,7 +144,9 @@ merge_access_levels:
push_access_levels: push_access_levels:
- protected_branch - protected_branch
create_access_levels: create_access_levels:
- user
- protected_tag - protected_tag
- group
container_repositories: container_repositories:
- project - project
- name - 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 ...@@ -28,9 +28,7 @@ RSpec.describe AbuseReport, type: :model do
end end
it 'lets a worker delete the user' do it 'lets a worker delete the user' do
expect(DeleteUserWorker).to receive(:perform_async).with(user.id, subject.user.id, expect(DeleteUserWorker).to receive(:perform_async).with(user.id, subject.user.id, hard_delete: true)
delete_solo_owned_groups: true,
hard_delete: true)
subject.remove_user(deleted_by: user) subject.remove_user(deleted_by: user)
end end
......
...@@ -50,7 +50,7 @@ describe Project, models: true do ...@@ -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(:external_wiki_service).dependent(:destroy) }
it { is_expected.to have_one(:project_feature).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(: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(:last_event).class_name('Event') }
it { is_expected.to have_one(:forked_from_project).through(:forked_project_link) } it { is_expected.to have_one(:forked_from_project).through(:forked_project_link) }
it { is_expected.to have_many(:commit_statuses) } it { is_expected.to have_many(:commit_statuses) }
...@@ -1446,16 +1446,13 @@ describe Project, models: true do ...@@ -1446,16 +1446,13 @@ describe Project, models: true do
end end
describe 'Project import job' do describe 'Project import job' do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project, import_url: generate(:url)) }
let(:mirror) { false }
before do before do
allow_any_instance_of(Gitlab::Shell).to receive(:import_repository) allow_any_instance_of(Gitlab::Shell).to receive(:import_repository)
.with(project.repository_storage_path, project.path_with_namespace, project.import_url) .with(project.repository_storage_path, project.path_with_namespace, project.import_url)
.and_return(true) .and_return(true)
allow(project).to receive(:repository_exists?).and_return(true)
expect_any_instance_of(Repository).to receive(:after_import) expect_any_instance_of(Repository).to receive(:after_import)
.and_call_original .and_call_original
end end
...@@ -1463,8 +1460,7 @@ describe Project, models: true do ...@@ -1463,8 +1460,7 @@ describe Project, models: true do
it 'imports a project' do it 'imports a project' do
expect_any_instance_of(RepositoryImportWorker).to receive(:perform).and_call_original expect_any_instance_of(RepositoryImportWorker).to receive(:perform).and_call_original
project.import_start project.import_schedule
project.add_import_job
expect(project.reload.import_status).to eq('finished') expect(project.reload.import_status).to eq('finished')
end end
...@@ -1551,7 +1547,7 @@ describe Project, models: true do ...@@ -1551,7 +1547,7 @@ describe Project, models: true do
describe '#add_import_job' do describe '#add_import_job' do
context 'forked' 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(:forked_from_project) { forked_project_link.forked_from_project }
let(:project) { forked_project_link.forked_to_project } let(:project) { forked_project_link.forked_to_project }
...@@ -1565,9 +1561,9 @@ describe Project, models: true do ...@@ -1565,9 +1561,9 @@ describe Project, models: true do
end end
context 'not forked' do context 'not forked' do
let(:project) { create(:empty_project) }
it 'schedules a RepositoryImportWorker job' do it 'schedules a RepositoryImportWorker job' do
project = create(:empty_project, import_url: generate(:url))
expect(RepositoryImportWorker).to receive(:perform_async).with(project.id) expect(RepositoryImportWorker).to receive(:perform_async).with(project.id)
project.add_import_job project.add_import_job
......
...@@ -9,11 +9,12 @@ describe GroupPolicy, models: true do ...@@ -9,11 +9,12 @@ describe GroupPolicy, models: true do
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:group) { create(:group) } let(:group) { create(:group) }
let(:reporter_permissions) { [:admin_label] }
let(:master_permissions) do let(:master_permissions) do
[ [
:create_projects, :create_projects,
:admin_milestones, :admin_milestones
:admin_label
] ]
end end
...@@ -42,6 +43,7 @@ describe GroupPolicy, models: true do ...@@ -42,6 +43,7 @@ describe GroupPolicy, models: true do
it do it do
is_expected.to include(:read_group) 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(*master_permissions)
is_expected.not_to include(*owner_permissions) is_expected.not_to include(*owner_permissions)
end end
...@@ -52,6 +54,7 @@ describe GroupPolicy, models: true do ...@@ -52,6 +54,7 @@ describe GroupPolicy, models: true do
it do it do
is_expected.to include(:read_group) 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(*master_permissions)
is_expected.not_to include(*owner_permissions) is_expected.not_to include(*owner_permissions)
end end
...@@ -62,6 +65,7 @@ describe GroupPolicy, models: true do ...@@ -62,6 +65,7 @@ describe GroupPolicy, models: true do
it do it do
is_expected.to include(:read_group) is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.not_to include(*master_permissions) is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions) is_expected.not_to include(*owner_permissions)
end end
...@@ -72,6 +76,7 @@ describe GroupPolicy, models: true do ...@@ -72,6 +76,7 @@ describe GroupPolicy, models: true do
it do it do
is_expected.to include(:read_group) is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.not_to include(*master_permissions) is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions) is_expected.not_to include(*owner_permissions)
end end
...@@ -82,6 +87,7 @@ describe GroupPolicy, models: true do ...@@ -82,6 +87,7 @@ describe GroupPolicy, models: true do
it do it do
is_expected.to include(:read_group) is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.to include(*master_permissions) is_expected.to include(*master_permissions)
is_expected.not_to include(*owner_permissions) is_expected.not_to include(*owner_permissions)
end end
...@@ -92,6 +98,7 @@ describe GroupPolicy, models: true do ...@@ -92,6 +98,7 @@ describe GroupPolicy, models: true do
it do it do
is_expected.to include(:read_group) is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.to include(*master_permissions) is_expected.to include(*master_permissions)
is_expected.to include(*owner_permissions) is_expected.to include(*owner_permissions)
end end
...@@ -102,14 +109,27 @@ describe GroupPolicy, models: true do ...@@ -102,14 +109,27 @@ describe GroupPolicy, models: true do
it do it do
is_expected.to include(:read_group) is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.to include(*master_permissions) is_expected.to include(*master_permissions)
is_expected.to include(*owner_permissions) is_expected.to include(*owner_permissions)
end end
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) } 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 } subject { described_class.abilities(current_user, nested_group).to_set }
context 'with no user' do context 'with no user' do
...@@ -117,6 +137,7 @@ describe GroupPolicy, models: true do ...@@ -117,6 +137,7 @@ describe GroupPolicy, models: true do
it do it do
is_expected.not_to include(:read_group) 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(*master_permissions)
is_expected.not_to include(*owner_permissions) is_expected.not_to include(*owner_permissions)
end end
...@@ -127,6 +148,7 @@ describe GroupPolicy, models: true do ...@@ -127,6 +148,7 @@ describe GroupPolicy, models: true do
it do it do
is_expected.to include(:read_group) 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(*master_permissions)
is_expected.not_to include(*owner_permissions) is_expected.not_to include(*owner_permissions)
end end
...@@ -137,6 +159,7 @@ describe GroupPolicy, models: true do ...@@ -137,6 +159,7 @@ describe GroupPolicy, models: true do
it do it do
is_expected.to include(:read_group) is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.not_to include(*master_permissions) is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions) is_expected.not_to include(*owner_permissions)
end end
...@@ -147,6 +170,7 @@ describe GroupPolicy, models: true do ...@@ -147,6 +170,7 @@ describe GroupPolicy, models: true do
it do it do
is_expected.to include(:read_group) is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.not_to include(*master_permissions) is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions) is_expected.not_to include(*owner_permissions)
end end
...@@ -157,6 +181,7 @@ describe GroupPolicy, models: true do ...@@ -157,6 +181,7 @@ describe GroupPolicy, models: true do
it do it do
is_expected.to include(:read_group) is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.to include(*master_permissions) is_expected.to include(*master_permissions)
is_expected.not_to include(*owner_permissions) is_expected.not_to include(*owner_permissions)
end end
...@@ -167,6 +192,7 @@ describe GroupPolicy, models: true do ...@@ -167,6 +192,7 @@ describe GroupPolicy, models: true do
it do it do
is_expected.to include(:read_group) is_expected.to include(:read_group)
is_expected.to include(*reporter_permissions)
is_expected.to include(*master_permissions) is_expected.to include(*master_permissions)
is_expected.to include(*owner_permissions) is_expected.to include(*owner_permissions)
end end
......
...@@ -5,76 +5,217 @@ describe 'Git HTTP requests', lib: true do ...@@ -5,76 +5,217 @@ describe 'Git HTTP requests', lib: true do
include WorkhorseHelpers include WorkhorseHelpers
include UserActivitiesHelpers include UserActivitiesHelpers
it "gives WWW-Authenticate hints" do shared_examples 'pulls require Basic HTTP Authentication' do
clone_get('doesnt/exist.git') 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
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 ') expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end end
end
end
describe "User with no identities" do context "when username and password are provided" do
let(:user) { create(:user) } context "when authentication fails" do
let(:project) { create(:project, :repository, path: 'project.git-project') } 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 authentication succeeds" do
context "when no authentication is provided" do it "does not respond to downloads with status 401 Unauthorized" do
it "responds with status 401 (no project existence information leak)" do download(path, user: user.username, password: user.password) do |response|
download('doesnt/exist.git') do |response| expect(response).not_to have_http_status(:unauthorized)
expect(response).to have_http_status(401) expect(response.header['WWW-Authenticate']).to be_nil
end
end
end
end
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 end
end end
context "when username and password are provided" do context "when username and password are provided" do
context "when authentication fails" do context "when authentication fails" do
it "responds with status 401" do it "responds to uploads with status 401 Unauthorized" do
download('doesnt/exist.git', user: user.username, password: "nope") do |response| upload(path, user: user.username, password: "wrong-password") do |response|
expect(response).to have_http_status(401) expect(response).to have_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end end
end end
end end
context "when authentication succeeds" do context "when authentication succeeds" do
it "responds with status 404" do it "does not respond to uploads with status 401 Unauthorized" do
download('/doesnt/exist.git', user: user.username, password: user.password) do |response| upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(404) expect(response).not_to have_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to be_nil
end end
end end
end end
end end
end end
context "when the Wiki for a project exists" do shared_examples_for 'pulls are allowed' do
it "responds with the right project" do it do
wiki = ProjectWiki.new(project) download(path, env) do |response|
project.update_attribute(:visibility_level, Project::PUBLIC) 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| 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
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) { {} }
it_behaves_like 'pulls are allowed'
it "responds to pulls with the wiki's repo" do
download(path) do |response|
json_body = ActiveSupport::JSON.decode(response.body) json_body = ActiveSupport::JSON.decode(response.body)
expect(response).to have_http_status(200)
expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace) 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) end
end end
end end
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 context 'but the repo is disabled' do
let(:project) { create(:project, :repository_disabled, :wiki_enabled) } let(:project) { create(:project, :repository, :public, :repository_disabled, :wiki_enabled) }
let(:wiki) { ProjectWiki.new(project) }
let(:path) { "/#{wiki.repository.path_with_namespace}.git" } 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
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 before do
project.team << [user, :developer] project.team << [user, :developer]
end end
context 'but the repo is disabled' do
let(:project) { create(:project, :repository, :private, :repository_disabled, :wiki_enabled) }
it 'allows clones' do it 'allows clones' do
download(path, user: user.username, password: user.password) do |response| download(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(200) expect(response).to have_http_status(:ok)
end end
end end
it 'allows pushes' do it 'pushes are allowed' do
upload(path, user: user.username, password: user.password) do |response| upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(200) 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 end
end end
...@@ -84,49 +225,60 @@ describe 'Git HTTP requests', lib: true do ...@@ -84,49 +225,60 @@ describe 'Git HTTP requests', lib: true do
let(:path) { "#{project.path_with_namespace}.git" } let(:path) { "#{project.path_with_namespace}.git" }
context "when the project is public" do context "when the project is public" do
before do let(:project) { create(:project, :repository, :public) }
project.update_attribute(:visibility_level, Project::PUBLIC)
end
it "downloads get status 200" do it_behaves_like 'pushes require Basic HTTP Authentication'
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 "uploads get status 401" do context 'when not authenticated' do
upload(path, {}) do |response| let(:env) { {} }
expect(response).to have_http_status(401)
end it_behaves_like 'pulls are allowed'
end end
context "with correct credentials" do context "when authenticated" do
let(:env) { { user: user.username, password: user.password } } let(:env) { { user: user.username, password: user.password } }
it "uploads get status 403" do context 'as a developer on the team' do
upload(path, env) do |response| before do
expect(response).to have_http_status(403) project.team << [user, :developer]
end
end end
context 'but git-receive-pack is disabled' do it_behaves_like 'pulls are allowed'
it "responds with status 404" do it_behaves_like 'pushes are allowed'
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) 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| upload(path, env) do |response|
expect(response).to have_http_status(403) expect(response).to have_http_status(:forbidden)
end expect(response.body).to eq(git_access_error(:receive_pack_disabled_over_http))
end end
end end
end end
context 'but git-upload-pack is disabled' do context 'but git-upload-pack over HTTP is disabled in config' do
it "responds with status 404" do it "rejects pushes with 403 Forbidden" do
allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false) allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
download(path, {}) do |response| download(path, env) do |response|
expect(response).to have_http_status(404) expect(response).to have_http_status(:forbidden)
expect(response.body).to eq(git_access_error(:upload_pack_disabled_over_http))
end
end
end
end
context 'and not a member of 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(change_access_error(:push_code))
end
end end
end end
end end
...@@ -141,66 +293,41 @@ describe 'Git HTTP requests', lib: true do ...@@ -141,66 +293,41 @@ describe 'Git HTTP requests', lib: true do
context 'when the repo is public' do context 'when the repo is public' do
context 'but the repo is disabled' do context 'but the repo is disabled' do
it 'does not allow to clone the repo' do let(:project) { create(:project, :public, :repository, :repository_disabled) }
project = create(:project, :public, :repository_disabled) let(:path) { "#{project.path_with_namespace}.git" }
let(:env) { {} }
download("#{project.path_with_namespace}.git", {}) do |response| it_behaves_like 'pulls require Basic HTTP Authentication'
expect(response).to have_http_status(:unauthorized) it_behaves_like 'pushes require Basic HTTP Authentication'
end
end
end end
context 'but the repo is enabled' do context 'but the repo is enabled' do
it 'allows to clone the repo' do let(:project) { create(:project, :public, :repository, :repository_enabled) }
project = create(:project, :public, :repository_enabled) let(:path) { "#{project.path_with_namespace}.git" }
let(:env) { {} }
download("#{project.path_with_namespace}.git", {}) do |response| it_behaves_like 'pulls are allowed'
expect(response).to have_http_status(:ok)
end
end
end end
context 'but only project members are allowed' do context 'but only project members are allowed' do
it 'does not allow to clone the repo' do let(:project) { create(:project, :public, :repository, :repository_private) }
project = create(:project, :public, :repository_private)
download("#{project.path_with_namespace}.git", {}) do |response| it_behaves_like 'pulls require Basic HTTP Authentication'
expect(response).to have_http_status(:unauthorized) it_behaves_like 'pushes require Basic HTTP Authentication'
end
end
end end
end end
end end
context "when the project is private" do context "when the project is private" do
before do let(:project) { create(:project, :repository, :private) }
project.update_attribute(:visibility_level, Project::PRIVATE)
end
context "when no authentication is provided" do it_behaves_like 'pulls require Basic HTTP Authentication'
it "responds with status 401 to downloads" do it_behaves_like 'pushes require Basic HTTP Authentication'
download(path, {}) do |response|
expect(response).to have_http_status(401)
end
end
it "responds with status 401 to uploads" do
upload(path, {}) do |response|
expect(response).to have_http_status(401)
end
end
end
context "when username and password are provided" do context "when username and password are provided" do
let(:env) { { user: user.username, password: 'nope' } } let(:env) { { user: user.username, password: 'nope' } }
context "when authentication fails" do 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 context "when the user is IP banned" do
it "responds with status 401" do it "responds with status 401" do
expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true) expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
...@@ -208,7 +335,7 @@ describe 'Git HTTP requests', lib: true do ...@@ -208,7 +335,7 @@ describe 'Git HTTP requests', lib: true do
clone_get(path, env) clone_get(path, env)
expect(response).to have_http_status(401) expect(response).to have_http_status(:unauthorized)
end end
end end
end end
...@@ -222,37 +349,39 @@ describe 'Git HTTP requests', lib: true do ...@@ -222,37 +349,39 @@ describe 'Git HTTP requests', lib: true do
end end
context "when the user is blocked" do context "when the user is blocked" do
it "responds with status 401" do it "rejects pulls with 401 Unauthorized" do
user.block user.block
project.team << [user, :master] project.team << [user, :master]
download(path, env) do |response| download(path, env) do |response|
expect(response).to have_http_status(401) expect(response).to have_http_status(:unauthorized)
end end
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 user.block
download('doesnt/exist.git', env) do |response| download('doesnt/exist.git', env) do |response|
expect(response).to have_http_status(401) expect(response).to have_http_status(:unauthorized)
end end
end end
end end
context "when the user isn't blocked" do context "when the user isn't blocked" do
it "downloads get status 200" do it "resets the IP in Rack Attack on download" do
expect(Rack::Attack::Allow2Ban).to receive(:reset) expect(Rack::Attack::Allow2Ban).to receive(:reset).twice
clone_get(path, env)
expect(response).to have_http_status(200) 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) expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end end
end
it "uploads get status 200" do it "resets the IP in Rack Attack on upload" do
upload(path, env) do |response| expect(Rack::Attack::Allow2Ban).to receive(:reset).twice
expect(response).to have_http_status(200)
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) expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end end
end end
...@@ -272,56 +401,43 @@ describe 'Git HTTP requests', lib: true do ...@@ -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") @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api")
end end
it "downloads get status 200" do let(:path) { "#{project.path_with_namespace}.git" }
clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token let(:env) { { 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
expect(response).to have_http_status(200) it_behaves_like 'pulls are allowed'
end it_behaves_like 'pushes are allowed'
end end
context 'when user has 2FA enabled' do context 'when user has 2FA enabled' do
let(:user) { create(:user, :two_factor) } let(:user) { create(:user, :two_factor) }
let(:access_token) { create(:personal_access_token, user: user) } let(:access_token) { create(:personal_access_token, user: user) }
let(:path) { "#{project.path_with_namespace}.git" }
before do before do
project.team << [user, :master] project.team << [user, :master]
end end
context 'when username and password are provided' do context 'when username and password are provided' do
it 'rejects the clone attempt' do it 'rejects pulls with 2FA error message' do
download("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response| download(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(401) 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') expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
end end
end end
it 'rejects the push attempt' do it 'rejects the push attempt' do
upload("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response| upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(401) 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') expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
end end
end end
end end
context 'when username and personal access token are provided' do context 'when username and personal access token are provided' do
it 'allows clones' do let(:env) { { user: user.username, password: access_token.token } }
download("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
expect(response).to have_http_status(200)
end
end
it 'allows pushes' do it_behaves_like 'pulls are allowed'
upload("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response| it_behaves_like 'pushes are allowed'
expect(response).to have_http_status(200)
end
end
end end
end end
...@@ -357,15 +473,15 @@ describe 'Git HTTP requests', lib: true do ...@@ -357,15 +473,15 @@ describe 'Git HTTP requests', lib: true do
end end
context "when the user doesn't have access to the project" do 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| 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
end end
it "uploads get status 404" do it "uploads get status 404" do
upload(path, user: user.username, password: user.password) do |response| 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 end
end end
...@@ -373,28 +489,41 @@ describe 'Git HTTP requests', lib: true do ...@@ -373,28 +489,41 @@ describe 'Git HTTP requests', lib: true do
end end
context "when a gitlab ci token is provided" do context "when a gitlab ci token is provided" do
let(:project) { create(:project, :repository) }
let(:build) { create(:ci_build, :running) } let(:build) { create(:ci_build, :running) }
let(:project) { build.project }
let(:other_project) { create(:empty_project) } let(:other_project) { create(:empty_project) }
before do
build.update!(project: project) # can't associate it on factory create
end
context 'when build created by system is authenticated' do context 'when build created by system is authenticated' do
it "downloads get status 200" do let(:path) { "#{project.path_with_namespace}.git" }
clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token let(:env) { { user: 'gitlab-ci-token', password: build.token } }
expect(response).to have_http_status(200) it_behaves_like 'pulls are allowed'
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 # A non-401 here is not an information leak since the system is
push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token # "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(401) expect(response).to have_http_status(:forbidden)
expect(response.body).to eq(git_access_error(:upload))
end end
it "downloads from other project get status 404" do # We are "authenticated" as CI using a valid token here. But we are
clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token # 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
end end
...@@ -405,31 +534,27 @@ describe 'Git HTTP requests', lib: true do ...@@ -405,31 +534,27 @@ describe 'Git HTTP requests', lib: true do
end end
shared_examples 'can download code only' do shared_examples 'can download code only' do
it 'downloads get status 200' do let(:path) { "#{project.path_with_namespace}.git" }
allow_any_instance_of(Repository). let(:env) { { user: 'gitlab-ci-token', password: build.token } }
to receive(:exists?).and_return(true)
clone_get "#{project.path_with_namespace}.git",
user: 'gitlab-ci-token', password: build.token
expect(response).to have_http_status(200) it_behaves_like 'pulls are allowed'
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
it 'downloads from non-existing repository and gets 403' do context 'when the repo does not exist' do
allow_any_instance_of(Repository). let(:project) { create(:empty_project) }
to receive(:exists?).and_return(false)
clone_get "#{project.path_with_namespace}.git", it 'rejects pulls with 403 Forbidden' do
user: 'gitlab-ci-token', password: build.token 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 end
it 'uploads get status 403' do it 'rejects pushes with 403 Forbidden' do
push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token 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
end end
...@@ -441,7 +566,7 @@ describe 'Git HTTP requests', lib: true do ...@@ -441,7 +566,7 @@ describe 'Git HTTP requests', lib: true do
it 'downloads from other project get status 403' 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 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
end end
...@@ -453,8 +578,7 @@ describe 'Git HTTP requests', lib: true do ...@@ -453,8 +578,7 @@ describe 'Git HTTP requests', lib: true do
it 'downloads from other project get status 404' 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 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 end
...@@ -462,6 +586,8 @@ describe 'Git HTTP requests', lib: true do ...@@ -462,6 +586,8 @@ describe 'Git HTTP requests', lib: true do
end end
context "when the project path doesn't end in .git" do 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 context "GET info/refs" do
let(:path) { "/#{project.path_with_namespace}/info/refs" } let(:path) { "/#{project.path_with_namespace}/info/refs" }
...@@ -515,7 +641,7 @@ describe 'Git HTTP requests', lib: true do ...@@ -515,7 +641,7 @@ describe 'Git HTTP requests', lib: true do
end end
context "retrieving an info/refs file" do context "retrieving an info/refs file" do
before { project.update_attribute(:visibility_level, Project::PUBLIC) } let(:project) { create(:project, :repository, :public) }
context "when the file exists" do context "when the file exists" do
before do before do
...@@ -529,7 +655,7 @@ describe 'Git HTTP requests', lib: true do ...@@ -529,7 +655,7 @@ describe 'Git HTTP requests', lib: true do
end end
it "returns the file" do it "returns the file" do
expect(response).to have_http_status(200) expect(response).to have_http_status(:ok)
end end
end end
...@@ -537,7 +663,8 @@ describe 'Git HTTP requests', lib: true do ...@@ -537,7 +663,8 @@ describe 'Git HTTP requests', lib: true do
before { get "/#{project.path_with_namespace}/blob/master/info/refs" } before { get "/#{project.path_with_namespace}/blob/master/info/refs" }
it "returns not found" do it "returns not found" do
expect(response).to have_http_status(404) expect(response).to have_http_status(:not_found)
end
end end
end end
end end
...@@ -546,6 +673,7 @@ describe 'Git HTTP requests', lib: true do ...@@ -546,6 +673,7 @@ describe 'Git HTTP requests', lib: true do
describe "User with LDAP identity" do describe "User with LDAP identity" do
let(:user) { create(:omniauth_user, extern_uid: dn) } let(:user) { create(:omniauth_user, extern_uid: dn) }
let(:dn) { 'uid=john,ou=people,dc=example,dc=com' } let(:dn) { 'uid=john,ou=people,dc=example,dc=com' }
let(:path) { 'doesnt/exist.git' }
before do before do
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
...@@ -553,45 +681,37 @@ describe 'Git HTTP requests', lib: true do ...@@ -553,45 +681,37 @@ describe 'Git HTTP requests', lib: true do
allow(Gitlab::LDAP::Authentication).to receive(:login).with(user.username, user.password).and_return(user) allow(Gitlab::LDAP::Authentication).to receive(:login).with(user.username, user.password).and_return(user)
end end
context "when authentication fails" do it_behaves_like 'pulls require Basic HTTP Authentication'
context "when no authentication is provided" do it_behaves_like 'pushes require Basic HTTP Authentication'
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
context "when authentication succeeds" do context "when authentication succeeds" do
context "when the project doesn't exist" do context "when the project doesn't exist" do
it "responds with status 404" do it "responds with status 404 Not Found" do
download('/doesnt/exist.git', user: user.username, password: user.password) do |response| 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
end end
end end
context "when the project exists" do 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 } }
context 'and the user is on the team' do
before do before do
project.team << [user, :master] project.team << [user, :master]
end end
it "responds with status 200" do it "responds with status 200" do
clone_get(path, user: user.username, password: user.password) do |response| clone_get(path, env) do |response|
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
end end
it_behaves_like 'pulls are allowed'
it_behaves_like 'pushes are allowed'
end
end end
end end
end end
......
...@@ -759,8 +759,8 @@ describe 'Git LFS API and storage' do ...@@ -759,8 +759,8 @@ describe 'Git LFS API and storage' do
context 'tries to push to own project' do context 'tries to push to own project' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'responds with 401' do it 'responds with 403 (not 404 because project is public)' do
expect(response).to have_http_status(401) expect(response).to have_http_status(403)
end end
end end
...@@ -769,8 +769,9 @@ describe 'Git LFS API and storage' do ...@@ -769,8 +769,9 @@ describe 'Git LFS API and storage' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) } let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'responds with 401' do # I'm not sure what this tests that is different from the previous test
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 end
end end
...@@ -778,8 +779,8 @@ describe 'Git LFS API and storage' do ...@@ -778,8 +779,8 @@ describe 'Git LFS API and storage' do
context 'does not have user' do context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) } let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it 'responds with 401' do it 'responds with 403 (not 404 because project is public)' do
expect(response).to have_http_status(401) expect(response).to have_http_status(403)
end end
end end
end end
...@@ -979,8 +980,8 @@ describe 'Git LFS API and storage' do ...@@ -979,8 +980,8 @@ describe 'Git LFS API and storage' do
put_authorize put_authorize
end end
it 'responds with 401' do it 'responds with 403 (not 404 because the build user can read the project)' do
expect(response).to have_http_status(401) expect(response).to have_http_status(403)
end end
end end
...@@ -993,8 +994,8 @@ describe 'Git LFS API and storage' do ...@@ -993,8 +994,8 @@ describe 'Git LFS API and storage' do
put_authorize put_authorize
end end
it 'responds with 401' do it 'responds with 404 (do not leak non-public project existence)' do
expect(response).to have_http_status(401) expect(response).to have_http_status(404)
end end
end end
end end
...@@ -1006,8 +1007,8 @@ describe 'Git LFS API and storage' do ...@@ -1006,8 +1007,8 @@ describe 'Git LFS API and storage' do
put_authorize put_authorize
end end
it 'responds with 401' do it 'responds with 404 (do not leak non-public project existence)' do
expect(response).to have_http_status(401) expect(response).to have_http_status(404)
end end
end end
end end
...@@ -1079,8 +1080,8 @@ describe 'Git LFS API and storage' do ...@@ -1079,8 +1080,8 @@ describe 'Git LFS API and storage' do
context 'tries to push to own project' do context 'tries to push to own project' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'responds with 401' do it 'responds with 403 (not 404 because project is public)' do
expect(response).to have_http_status(401) expect(response).to have_http_status(403)
end end
end end
...@@ -1089,8 +1090,9 @@ describe 'Git LFS API and storage' do ...@@ -1089,8 +1090,9 @@ describe 'Git LFS API and storage' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) } let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'responds with 401' do # I'm not sure what this tests that is different from the previous test
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 end
end end
...@@ -1098,8 +1100,8 @@ describe 'Git LFS API and storage' do ...@@ -1098,8 +1100,8 @@ describe 'Git LFS API and storage' do
context 'does not have user' do context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) } let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it 'responds with 401' do it 'responds with 403 (not 404 because project is public)' do
expect(response).to have_http_status(401) expect(response).to have_http_status(403)
end end
end end
end end
......
...@@ -161,15 +161,13 @@ describe Projects::CreateService, '#execute', services: true do ...@@ -161,15 +161,13 @@ describe Projects::CreateService, '#execute', services: true do
end end
context 'when a bad service template is created' do 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 it 'reports an error in the imported project' do
opts[:import_url] = 'http://www.gitlab.com/gitlab-org/gitlab-ce' 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) 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 expect(project.services.count).to eq 0
end end
end end
......
...@@ -35,9 +35,14 @@ module GitHttpHelpers ...@@ -35,9 +35,14 @@ module GitHttpHelpers
yield response yield response
end end
def download_or_upload(*args, &block)
download(*args, &block)
upload(*args, &block)
end
def auth_env(user, password, spnego_request_token) def auth_env(user, password, spnego_request_token)
env = workhorse_internal_api_request_header env = workhorse_internal_api_request_header
if user && password if user
env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password) env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password)
elsif spnego_request_token elsif spnego_request_token
env['HTTP_AUTHORIZATION'] = "Negotiate #{::Base64.strict_encode64('opaque_request_token')}" env['HTTP_AUTHORIZATION'] = "Negotiate #{::Base64.strict_encode64('opaque_request_token')}"
...@@ -45,4 +50,19 @@ module GitHttpHelpers ...@@ -45,4 +50,19 @@ module GitHttpHelpers
env env
end 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 end
require 'spec_helper' require 'spec_helper'
describe RepositoryForkWorker do 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(:fork_project) { create(:project, :repository, forked_from_project: project) }
let(:shell) { Gitlab::Shell.new } let(:shell) { Gitlab::Shell.new }
...@@ -46,15 +46,27 @@ describe RepositoryForkWorker do ...@@ -46,15 +46,27 @@ describe RepositoryForkWorker do
end end
it "handles bad fork" do 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(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( it 'handles unexpected error' do
project.id, source_path = project.full_path
'/test/path', target_path = fork_project.namespace.full_path
project.full_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 end
end end
require 'spec_helper' require 'spec_helper'
describe RepositoryImportWorker do describe RepositoryImportWorker do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project, :import_scheduled) }
subject { described_class.new } subject { described_class.new }
...@@ -21,15 +21,26 @@ describe RepositoryImportWorker do ...@@ -21,15 +21,26 @@ describe RepositoryImportWorker do
context 'when the import has failed' do context 'when the import has failed' do
it 'hide the credentials that were used in the import URL' 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 } 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 }) expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error })
allow(subject).to receive(:jid).and_return('123') allow(subject).to receive(:jid).and_return('123')
expect do
subject.perform(project.id) subject.perform(project.id)
end.to raise_error(RepositoryImportWorker::ImportError, error)
expect(project.reload.import_error).to include("https://*****:*****@test.com/root/repoC.git/")
expect(project.reload.import_jid).not_to be_nil expect(project.reload.import_jid).not_to be_nil
end end
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
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