Commit 1b08cd81 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into add-pagination-headers-to-api

parents c31d777c e47f0e56
Please view this file on the master branch, on stable branches it's out of date.
v 8.5.0 (unreleased)
- Remove gray background from layout in UI
v 8.4.0 (unreleased)
- Add pagination headers to already paginated API resources
- Improve the consistency of commit titles, branch names, tag names, issue/MR titles, on their respective project pages
- Autocomplete data is now always loaded, instead of when focusing a comment text area (Yorick Peterse)
- Improved performance of finding issues for an entire group (Yorick Peterse)
- Added custom application performance measuring system powered by InfluxDB (Yorick Peterse)
......@@ -45,10 +49,12 @@ v 8.4.0 (unreleased)
- Allow subsequent validations in CI Linter
- Show referenced MRs & Issues only when the current viewer can access them
- Fix Encoding::CompatibilityError bug when markdown content has some complex URL (Jason Lee)
- Add API support for managing build variables of project
- Allow broadcast messages to be edited
v 8.3.4
- Use gitlab-workhorse 0.5.4 (fixes API routing bug)
- Add build artifacts browser
v 8.3.3
- Preserve CE behavior with JIRA integration by only calling API if URL is set
......
class @Activities
constructor: ->
Pager.init 20, true
$(".event-filter .btn").bind "click", (event) =>
$(".event-filter a").bind "click", (event) =>
event.preventDefault()
@toggleFilter($(event.currentTarget))
@reloadActivities()
......@@ -12,7 +12,7 @@ class @Activities
toggleFilter: (sender) ->
sender.toggleClass "active"
sender.closest('li').toggleClass "active"
event_filters = $.cookie("event_filter")
filter = sender.attr("id").split("_")[0]
if event_filters
......
......@@ -48,14 +48,15 @@ class @MergeRequest
_this = @
$('a.btn-close, a.btn-reopen').on 'click', (e) ->
$this = $(this)
if $this.data('submitted')
return
e.preventDefault()
e.stopImmediatePropagation()
shouldSubmit = $this.hasClass('btn-comment')
console.log("shouldSubmit")
if shouldSubmit && $this.data('submitted')
return
if shouldSubmit
_this.submitNoteForm($this.closest('form'),$this)
if $this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')
e.preventDefault()
e.stopImmediatePropagation()
_this.submitNoteForm($this.closest('form'),$this)
submitNoteForm: (form, $button) =>
noteText = form.find("textarea.js-note-text").val()
......
......@@ -5,7 +5,7 @@
#
# ### Example Markup
#
# <ul class="nav nav-tabs merge-request-tabs">
# <ul class="nav-links merge-request-tabs">
# <li class="notes-tab active">
# <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
# Discussion
......
......@@ -521,9 +521,13 @@ class @Notes
if textarea.val().trim().length > 0
form.find('.js-note-target-reopen').text('Comment & reopen')
form.find('.js-note-target-close').text('Comment & close')
form.find('.js-note-target-reopen').addClass('btn-comment-and-reopen')
form.find('.js-note-target-close').addClass('btn-comment-and-close')
else
form.find('.js-note-target-reopen').text('Reopen')
form.find('.js-note-target-close').text('Close')
form.find('.js-note-target-reopen').removeClass('btn-comment-and-reopen')
form.find('.js-note-target-close').removeClass('btn-comment-and-close')
initTaskList: ->
@enableTaskList()
......
......@@ -24,6 +24,7 @@
@import "framework/lists.scss";
@import "framework/markdown_area.scss";
@import "framework/mobile.scss";
@import "framework/nav.scss";
@import "framework/pagination.scss";
@import "framework/panels.scss";
@import "framework/selects.scss";
......
......@@ -18,9 +18,9 @@
line-height: 36px;
}
.content-block,
.gray-content-block {
margin: -$gl-padding;
margin-top: 0;
margin-bottom: -$gl-padding;
background-color: $background-color;
padding: $gl-padding;
margin-bottom: 0px;
......@@ -86,10 +86,7 @@
.cover-block {
text-align: center;
background: $background-color;
margin: -$gl-padding;
margin-bottom: 0;
padding: 44px $gl-padding;
border-bottom: 1px solid $border-color;
padding-top: 44px;
position: relative;
.avatar-holder {
......@@ -136,3 +133,23 @@
.block-connector {
margin-top: -1px;
}
.nav-block {
.controls {
float: right;
.btn {
padding: 7px 10px;
margin-top: 11px;
}
}
}
.content-block {
padding: $gl-padding 0;
border-bottom: 1px solid $border-color;
&.oneline-block {
line-height: 42px;
}
}
......@@ -131,6 +131,12 @@
&:last-child {
margin-right: 0px;
}
&.btn-xs {
margin-right: 3px;
}
}
&.disabled {
pointer-events: auto !important;
}
}
......@@ -153,33 +159,6 @@
}
}
.btn-group-next {
.btn {
padding: 9px 0px;
font-size: 15px;
color: #7f8fa4;
border-color: #e7e9ed;
width: 140px;
.badge {
font-weight: normal;
background-color: #eee;
color: #78a;
}
&.active {
border-color: $gl-info;
background: $gl-info;
color: #fff;
.badge {
color: $gl-info;
background-color: white;
}
}
}
}
.btn-clipboard {
border: none;
}
......@@ -374,75 +374,6 @@ table {
}
}
.center-top-menu, .left-top-menu {
@include nav-menu;
text-align: center;
margin-top: 5px;
margin-bottom: $gl-padding;
height: auto;
margin-top: -$gl-padding;
&.no-bottom {
margin-bottom: 0;
}
&.no-top {
margin-top: 0;
}
li a {
display: inline-block;
padding-top: $gl-padding;
padding-bottom: 11px;
margin-bottom: -1px;
}
&.bottom-border {
border-bottom: 1px solid $border-color;
height: 57px;
}
&.wide {
margin-left: -$gl-padding;
margin-right: -$gl-padding;
}
}
.left-top-menu {
text-align: left;
border-bottom: 1px solid #EEE;
}
.center-middle-menu {
@include nav-menu;
padding: 0;
text-align: center;
margin: -$gl-padding;
margin-top: 0;
margin-bottom: 0;
height: 58px;
border-bottom: 1px solid $border-color;
li {
&:after {
content: "|";
color: $border-gray-light;
}
&:last-child {
&:after {
content: none;
}
}
> a {
display: inline-block;
text-transform: uppercase;
font-size: 13px;
}
}
}
.dropzone .dz-preview .dz-progress {
border-color: $border-color !important;
}
......
......@@ -3,11 +3,8 @@
*
*/
.file-holder {
margin-left: -$gl-padding;
margin-right: -$gl-padding;
border: none;
border-top: 1px solid #E7E9EE;
border-bottom: 1px solid #E7E9EE;
border: 1px solid $border-color;
&.readme-holder {
border-bottom: 0;
......
......@@ -8,10 +8,12 @@
.flash-notice {
@extend .alert;
@extend .alert-info;
margin: 0;
}
.flash-alert {
@extend .alert;
@extend .alert-danger;
margin: 0;
}
}
......@@ -28,6 +28,7 @@ header {
min-height: $header-height;
background-color: #fff;
border: none;
border-bottom: 1px solid #EEE;
.container-fluid {
width: 100% !important;
......
......@@ -5,8 +5,6 @@ html {
}
body {
background-color: #F3F3F3 !important;
&.navless {
background-color: white !important;
}
......
......@@ -109,10 +109,8 @@ ul.content-list {
padding: 0;
> li {
padding: $gl-padding;
padding: $gl-padding 0;
border-color: $table-border-color;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
color: $gl-gray;
.avatar {
......@@ -133,6 +131,7 @@ ul.content-list {
.panel > .content-list {
li {
margin: 0;
padding: $gl-padding;
}
}
......@@ -148,7 +147,7 @@ ul.controls {
> li {
float: left;
margin-right: 10px;
&:last-child {
margin-right: 0;
}
......
......@@ -65,13 +65,6 @@
position: relative;
}
.md-header {
ul {
float: left;
margin-bottom: 1px;
}
}
.referenced-users {
color: #4c4e54;
padding-top: 10px;
......@@ -85,23 +78,6 @@
box-shadow: none;
}
.new_note,
.edit_note,
.detail-page-description,
.milestone-description,
.wiki-content,
.merge-request-form {
.nav-tabs {
margin-bottom: 0;
border: none;
li a,
li.active a {
border: 1px solid #DDD;
}
}
}
.markdown-area {
@include border-radius(0);
background: #FFF;
......
......@@ -118,38 +118,3 @@
font-size: 16px;
line-height: 24px;
}
@mixin nav-menu {
padding: 0;
margin: 0;
list-style: none;
height: 56px;
li {
display: inline-block;
a {
padding: 14px;
font-size: 15px;
line-height: 28px;
color: #959494;
border-bottom: 2px solid transparent;
&:hover, &:active, &:focus {
text-decoration: none;
outline: none;
}
}
&.active a {
color: #616060;
border-bottom: 2px solid #4688f1;
}
.badge {
font-weight: normal;
background-color: #eee;
color: #78a;
}
}
}
......@@ -9,7 +9,7 @@
padding-right: 5px;
}
.nav.nav-tabs > li > a {
.nav-links > li > a {
padding: 10px;
font-size: 12px;
margin-right: 3px;
......@@ -81,7 +81,7 @@
display: none;
}
.center-top-menu, .left-top-menu {
.nav-links, .nav-links {
li a {
font-size: 14px;
padding: 19px 10px;
......
.nav-links {
padding: 0;
margin: 0;
list-style: none;
height: auto;
border-bottom: 1px solid $border-color;
li {
display: inline-block;
a {
display: inline-block;
padding: 14px;
padding-top: $gl-padding;
padding-bottom: 11px;
margin-bottom: -1px;
font-size: 15px;
line-height: 28px;
color: #959494;
border-bottom: 2px solid transparent;
&:hover, &:active, &:focus {
text-decoration: none;
outline: none;
}
}
&.active a {
color: #000000;
border-bottom: 2px solid #4688f1;
}
.badge {
font-weight: normal;
background-color: #eee;
color: #78a;
}
}
}
......@@ -21,11 +21,10 @@
.content-wrapper {
width: 100%;
padding: 20px;
.container-fluid {
background: #FFF;
padding: $gl-padding;
padding: 0 $gl-padding;
&.container-blank {
background: none;
......
.table-holder {
margin: -$gl-padding;
margin-top: 0;
margin-bottom: 0;
margin: 0;
}
table {
&.table {
margin-bottom: $gl-padding;
.dropdown-menu a {
text-decoration: none;
}
......@@ -32,6 +30,7 @@ table {
}
th {
background-color: $background-color;
font-weight: normal;
font-size: 15px;
border-bottom: 1px solid $border-color !important;
......
......@@ -5,10 +5,8 @@
padding: 0;
.timeline-entry {
padding: $gl-padding;
padding: $gl-padding 0;
border-color: $table-border-color;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
color: $gl-gray;
border-bottom: 1px solid $border-white-light;
......
......@@ -99,47 +99,6 @@
}
}
// Nav tabs
.nav.nav-tabs {
margin-bottom: 15px;
li {
> a {
margin-right: 5px;
line-height: 20px;
border-color: #EEE;
color: #888;
border-bottom: 1px solid #ddd;
.badge {
background-color: #eee;
color: #888;
text-shadow: 0 1px 1px #fff;
}
i.fa {
line-height: 14px;
}
}
&.active {
> a {
border-color: #CCC;
border-bottom: 1px solid #fff;
color: #333;
font-weight: bold;
}
}
}
}
.nav-tabs > li > a,
.nav-pills > li > a {
color: #666;
}
.nav-pills > .active > a > span > .badge {
background-color: #fff;
color: $gl-primary;
}
/**
* fix to keep tooltips position in top navigation bar
......
......@@ -177,7 +177,7 @@ body {
}
.page-title {
margin-top: 0px;
margin-top: $gl-padding;
line-height: 1.3;
font-size: 1.25em;
font-weight: 600;
......
......@@ -4,7 +4,7 @@
position: absolute;
top: 0px;
right: 4px;
line-height: 40px;
line-height: 56px;
}
a.js-zen-leave {
......
.branch-name{
font-weight: 600;
}
......@@ -2,6 +2,10 @@
display: block;
}
.commit-row-title .commit-title {
font-weight: 600;
}
.commit-author, .commit-committer{
display: block;
color: #999;
......@@ -35,6 +39,8 @@
}
.commit-box {
border-top: 1px solid $border-color;
.commit-title {
margin: 0;
font-size: 23px;
......
.detail-page-header {
margin: -$gl-padding;
padding: 7px $gl-padding;
margin-bottom: 0px;
padding: 11px 0;
border-bottom: 1px solid $border-color;
color: #5c5d5e;
font-size: 16px;
......
// Common
.diff-file {
margin-left: -$gl-padding;
margin-right: -$gl-padding;
border: none;
border-bottom: 1px solid #E7E9EE;
border: 1px solid $border-color;
border-top: none;
.diff-header {
position: relative;
......
......@@ -4,9 +4,7 @@
*/
.event-item {
font-size: $gl-font-size;
padding: $gl-padding $gl-padding $gl-padding ($gl-padding + $gl-avatar-size + 15px);
margin-left: -$gl-padding;
margin-right: -$gl-padding;
padding: $gl-padding 0 $gl-padding ($gl-avatar-size + 15px);
border-bottom: 1px solid $table-border-color;
color: #7f8fa4;
......
......@@ -11,3 +11,8 @@
height: 42px;
}
}
.content-list .group-name {
font-weight: 600;
color: #4c4e54;
}
......@@ -30,7 +30,7 @@
margin-top: 7px;
}
.center-top-menu {
.nav-links {
text-align: left;
}
}
......
......@@ -6,7 +6,7 @@
.issue-title {
margin-bottom: 5px;
font-size: $list-font-size;
font-weight: bold;
font-weight: 600;
}
.issue-info {
......
......@@ -3,9 +3,9 @@
*
*/
.mr-state-widget {
background: #F7F8FA;
background: $background-color;
color: $gl-gray;
border: 1px solid #dce0e6;
border: 1px solid $border-color;
@include border-radius(2px);
form {
......@@ -150,7 +150,7 @@
.merge-request-title {
margin-bottom: 5px;
font-size: $list-font-size;
font-weight: bold;
font-weight: 600;
}
.merge-request-info {
......
......@@ -26,6 +26,8 @@
}
.project-home-panel {
padding-bottom: 40px;
border-bottom: 1px solid $border-color;
.cover-controls {
.project-settings-dropdown {
......@@ -226,7 +228,6 @@
}
.projects-search-form {
.input-group .form-control {
height: 42px;
}
......@@ -351,28 +352,6 @@
color: #555;
}
ul.nav.nav-projects-tabs {
@extend .nav-tabs;
padding-left: 8px;
li {
a {
padding: 6px 25px;
margin-top: 2px;
border-color: #DDD;
background-color: #EEE;
text-shadow: 0 1px 1px white;
color: #555;
}
&.active {
a {
font-weight: bold;
}
}
}
}
.project_member_row form {
margin: 0px;
}
......@@ -416,11 +395,8 @@ ul.nav.nav-projects-tabs {
.top-area {
border-bottom: 1px solid #EEE;
margin: 0 -16px;
padding: 0 $gl-padding;
height: 42px;
ul.left-top-menu {
ul.nav-links {
display: inline-block;
width: 50%;
margin-bottom: 0px;
......@@ -482,11 +458,11 @@ table.table.protected-branches-list tr.no-border {
padding-top: 10px;
padding-bottom: 4px;
ul.nav-pills {
ul.nav {
display:inline-block;
}
.nav-pills li {
.nav li {
display:inline;
}
......@@ -523,8 +499,7 @@ pre.light-well {
}
.projects-search-form {
margin: -$gl-padding;
padding: $gl-padding;
padding: $gl-padding 0;
padding-bottom: 0;
margin-bottom: 0px;
......@@ -574,10 +549,8 @@ pre.light-well {
@include basic-list;
.project-row {
padding: $gl-padding;
padding: $gl-padding 0;
border-color: $table-border-color;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
&.no-description {
.project {
......@@ -631,8 +604,6 @@ pre.light-well {
}
.project-last-commit {
margin: 0 7px;
.ci-status {
margin-right: 16px;
}
......@@ -662,9 +633,7 @@ pre.light-well {
}
.project-show-readme .readme-holder {
margin-left: -$gl-padding;
margin-right: -$gl-padding;
padding: ($gl-padding + 7px);
padding: $gl-padding 0;
border-top: 0;
.edit-project-readme {
......
.tag-name{
font-weight: 600;
}
.tree-holder {
> .nav-block {
margin: 11px 0;
}
.file-finder {
width: 50%;
......@@ -13,7 +16,7 @@
tr {
> td, > th {
line-height: 28px;
line-height: 26px;
}
&:hover {
......@@ -86,12 +89,14 @@
.blob-commit-info {
list-style: none;
padding: $gl-padding;
background: $background-color;
border: 1px solid $border-color;
border-bottom: none;
margin: 0;
padding: 0;
margin-bottom: 5px;
.commit {
padding: $gl-padding 0;
padding: 0;
.commit-row-title {
.commit-row-message {
......@@ -115,3 +120,8 @@
font-weight: normal;
color: $md-link-color;
}
.tree-controls {
float: right;
margin-top: 11px;
}
......@@ -26,6 +26,7 @@ class Admin::IdentitiesController < Admin::ApplicationController
def update
if @identity.update_attributes(identity_params)
RepairLdapBlockedUserService.new(@user).execute
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully updated.'
else
render :edit
......@@ -34,6 +35,7 @@ class Admin::IdentitiesController < Admin::ApplicationController
def destroy
if @identity.destroy
RepairLdapBlockedUserService.new(@user).execute
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully removed.'
else
redirect_to admin_user_identities_path(@user), alert: 'Failed to remove user identity.'
......
......@@ -40,7 +40,9 @@ class Admin::UsersController < Admin::ApplicationController
end
def unblock
if user.activate
if user.ldap_blocked?
redirect_back_or_admin_user(alert: "This user cannot be unlocked manually from GitLab")
elsif user.activate
redirect_back_or_admin_user(notice: "Successfully unblocked")
else
redirect_back_or_admin_user(alert: "Error occurred. User was not unblocked")
......
class Projects::ArtifactsController < Projects::ApplicationController
layout 'project'
before_action :authorize_read_build_artifacts!
def download
unless artifacts_file.file_storage?
return redirect_to artifacts_file.url
end
unless artifacts_file.exists?
return not_found!
end
send_file artifacts_file.path, disposition: 'attachment'
end
def browse
return render_404 unless build.artifacts?
directory = params[:path] ? "#{params[:path]}/" : ''
@entry = build.artifacts_metadata_entry(directory)
return render_404 unless @entry.exists?
end
def file
entry = build.artifacts_metadata_entry(params[:path])
if entry.exists?
render json: { archive: build.artifacts_file.path,
entry: Base64.encode64(entry.path) }
else
render json: {}, status: 404
end
end
private
def build
@build ||= project.builds.unscoped.find_by!(id: params[:build_id])
end
def artifacts_file
@artifacts_file ||= build.artifacts_file
end
def authorize_read_build_artifacts!
unless can?(current_user, :read_build_artifacts, @project)
if current_user.nil?
return authenticate_user!
else
return render_404
end
end
end
end
......@@ -2,7 +2,6 @@ class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all]
before_action :authorize_manage_builds!, except: [:index, :show, :status]
before_action :authorize_download_build_artifacts!, only: [:download]
layout "project"
......@@ -51,18 +50,6 @@ class Projects::BuildsController < Projects::ApplicationController
redirect_to build_path(build)
end
def download
unless artifacts_file.file_storage?
return redirect_to artifacts_file.url
end
unless artifacts_file.exists?
return not_found!
end
send_file artifacts_file.path, disposition: 'attachment'
end
def status
render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
end
......@@ -79,10 +66,6 @@ class Projects::BuildsController < Projects::ApplicationController
@build ||= project.builds.unscoped.find_by!(id: params[:id])
end
def artifacts_file
build.artifacts_file
end
def build_path(build)
namespace_project_build_path(build.project.namespace, build.project, build)
end
......@@ -92,14 +75,4 @@ class Projects::BuildsController < Projects::ApplicationController
return page_404
end
end
def authorize_download_build_artifacts!
unless can?(current_user, :download_build_artifacts, @project)
if current_user.nil?
return authenticate_user!
else
return render_404
end
end
end
end
......@@ -27,13 +27,15 @@ module EventsHelper
key = key.to_s
active = 'active' if @event_filter.active?(key)
link_opts = {
class: "event-filter-link btn btn-default #{active}",
class: "event-filter-link",
id: "#{key}_event_filter",
title: "Filter by #{tooltip.downcase}",
}
link_to request.path, link_opts do
content_tag(:span, ' ' + tooltip)
content_tag :li, class: active do
link_to request.path, link_opts do
content_tag(:span, ' ' + tooltip)
end
end
end
......
......@@ -175,7 +175,7 @@ class Ability
:create_merge_request,
:create_wiki,
:manage_builds,
:download_build_artifacts,
:read_build_artifacts,
:push_code
]
end
......
......@@ -30,10 +30,12 @@
# description :string(255)
# artifacts_file :text
# gl_project_id :integer
# artifacts_metadata :text
#
module Ci
class Build < CommitStatus
include Gitlab::Application.routes.url_helpers
LAZY_ATTRIBUTES = ['trace']
belongs_to :runner, class_name: 'Ci::Runner'
......@@ -49,6 +51,7 @@ module Ci
scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) }
mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader
acts_as_taggable
......@@ -291,21 +294,18 @@ module Ci
end
def target_url
Gitlab::Application.routes.url_helpers.
namespace_project_build_url(project.namespace, project, self)
namespace_project_build_url(project.namespace, project, self)
end
def cancel_url
if active?
Gitlab::Application.routes.url_helpers.
cancel_namespace_project_build_path(project.namespace, project, self)
cancel_namespace_project_build_path(project.namespace, project, self)
end
end
def retry_url
if retryable?
Gitlab::Application.routes.url_helpers.
retry_namespace_project_build_path(project.namespace, project, self)
retry_namespace_project_build_path(project.namespace, project, self)
end
end
......@@ -321,20 +321,35 @@ module Ci
pending? && !any_runners_online?
end
def download_url
if artifacts_file.exists?
Gitlab::Application.routes.url_helpers.
download_namespace_project_build_path(project.namespace, project, self)
end
end
def execute_hooks
build_data = Gitlab::BuildDataBuilder.build(self)
project.execute_hooks(build_data.dup, :build_hooks)
project.execute_services(build_data.dup, :build_hooks)
end
def artifacts?
artifacts_file.exists?
end
def artifacts_download_url
if artifacts?
download_namespace_project_build_artifacts_path(project.namespace, project, self)
end
end
def artifacts_browse_url
if artifacts_browser_supported?
browse_namespace_project_build_artifacts_path(project.namespace, project, self)
end
end
def artifacts_browser_supported?
artifacts? && artifacts_metadata.exists?
end
def artifacts_metadata_entry(path)
Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path).to_entry
end
private
......
......@@ -18,8 +18,12 @@ module Ci
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
validates_presence_of :key
validates_uniqueness_of :key, scope: :gl_project_id
validates :key,
presence: true,
length: { within: 0..255 },
format: { with: /\A[a-zA-Z0-9_]+\z/,
message: "can contain only letters, digits and '_'." }
attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base
end
......
......@@ -131,7 +131,11 @@ class CommitStatus < ActiveRecord::Base
false
end
def download_url
def artifacts_download_url
nil
end
def artifacts_browse_url
nil
end
end
......@@ -18,4 +18,8 @@ class Identity < ActiveRecord::Base
validates :provider, presence: true
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }
validates :user_id, uniqueness: { scope: :provider }
def ldap?
provider.starts_with?('ldap')
end
end
......@@ -397,7 +397,7 @@ class Project < ActiveRecord::Base
result.password = '*****' unless result.password.nil?
result.to_s
rescue
original_url
self.import_url
end
def check_limit
......
......@@ -196,10 +196,22 @@ class User < ActiveRecord::Base
state_machine :state, initial: :active do
event :block do
transition active: :blocked
transition ldap_blocked: :blocked
end
event :ldap_block do
transition active: :ldap_blocked
end
event :activate do
transition blocked: :active
transition ldap_blocked: :active
end
state :blocked, :ldap_blocked do
def blocked?
true
end
end
end
......@@ -207,7 +219,7 @@ class User < ActiveRecord::Base
# Scopes
scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_state(:blocked) }
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :active, -> { with_state(:active) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
......
class RepairLdapBlockedUserService
attr_accessor :user
def initialize(user)
@user = user
end
def execute
user.block if ldap_hard_blocked?
end
private
def ldap_hard_blocked?
user.ldap_blocked? && !user.ldap_user?
end
end
......@@ -60,8 +60,8 @@
%td
.pull-right
- if current_user && can?(current_user, :download_build_artifacts, project) && build.download_url
= link_to build.download_url, title: 'Download artifacts' do
- if current_user && can?(current_user, :read_build_artifacts, project) && build.artifacts?
= link_to build.artifacts_download_url, title: 'Download artifacts' do
%i.fa.fa-download
- if current_user && can?(current_user, :manage_builds, build.project)
- if build.active?
......
......@@ -4,7 +4,7 @@
- if @all_builds.running_or_pending.any?
= link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
%ul.center-top-menu
%ul.nav-links
%li{class: ('active' if @scope.nil?)}
= link_to admin_builds_path do
All
......
- page_title "Logs"
- loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
Gitlab::ProductionLogger, Gitlab::SidekiqLogger]
%ul.nav.nav-tabs.log-tabs
%ul.nav-links.log-tabs
- loggers.each do |klass|
%li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
= link_to klass::file_name, "##{klass::file_name_noext}",
'data-toggle' => 'tab'
%p.light To prevent performance issues admin logs output the last 2000 lines
.gray-content-block
To prevent performance issues admin logs output the last 2000 lines
.tab-content
- loggers.each do |klass|
.tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''),
......
......@@ -12,7 +12,7 @@
%i.fa.fa-pencil-square-o
Edit
%hr
%ul.nav.nav-tabs
%ul.nav-links
= nav_link(path: 'users#show') do
= link_to "Account", admin_user_path(@user)
= nav_link(path: 'users#groups') do
......@@ -23,3 +23,4 @@
= link_to "SSH keys", keys_admin_user_path(@user)
= nav_link(controller: :identities) do
= link_to "Identities", admin_user_identities_path(@user)
.append-bottom-default
- page_title "Users"
= render 'shared/show_aside'
.row
%aside.col-md-3
.admin-filter
%ul.nav.nav-pills.nav-stacked
%li{class: "#{'active' unless params[:filter]}"}
= link_to admin_users_path do
Active
%small.pull-right= number_with_delimiter(User.active.count)
%li{class: "#{'active' if params[:filter] == "admins"}"}
= link_to admin_users_path(filter: "admins") do
Admins
%small.pull-right= number_with_delimiter(User.admins.count)
%li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
= link_to admin_users_path(filter: 'two_factor_enabled') do
2FA Enabled
%small.pull-right= number_with_delimiter(User.with_two_factor.count)
%li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
= link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled
%small.pull-right= number_with_delimiter(User.without_two_factor.count)
%li{class: "#{'active' if params[:filter] == "blocked"}"}
= link_to admin_users_path(filter: "blocked") do
Blocked
%small.pull-right= number_with_delimiter(User.blocked.count)
%li{class: "#{'active' if params[:filter] == "wop"}"}
= link_to admin_users_path(filter: "wop") do
Without projects
%small.pull-right= number_with_delimiter(User.without_projects.count)
%hr
= form_tag admin_users_path, method: :get, class: 'form-inline' do
.form-group
= search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false
= hidden_field_tag "filter", params[:filter]
= button_tag class: 'btn btn-primary' do
%i.fa.fa-search
%hr
= link_to 'Reset', admin_users_path, class: "btn btn-cancel"
.admin-filter
%ul.nav-links
%li{class: "#{'active' unless params[:filter]}"}
= link_to admin_users_path do
Active
%small.badge= number_with_delimiter(User.active.count)
%li{class: "#{'active' if params[:filter] == "admins"}"}
= link_to admin_users_path(filter: "admins") do
Admins
%small.badge= number_with_delimiter(User.admins.count)
%li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
= link_to admin_users_path(filter: 'two_factor_enabled') do
2FA Enabled
%small.badge= number_with_delimiter(User.with_two_factor.count)
%li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
= link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled
%small.badge= number_with_delimiter(User.without_two_factor.count)
%li{class: "#{'active' if params[:filter] == "blocked"}"}
= link_to admin_users_path(filter: "blocked") do
Blocked
%small.badge= number_with_delimiter(User.blocked.count)
%li{class: "#{'active' if params[:filter] == "wop"}"}
= link_to admin_users_path(filter: "wop") do
Without projects
%small.badge= number_with_delimiter(User.without_projects.count)
%section.col-md-9
.panel.panel-default
.panel-heading
Users (#{number_with_delimiter(@users.total_count)})
.panel-head-actions
.dropdown.inline
%a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"}
%span.light sort:
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_name
%b.caret
%ul.dropdown-menu
%li
= link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
= sort_title_name
= link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
= sort_title_recently_signin
= link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
= sort_title_oldest_signin
= link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
= sort_title_recently_created
= link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
= sort_title_oldest_created
= link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
= sort_title_recently_updated
= link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
= sort_title_oldest_updated
= link_to 'New User', new_admin_user_path, class: "btn btn-new btn-sm"
%ul.well-list
- @users.each do |user|
.gray-content-block.second-block
.pull-right
.dropdown.inline
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%span.light sort:
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_name
%b.caret
%ul.dropdown-menu
%li
.list-item-name
- if user.blocked?
%i.fa.fa-lock.cred
= link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
= sort_title_name
= link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
= sort_title_recently_signin
= link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
= sort_title_oldest_signin
= link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
= sort_title_recently_created
= link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
= sort_title_oldest_created
= link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
= sort_title_recently_updated
= link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
= sort_title_oldest_updated
= link_to 'New User', new_admin_user_path, class: "btn btn-new"
= form_tag admin_users_path, method: :get, class: 'form-inline' do
.form-group
= search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false
= hidden_field_tag "filter", params[:filter]
= button_tag class: 'btn btn-primary' do
%i.fa.fa-search
.panel.panel-default
%ul.well-list
- @users.each do |user|
%li
.list-item-name
- if user.blocked?
%i.fa.fa-lock.cred
- else
%i.fa.fa-user.cgreen
= link_to user.name, [:admin, user]
- if user.admin?
%strong.cred (Admin)
- if user == current_user
%span.cred It's you!
.pull-right
%span.light
%i.fa.fa-envelope
= mail_to user.email, user.email, class: 'light'
&nbsp;
.pull-right
= link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs'
- unless user == current_user
- if user.ldap_blocked?
= link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do
%i.fa.fa-lock
Unblock
- elsif user.blocked?
= link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success'
- else
%i.fa.fa-user.cgreen
= link_to user.name, [:admin, user]
- if user.admin?
%strong.cred (Admin)
- if user == current_user
%span.cred It's you!
.pull-right
%span.light
%i.fa.fa-envelope
= mail_to user.email, user.email, class: 'light'
&nbsp;
= link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-xs"
- unless user == current_user
- if user.blocked?
= link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success"
- else
= link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs btn-warning"
- if user.access_locked?
= link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success", data: { confirm: 'Are you sure?' }
- if user.can_be_removed?
= link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove"
= paginate @users, theme: "gitlab"
= link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning'
- if user.access_locked?
= 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?
= link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove'
= paginate @users, theme: "gitlab"
.hidden-xs
= render "events/event_last_push", event: @last_push
.gray-content-block
.nav-block
- if current_user
.pull-right
.controls
= link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
%i.fa.fa-rss
= render 'shared/event_filter'
......
%ul.center-top-menu
%ul.nav-links
%li{ class: ("active" unless params[:filter]) }
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
......
%ul.center-top-menu
%ul.nav-links
= nav_link(page: dashboard_groups_path) do
= link_to dashboard_groups_path, title: 'Your groups', data: {placement: 'right'} do
Your Groups
......
= content_for :flash_message do
= render 'shared/project_limit'
.top-area
%ul.left-top-menu
%ul.nav-links
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
......
%ul.center-top-menu
%ul.nav-links
= nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
= link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
Your Snippets
......
......@@ -48,7 +48,7 @@
#{@milestone.open_items_count} open
= milestone_progress_bar(@milestone)
%ul.center-top-menu.no-top.no-bottom
%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
......
......@@ -3,32 +3,36 @@
= render 'dashboard/snippets_head'
.gray-content-block
.pull-right
.nav-block
.controls
= link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
= icon('plus')
New Snippet
.btn-group.btn-group-next.snippet-scope-menu
= link_to dashboard_snippets_path, class: "btn btn-default #{"active" unless params[:scope]}" do
All
%span.badge
= current_user.snippets.count
= link_to dashboard_snippets_path(scope: 'are_private'), class: "btn btn-default #{"active" if params[:scope] == "are_private"}" do
Private
%span.badge
= current_user.snippets.are_private.count
= link_to dashboard_snippets_path(scope: 'are_internal'), class: "btn btn-default #{"active" if params[:scope] == "are_internal"}" do
Internal
%span.badge
= current_user.snippets.are_internal.count
= link_to dashboard_snippets_path(scope: 'are_public'), class: "btn btn-default #{"active" if params[:scope] == "are_public"}" do
Public
%span.badge
= current_user.snippets.are_public.count
.nav-links.snippet-scope-menu
%li{ class: ("active" unless params[:scope]) }
= link_to dashboard_snippets_path do
All
%span.badge
= current_user.snippets.count
%li{ class: ("active" if params[:scope] == "are_private") }
= link_to dashboard_snippets_path(scope: 'are_private') do
Private
%span.badge
= current_user.snippets.are_private.count
%li{ class: ("active" if params[:scope] == "are_internal") }
= link_to dashboard_snippets_path(scope: 'are_internal') do
Internal
%span.badge
= current_user.snippets.are_internal.count
%li{ class: ("active" if params[:scope] == "are_public") }
= link_to dashboard_snippets_path(scope: 'are_public') do
Public
%span.badge
= current_user.snippets.are_public.count
= render 'snippets/snippets'
......@@ -7,7 +7,7 @@
%h3 Sign in
.login-body
- if form_based_providers.any?
%ul.nav.nav-tabs
%ul.nav-links
- if crowd_enabled?
%li.active
= link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab'
......
- header_title group_title(@group, "Settings", edit_group_path(@group))
- @blank_container = true
.panel.panel-default
.panel.panel-default.prepend-top-default
.panel-heading
Group settings
.panel-body
......
......@@ -2,7 +2,7 @@
- header_title group_title(@group, "Members", group_group_members_path(@group))
- @blank_container = true
.group-members-page
.group-members-page.prepend-top-default
- if current_user && current_user.can?(:admin_group_member, @group)
.panel.panel-default
.panel-heading
......
......@@ -54,7 +54,7 @@
#{@milestone.open_items_count} open
= milestone_progress_bar(@milestone)
%ul.center-top-menu.no-top.no-bottom
%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
......
- page_title "Projects"
- header_title group_title(@group, "Projects", projects_group_path(@group))
.panel.panel-default
.panel.panel-default.prepend-top-default
.panel-heading
%strong= @group.name
projects:
......
- @no_container = true
- @blank_container = true
- unless can?(current_user, :read_group, @group)
- @disable_search_panel = true
......@@ -25,8 +28,8 @@
.cover-desc.description
= markdown(@group.description, pipeline: :description)
- if can?(current_user, :read_group, @group)
%ul.center-top-menu.no-top
%ul.nav-links
%li.active
= link_to "#activity", 'data-toggle' => 'tab' do
Activity
......@@ -35,20 +38,22 @@
= link_to "#projects", 'data-toggle' => 'tab' do
Projects
.tab-content
.tab-pane.active#activity
.gray-content-block.activity-filter-block
- if current_user
= render "events/event_last_push", event: @last_push
- if can?(current_user, :read_group, @group)
%div{ class: container_class }
.tab-content
.tab-pane.active#activity
.activity-filter-block
- if current_user
= render "events/event_last_push", event: @last_push
= render 'shared/event_filter'
= render 'shared/event_filter'
.content_list
= spinner
.content_list
= spinner
.tab-pane#projects
= render "projects", projects: @projects
.tab-pane#projects
= render "projects", projects: @projects
- else
%p.center-top-menu.no-top
%p.nav-links.no-top
No projects to show
......@@ -139,26 +139,9 @@
%h2#navs Navigation
%h4
%code .center-top-menu
%code .nav-links
.example
%ul.center-top-menu
%li.active
%a Open
%li
%a Closed
%h4
%code .btn-group.btn-group-next
.example
%div.btn-group.btn-group-next
%a.btn.active Open
%a.btn Closed
%h4
%code .nav.nav-tabs
.example
%ul.nav.nav-tabs
%ul.nav-links
%li.active
%a Open
%li
......
......@@ -24,7 +24,7 @@
.content-wrapper
= render "layouts/flash"
= yield :flash_message
%div{ class: container_class }
%div{ class: (container_class unless @no_container) }
.content
.clearfix
= yield
......@@ -6,7 +6,7 @@
.alert.alert-info
Some options are unavailable for LDAP accounts
.account-page
.account-page.prepend-top-default
.panel.panel-default.update-token
.panel-heading
Reset Private token
......
.gray-content-block.activity-filter-block
.nav-block.activity-filter-block
- if current_user
.pull-right
.controls
= link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do
%i.fa.fa-rss
......
#tree-holder.tree-holder.clearfix
.gray-content-block.second-block
.nav-block
= render 'projects/tree/tree_header', tree: @tree
= render 'projects/tree/tree_content', tree: @tree
......
.md-area
.md-header.clearfix
%ul.center-top-menu
%ul.nav-links
%li.active
%a.js-md-write-button(href="#md-write-holder" tabindex="-1")
Write
......
%tr{ class: 'tree-item' }
%td.tree-item-file-name
= tree_icon('folder', '755', directory.name)
%span.str-truncated
= link_to directory.name, browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: directory.path)
%td
%td
%tr{ class: 'tree-item' }
%td.tree-item-file-name
= tree_icon('file', '664', file.name)
%span.str-truncated
= file.name
%td
= number_to_human_size(file.metadata[:size], precision: 2)
%td
= link_to file_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: file.path),
class: 'btn btn-xs btn-default artifact-download' do
= icon('download')
- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds'
= render 'projects/builds/header_title'
#tree-holder.tree-holder
.gray-content-block.top-block.clearfix
.pull-right
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build),
class: 'btn btn-default' do
= icon('download')
Download artifacts archive
%div.tree-content-holder
.table-holder
%table.table.tree-table.table-striped
%thead
%tr
%th Name
%th Size
%th Download
= render partial: 'tree_directory', collection: @entry.directories(parent: true), as: :directory
= render partial: 'tree_file', collection: @entry.files, as: :file
- if @entry.empty?
.center Empty
.gray-content-block.top-block
.nav-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'blob', path: @path
......
......@@ -2,7 +2,7 @@
= render "header_title"
.file-editor
%ul.center-top-menu.no-bottom.js-edit-mode
%ul.nav-links.no-bottom.js-edit-mode
%li.active
= link_to '#editor' do
= icon('edit')
......
......@@ -6,7 +6,7 @@
%li(class="js-branch-#{branch.name}")
%div
= link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do
%strong.str-truncated= branch.name
.branch-name.str-truncated= branch.name
&nbsp;
- if branch.name == @repository.root_ref
%span.label.label-primary default
......
......@@ -8,7 +8,7 @@
- if @all_builds.running_or_pending.any?
= link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
%ul.center-top-menu
%ul.nav-links
%li{class: ('active' if @scope.nil?)}
= link_to project_builds_path(@project) do
All
......
......@@ -14,7 +14,7 @@
#up-build-trace
- if @commit.matrix_for_ref?(@build.ref)
%ul.center-top-menu.no-top.no-bottom
%ul.nav-links.no-top.no-bottom
- @commit.latest_builds_for_ref(@build.ref).each do |build|
%li{class: ('active' if build == @build) }
= link_to namespace_project_build_path(@project.namespace, @project, build) do
......@@ -89,9 +89,15 @@
Test coverage
%h1 #{@build.coverage}%
- if current_user && can?(current_user, :download_build_artifacts, @project) && @build.download_url
.build-widget.center
= link_to "Download artifacts", @build.download_url, class: 'btn btn-sm btn-primary'
- if current_user && can?(current_user, :read_build_artifacts, @project) && @build.artifacts?
.build-widget.artifacts
%h4.title Build artifacts
.center
.btn-group{ role: :group }
= link_to "Download", @build.artifacts_download_url, class: 'btn btn-sm btn-primary'
- if @build.artifacts_browser_supported?
= link_to "Browse", @build.artifacts_browse_url, class: 'btn btn-sm btn-primary'
.build-widget
%h4.title
......
%ul.center-top-menu.no-top.no-bottom.commit-ci-menu
%ul.nav-links.no-top.no-bottom.commit-ci-menu
= nav_link(path: 'commit#show') do
= link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Changes
......
......@@ -50,7 +50,7 @@
.commit-info-row.branches
%i.fa.fa-spinner.fa-spin
.commit-box.gray-content-block.middle-block
.commit-box.content-block
%h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line
- if @commit.description.present?
......
......@@ -2,7 +2,9 @@
- page_description @commit.description
= render "projects/commits/header_title"
= render "commit_box"
.prepend-top-default
= render "commit_box"
- if @ci_commit
= render "ci_menu"
- else
......
......@@ -66,8 +66,8 @@
%td
.pull-right
- if current_user && can?(current_user, :download_build_artifacts, commit_status.project) && commit_status.download_url
= link_to commit_status.download_url, title: 'Download artifacts' do
- if current_user && can?(current_user, :read_build_artifacts, commit_status.project) && commit_status.artifacts?
= link_to commit_status.artifacts_download_url, title: 'Download artifacts' do
%i.fa.fa-download
- if current_user && can?(current_user, :manage_builds, commit_status.project)
- if commit_status.active?
......
......@@ -11,7 +11,7 @@
= cache(cache_key) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
.commit-row-title
%strong.str-truncated
.commit-title.str-truncated
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
- if commit.description?
%a.text-expander.js-toggle-button ...
......
%ul.center-top-menu
%ul.nav-links
= nav_link(controller: [:commit, :commits]) do
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
Commits
......
......@@ -6,7 +6,7 @@
= render "head"
.gray-content-block
.gray-content-block.second-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'commits'
......
......@@ -3,7 +3,7 @@
- diff_files = safe_diff_files(diffs)
.gray-content-block.middle-block.oneline-block
.content-block.oneline-block
.inline-parallel-buttons
.btn-group
= inline_diff_btn
......
- @blank_container = true
.project-edit-container
.project-edit-container.prepend-top-default
.project-edit-errors
.project-edit-content
.panel.panel-default
......
%ul.center-top-menu
%ul.nav-links
= nav_link(action: :show) do
= link_to 'Contributors', namespace_project_graph_path
= nav_link(action: :commits) do
......
......@@ -18,7 +18,7 @@
= f.hidden_field :target_branch
.mr-compare.merge-request
%ul.merge-request-tabs.center-top-menu.no-top.no-bottom
%ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.commits-tab
= link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
Commits
......
......@@ -45,7 +45,7 @@
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
- if @commits.present?
%ul.merge-request-tabs.center-top-menu.no-top.no-bottom
%ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
Discussion
......
......@@ -57,7 +57,7 @@
%span.pull-right= @milestone.expires_at
= milestone_progress_bar(@milestone)
%ul.center-top-menu.no-top.no-bottom
%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
......
......@@ -2,7 +2,7 @@
= render "header_title"
- @blank_container = true
.project-members-page
.project-members-page.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
.panel.panel-default
.panel-heading
......
- page_title "Runners"
.light
.light.prepend-top-default
%p
A 'runner' is a process which runs a build.
You can setup as many runners as you need.
......
- @no_container = true
- @blank_container = true
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity")
......@@ -8,11 +11,10 @@
= render 'shared/no_password'
= render 'projects/last_push'
= render "home_panel"
.project-stats.gray-content-block.second-block
%ul.nav.nav-pills
%ul.nav
%li
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
= pluralize(number_with_delimiter(@project.commit_count), 'commit')
......@@ -57,15 +59,17 @@
= link_to add_contribution_guide_path(@project) do
Add Contribution guide
- if @project.archived?
.text-warning.center.prepend-top-20
%p
= icon("exclamation-triangle fw")
Archived project! Repository is read-only
- if @repository.commit
.content-block.second-block.white
= render 'projects/last_commit', commit: @repository.commit, project: @project
%div{ class: container_class }
= render 'projects/last_commit', commit: @repository.commit, project: @project
%div{ class: container_class }
- if @project.archived?
.text-warning.center.prepend-top-20
%p
= icon("exclamation-triangle fw")
Archived project! Repository is read-only
%div{class: "project-show-#{default_project_view}"}
= render default_project_view
%div{class: "project-show-#{default_project_view}"}
= render default_project_view
......@@ -3,7 +3,7 @@
%li
%div
= link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do
%strong
.tag-name
= icon('tag')
= tag.name
- if tag.message.present?
......
......@@ -17,8 +17,8 @@
.pull-right
= link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has_tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
%i.fa.fa-trash-o
.title
%strong= @tag.name
.tag-name.title
= @tag.name
- if @tag.message.present?
%span.light
&nbsp;
......
%div.tree-content-holder
.table-holder
%table.table#tree-slider{class: "table_#{@hex_path} tree-table table-striped" }
%table.table#tree-slider{class: "table_#{@hex_path} tree-table" }
%thead
%tr
%th Name
......
......@@ -5,13 +5,13 @@
= auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
= render 'projects/last_push'
.pull-right
.tree-controls
= render 'projects/find_file_link'
- if can? current_user, :download_code, @project
= render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true
#tree-holder.tree-holder.clearfix
.gray-content-block.top-block
.nav-block
= render 'projects/tree/tree_header', tree: @tree
= render 'projects/tree/tree_content', tree: @tree
......@@ -7,7 +7,7 @@
= render 'projects/wikis/new'
%ul.center-top-menu
%ul.nav-links
= nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
= link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
......
%ul.nav.nav-tabs.search-filter
%ul.nav-links.search-filter
- if @project
%li{class: ("active" if @scope == 'blobs')}
= link_to search_filter_path(scope: 'blobs') do
......
- if @search_results.empty?
= render partial: "search/results/empty"
- else
%p.light
.gray-content-block
Search results for
%code
= @search_term
......
- page_title @search_term
= render 'search/form'
.prepend-top-default
= render 'search/form'
- if @search_term
= render 'search/category'
= render 'search/results'
.btn-group.btn-group-next.event-filter
%ul.nav-links.event-filter
= event_filter_link EventFilter.push, 'Push events'
= event_filter_link EventFilter.merged, 'Merge events'
= event_filter_link EventFilter.comments, 'Comments'
......
.milestones-filters
%ul.center-top-menu
%ul.nav-links
%li{class: ("active" if params[:state].blank? || params[:state] == 'opened')}
= link_to milestones_filter_path(state: 'opened') do
Open
......
......@@ -10,8 +10,7 @@
%i.fa.fa-sign-out
= image_tag group_icon(group), class: "avatar s46 hidden-xs"
= link_to group, class: 'group-name' do
%strong= group.name
= link_to group.name, group, class: 'group-name'
- if group_member
as
......
.issues-filters
.issues-state-filters
%ul.center-top-menu
%ul.nav-links
- if defined?(type) && type == :merge_requests
- page_context_word = 'merge requests'
- else
......
- page_title t('sherlock.title'), t('sherlock.transaction'), t('sherlock.query')
- header_title t('sherlock.title'), sherlock_transactions_path
%ul.center-top-menu
%ul.nav-links
%li.active
%a(href="#tab-general" data-toggle="tab")
= t('sherlock.general')
......
- page_title t('sherlock.title'), t('sherlock.transaction')
- header_title t('sherlock.title'), sherlock_transactions_path
%ul.center-top-menu
%ul.nav-links
%li.active
%a(href="#tab-general" data-toggle="tab")
= t('sherlock.general')
......
- page_title @user.name
- page_description @user.bio
- header_title @user.name, user_path(@user)
- @no_container = true
- @blank_container = true
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
......@@ -8,6 +10,25 @@
= render 'shared/show_aside'
.cover-block
.cover-controls
- if @user == current_user
= link_to profile_path, class: 'btn btn-gray' do
= icon('pencil')
- elsif current_user
%span.report-abuse
- if @user.abuse_report
%button.btn.btn-danger{ title: 'Already reported for abuse',
data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
= icon('exclamation-circle')
- else
= link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray',
title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
= icon('exclamation-circle')
- if current_user
&nbsp;
= link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
= icon('rss')
.avatar-holder
= link_to avatar_icon(@user, 400), target: '_blank' do
= image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
......@@ -47,74 +68,56 @@
= icon('map-marker')
= @user.location
%ul.nav-links.center
%li.active
= link_to "#activity", 'data-toggle' => 'tab' do
Activity
- if @groups.any?
%li
= link_to "#groups", 'data-toggle' => 'tab' do
Groups
- if @contributed_projects.present?
%li
= link_to "#contributed", 'data-toggle' => 'tab' do
Contributed projects
- if @projects.present?
%li
= link_to "#personal", 'data-toggle' => 'tab' do
Personal projects
.cover-controls
- if @user == current_user
= link_to profile_path, class: 'btn btn-gray' do
= icon('pencil')
- elsif current_user
%span.report-abuse
- if @user.abuse_report
%button.btn.btn-danger{ title: 'Already reported for abuse',
data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
= icon('exclamation-circle')
- else
= link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray',
title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
= icon('exclamation-circle')
- if current_user
&nbsp;
= link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
= icon('rss')
.gray-content-block.second-block
.user-calendar
%h4.center.light
%i.fa.fa-spinner.fa-spin
.user-calendar-activities
%div{ class: container_class }
.tab-content
.tab-pane.active#activity
.gray-content-block.white.second-block
%div{ class: container_class }
.user-calendar
%h4.center.light
%i.fa.fa-spinner.fa-spin
.user-calendar-activities
%ul.center-top-menu.no-top.no-bottom.bottom-border.wide
%li.active
= link_to "#activity", 'data-toggle' => 'tab' do
Activity
- if @groups.any?
%li
= link_to "#groups", 'data-toggle' => 'tab' do
Groups
- if @contributed_projects.present?
%li
= link_to "#contributed", 'data-toggle' => 'tab' do
Contributed projects
- if @projects.present?
%li
= link_to "#personal", 'data-toggle' => 'tab' do
Personal projects
.tab-content
.tab-pane.active#activity
.content_list
= spinner
.content_list
= spinner
- if @groups.any?
.tab-pane#groups
%ul.content-list
- @groups.each do |group|
= render 'shared/groups/group', group: group
- if @groups.any?
.tab-pane#groups
%ul.content-list
- @groups.each do |group|
= render 'shared/groups/group', group: group
- if @contributed_projects.present?
.tab-pane#contributed
.contributed-projects
= render 'shared/projects/list',
projects: @contributed_projects.sort_by(&:star_count).reverse,
projects_limit: 5, stars: true, avatar: true
- if @contributed_projects.present?
.tab-pane#contributed
.contributed-projects
= render 'shared/projects/list',
projects: @contributed_projects.sort_by(&:star_count).reverse,
projects_limit: 10, stars: true, avatar: true
- if @projects.present?
.tab-pane#personal
.personal-projects
= render 'shared/projects/list',
projects: @projects.sort_by(&:star_count).reverse,
projects_limit: 10, stars: true, avatar: true
- if @projects.present?
.tab-pane#personal
.personal-projects
= render 'shared/projects/list',
projects: @projects.sort_by(&:star_count).reverse,
projects_limit: 10, stars: true, avatar: true
:javascript
$(".user-calendar").load("#{user_calendar_path}");
......@@ -604,9 +604,14 @@ Rails.application.routes.draw do
member do
get :status
post :cancel
get :download
post :retry
end
resource :artifacts, only: [] do
get :download
get :browse, path: 'browse(/*path)', format: false
get :file, path: 'file/*path', format: false
end
end
resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
......
class Gitlab::Seeder::Builds
BUILD_STATUSES = %w(running pending success failed canceled)
def initialize(project)
@project = project
end
def seed!
ci_commits.each do |ci_commit|
build = Ci::Build.new(build_attributes_for(ci_commit))
artifacts_cache_file(artifacts_archive_path) do |file|
build.artifacts_file = file
end
artifacts_cache_file(artifacts_metadata_path) do |file|
build.artifacts_metadata = file
end
begin
build.save!
print '.'
rescue ActiveRecord::RecordInvalid
print 'F'
end
end
end
def ci_commits
commits = @project.repository.commits('master', nil, 5)
commits_sha = commits.map { |commit| commit.raw.id }
commits_sha.map do |sha|
@project.ensure_ci_commit(sha)
end
rescue
[]
end
def build_attributes_for(ci_commit)
{ name: 'test build', commands: "$ build command",
stage: 'test', stage_idx: 1, ref: 'master',
user_id: build_user, gl_project_id: @project.id,
status: build_status, commit_id: ci_commit.id,
created_at: Time.now, updated_at: Time.now }
end
def build_user
@project.team.users.sample
end
def build_status
BUILD_STATUSES.sample
end
def artifacts_archive_path
Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
end
def artifacts_metadata_path
Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
end
def artifacts_cache_file(file_path)
cache_path = file_path.to_s.gsub('ci_', "p#{@project.id}_")
FileUtils.copy(file_path, cache_path)
File.open(cache_path) do |file|
yield file
end
end
end
Gitlab::Seeder.quiet do
Project.all.sample(10).each do |project|
project_builds = Gitlab::Seeder::Builds.new(project)
project_builds.seed!
end
end
class AddArtifactsMetadataToCiBuild < ActiveRecord::Migration
def change
add_column :ci_builds, :artifacts_metadata, :text
end
end
......@@ -123,6 +123,7 @@ ActiveRecord::Schema.define(version: 20160113111034) do
t.string "description"
t.text "artifacts_file"
t.integer "gl_project_id"
t.text "artifacts_metadata"
end
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
......
......@@ -23,6 +23,7 @@
- [Namespaces](namespaces.md)
- [Settings](settings.md)
- [Keys](keys.md)
- [Build Variables](build_variables.md)
## Clients
......
# Build Variables
## List project variables
Get list of a project's build variables.
```
GET /projects/:id/variables
```
| Attribute | Type | required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
```
curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables"
```
```json
[
{
"key": "TEST_VARIABLE_1",
"value": "TEST_1"
},
{
"key": "TEST_VARIABLE_2",
"value": "TEST_2"
}
]
```
## Show variable details
Get the details of a project's specific build variable.
```
GET /projects/:id/variables/:key
```
| Attribute | Type | required | Description |
|-----------|---------|----------|-----------------------|
| `id` | integer | yes | The ID of a project |
| `key` | string | yes | The `key` of a variable |
```
curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1"
```
```json
{
"key": "TEST_VARIABLE_1",
"value": "TEST_1"
}
```
## Create variable
Create a new build variable.
```
POST /projects/:id/variables
```
| Attribute | Type | required | Description |
|-----------|---------|----------|-----------------------|
| `id` | integer | yes | The ID of a project |
| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
| `value` | string | yes | The `value` of a variable |
```
curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" -F "key=NEW_VARIABLE" -F "value=new value"
```
```json
{
"key": "NEW_VARIABLE",
"value": "new value"
}
```
## Update variable
Update a project's build variable.
```
PUT /projects/:id/variables/:key
```
| Attribute | Type | required | Description |
|-----------|---------|----------|-------------------------|
| `id` | integer | yes | The ID of a project |
| `key` | string | yes | The `key` of a variable |
| `value` | string | yes | The `value` of a variable |
```
curl -X PUT -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" -F "value=updated value"
```
```json
{
"key": "NEW_VARIABLE",
"value": "updated value"
}
```
## Remove variable
Remove a project's build variable.
```
DELETE /projects/:id/variables/:key
```
| Attribute | Type | required | Description |
|-----------|---------|----------|-------------------------|
| `id` | integer | yes | The ID of a project |
| `key` | string | yes | The `key` of a variable |
```
curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1"
```
```json
{
"key": "VARIABLE_1",
"value": "VALUE_1"
}
```
......@@ -558,7 +558,8 @@ Parameters:
- `uid` (required) - id of specified user
Will return `200 OK` on success, or `404 User Not Found` is user cannot be found.
Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
`403 Forbidden` when trying to block an already blocked user by LDAP synchronization.
## Unblock user
......@@ -572,4 +573,5 @@ Parameters:
- `uid` (required) - id of specified user
Will return `200 OK` on success, or `404 User Not Found` is user cannot be found.
Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
`403 Forbidden` when trying to unblock a user blocked by LDAP synchronization.
Feature: Project Builds
Background:
Given I sign in as a user
And I own a project
And CI is enabled
And I have recent build for my project
Scenario: I browse build summary page
When I visit recent build summary page
Then I see summary for build
And I see build trace
Scenario: I download build artifacts
Given recent build has artifacts available
When I visit recent build summary page
And I click artifacts download button
Then download of build artifacts archive starts
Scenario: I browse build artifacts
Given recent build has artifacts available
And recent build has artifacts metadata available
When I visit recent build summary page
And I click artifacts browse button
Then I should see content of artifacts archive
Scenario: I browse subdirectory of build artifacts
Given recent build has artifacts available
And recent build has artifacts metadata available
When I visit recent build summary page
And I click artifacts browse button
And I click link to subdirectory within build artifacts
Then I should see content of subdirectory within artifacts archive
Scenario: I browse directory with UTF-8 characters in name
Given recent build has artifacts available
And recent build has artifacts metadata available
And recent build artifacts contain directory with UTF-8 characters
When I visit recent build summary page
And I click artifacts browse button
And I navigate to directory with UTF-8 characters in name
Then I should see content of directory with UTF-8 characters in name
Scenario: I try to browse directory with invalid UTF-8 characters in name
Given recent build has artifacts available
And recent build has artifacts metadata available
And recent build artifacts contain directory with invalid UTF-8 characters
When I visit recent build summary page
And I click artifacts browse button
And I navigate to parent directory of directory with invalid name
Then I should not see directory with invalid name on the list
Scenario: I download a single file from build artifacts
Given recent build has artifacts available
And recent build has artifacts metadata available
When I visit recent build summary page
And I click artifacts browse button
And I click download button for a file within build artifacts
Then download of a file extracted from build artifacts should start
class Spinach::Features::ProjectBuilds < Spinach::FeatureSteps
include SharedAuthentication
include SharedProject
include SharedBuilds
include RepoHelpers
step 'I see summary for build' do
expect(page).to have_content "Build ##{@build.id}"
end
step 'I see build trace' do
expect(page).to have_css '#build-trace'
end
step 'I click artifacts download button' do
page.within('.artifacts') { click_link 'Download' }
end
step 'download of build artifacts archive starts' do
expect(page.response_headers['Content-Type']).to eq 'application/zip'
expect(page.response_headers['Content-Transfer-Encoding']).to eq 'binary'
end
step 'I click artifacts browse button' do
page.within('.artifacts') { click_link 'Browse' }
end
step 'I should see content of artifacts archive' do
page.within('.tree-table') do
expect(page).to have_no_content '..'
expect(page).to have_content 'other_artifacts_0.1.2'
expect(page).to have_content 'ci_artifacts.txt'
expect(page).to have_content 'rails_sample.jpg'
end
end
step 'I click link to subdirectory within build artifacts' do
page.within('.tree-table') { click_link 'other_artifacts_0.1.2' }
end
step 'I should see content of subdirectory within artifacts archive' do
page.within('.tree-table') do
expect(page).to have_content '..'
expect(page).to have_content 'another-subdirectory'
expect(page).to have_content 'doc_sample.txt'
end
end
step 'recent build artifacts contain directory with UTF-8 characters' do
# metadata fixture contains relevant directory
end
step 'I navigate to directory with UTF-8 characters in name' do
page.within('.tree-table') { click_link 'tests_encoding' }
page.within('.tree-table') { click_link 'utf8 test dir ✓' }
end
step 'I should see content of directory with UTF-8 characters in name' do
page.within('.tree-table') do
expect(page).to have_content '..'
expect(page).to have_content 'regular_file_2'
end
end
step 'recent build artifacts contain directory with invalid UTF-8 characters' do
# metadata fixture contains relevant directory
end
step 'I navigate to parent directory of directory with invalid name' do
page.within('.tree-table') { click_link 'tests_encoding' }
end
step 'I should not see directory with invalid name on the list' do
page.within('.tree-table') do
expect(page).to have_no_content('non-utf8-dir')
end
end
step 'I click download button for a file within build artifacts' do
page.within('.tree-table') { first('.artifact-download').click }
end
step 'download of a file extracted from build artifacts should start' do
# this will be accelerated by Workhorse
response_json = JSON.parse(page.body, symbolize_names: true)
expect(response_json[:archive]).to end_with('build_artifacts.zip')
expect(response_json[:entry]).to eq Base64.encode64('ci_artifacts.txt')
end
end
......@@ -6,7 +6,7 @@ module SharedActiveTab
end
def ensure_active_sub_tab(content)
expect(find('div.content ul.center-top-menu li.active')).to have_content(content)
expect(find('div.content ul.nav-links li.active')).to have_content(content)
end
def ensure_active_sub_nav(content)
......@@ -18,7 +18,7 @@ module SharedActiveTab
end
step 'no other sub tabs should be active' do
expect(page).to have_selector('div.content ul.center-top-menu li.active', count: 1)
expect(page).to have_selector('div.content ul.nav-links li.active', count: 1)
end
step 'no other sub navs should be active' do
......
module SharedBuilds
include Spinach::DSL
step 'CI is enabled' do
@project.enable_ci
end
step 'I have recent build for my project' do
ci_commit = create :ci_commit, project: @project, sha: sample_commit.id
@build = create :ci_build, commit: ci_commit
end
step 'I visit recent build summary page' do
visit namespace_project_build_path(@project.namespace, @project, @build)
end
step 'recent build has artifacts available' do
artifacts = Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
archive = fixture_file_upload(artifacts, 'application/zip')
@build.update_attributes(artifacts_file: archive)
end
step 'recent build has artifacts metadata available' do
metadata = Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
gzip = fixture_file_upload(metadata, 'application/x-gzip')
@build.update_attributes(artifacts_metadata: gzip)
end
end
......@@ -54,5 +54,6 @@ module API
mount Keys
mount Tags
mount Triggers
mount Variables
end
end
......@@ -366,5 +366,9 @@ module API
class TriggerRequest < Grape::Entity
expose :id, :variables
end
class Variable < Grape::Entity
expose :key, :value
end
end
end
......@@ -287,12 +287,14 @@ module API
# file helpers
def uploaded_file!(field, uploads_path)
def uploaded_file(field, uploads_path)
if params[field]
bad_request!("#{field} is not a file") unless params[field].respond_to?(:filename)
return params[field]
end
return nil unless params["#{field}.path"] && params["#{field}.name"]
# sanitize file paths
# this requires all paths to exist
required_attributes! %W(#{field}.path)
......
......@@ -284,10 +284,12 @@ module API
authenticated_as_admin!
user = User.find_by(id: params[:id])
if user
if !user
not_found!('User')
elsif !user.ldap_blocked?
user.block
else
not_found!('User')
forbidden!('LDAP blocked users cannot be modified by the API')
end
end
......@@ -299,10 +301,12 @@ module API
authenticated_as_admin!
user = User.find_by(id: params[:id])
if user
user.activate
else
if !user
not_found!('User')
elsif user.ldap_blocked?
forbidden!('LDAP blocked users cannot be unblocked by the API')
else
user.activate
end
end
end
......
module API
# Projects variables API
class Variables < Grape::API
before { authenticate! }
before { authorize_admin_project }
resource :projects do
# Get project variables
#
# Parameters:
# id (required) - The ID of a project
# page (optional) - The page number for pagination
# per_page (optional) - The value of items per page to show
# Example Request:
# GET /projects/:id/variables
get ':id/variables' do
variables = user_project.variables
present paginate(variables), with: Entities::Variable
end
# Get specific variable of a project
#
# Parameters:
# id (required) - The ID of a project
# key (required) - The `key` of variable
# Example Request:
# GET /projects/:id/variables/:key
get ':id/variables/:key' do
key = params[:key]
variable = user_project.variables.find_by(key: key.to_s)
return not_found!('Variable') unless variable
present variable, with: Entities::Variable
end
# Create a new variable in project
#
# Parameters:
# id (required) - The ID of a project
# key (required) - The key of variable
# value (required) - The value of variable
# Example Request:
# POST /projects/:id/variables
post ':id/variables' do
required_attributes! [:key, :value]
variable = user_project.variables.create(key: params[:key], value: params[:value])
if variable.valid?
present variable, with: Entities::Variable
else
render_validation_error!(variable)
end
end
# Update existing variable of a project
#
# Parameters:
# id (required) - The ID of a project
# key (optional) - The `key` of variable
# value (optional) - New value for `value` field of variable
# Example Request:
# PUT /projects/:id/variables/:key
put ':id/variables/:key' do
variable = user_project.variables.find_by(key: params[:key].to_s)
return not_found!('Variable') unless variable
attrs = attributes_for_keys [:value]
if variable.update(attrs)
present variable, with: Entities::Variable
else
render_validation_error!(variable)
end
end
# Delete existing variable of a project
#
# Parameters:
# id (required) - The ID of a project
# key (required) - The ID of a variable
# Example Request:
# DELETE /projects/:id/variables/:key
delete ':id/variables/:key' do
variable = user_project.variables.find_by(key: params[:key].to_s)
return not_found!('Variable') unless variable
variable.destroy
present variable, with: Entities::Variable
end
end
end
end
......@@ -78,11 +78,13 @@ module Ci
# Parameters:
# id (required) - The ID of a build
# token (required) - The build authorization token
# file (required) - The uploaded file
# file (required) - Artifacts file
# Parameters (accelerated by GitLab Workhorse):
# file.path - path to locally stored body (generated by Workhorse)
# file.name - real filename as send in Content-Disposition
# file.type - real content type as send in Content-Type
# metadata.path - path to locally stored body (generated by Workhorse)
# metadata.name - filename (generated by Workhorse)
# Headers:
# BUILD-TOKEN (required) - The build authorization token, the same as token
# Body:
......@@ -96,13 +98,20 @@ module Ci
build = Ci::Build.find_by_id(params[:id])
not_found! unless build
authenticate_build_token!(build)
forbidden!('build is not running') unless build.running?
forbidden!('Build is not running!') unless build.running?
file = uploaded_file!(:file, ArtifactUploader.artifacts_upload_path)
file_to_large! unless file.size < max_artifacts_size
artifacts_upload_path = ArtifactUploader.artifacts_upload_path
artifacts = uploaded_file(:file, artifacts_upload_path)
metadata = uploaded_file(:metadata, artifacts_upload_path)
if build.update_attributes(artifacts_file: file)
present build, with: Entities::Build
bad_request!('Missing artifacts file!') unless artifacts
file_to_large! unless artifacts.size < max_artifacts_size
build.artifacts_file = artifacts
build.artifacts_metadata = metadata
if build.save
present(build, with: Entities::Build)
else
render_validation_error!(build)
end
......@@ -148,6 +157,7 @@ module Ci
not_found! unless build
authenticate_build_token!(build)
build.remove_artifacts_file!
build.remove_artifacts_metadata!
end
end
end
......
require 'zlib'
require 'json'
module Gitlab
module Ci
module Build
module Artifacts
class Metadata
class ParserError < StandardError; end
VERSION_PATTERN = /^[\w\s]+(\d+\.\d+\.\d+)/
INVALID_PATH_PATTERN = %r{(^\.?\.?/)|(/\.?\.?/)}
attr_reader :file, :path, :full_version
def initialize(file, path)
@file, @path = file, path
@full_version = read_version
end
def version
@full_version.match(VERSION_PATTERN)[1]
end
def errors
gzip do |gz|
read_string(gz) # version
errors = read_string(gz)
raise ParserError, 'Errors field not found!' unless errors
begin
JSON.parse(errors)
rescue JSON::ParserError
raise ParserError, 'Invalid errors field!'
end
end
end
def find_entries!
gzip do |gz|
2.times { read_string(gz) } # version and errors fields
match_entries(gz)
end
end
def to_entry
entries = find_entries!
Entry.new(@path, entries)
end
private
def match_entries(gz)
entries = {}
match_pattern = %r{^#{Regexp.escape(@path)}[^/]*/?$}
until gz.eof? do
begin
path = read_string(gz).force_encoding('UTF-8')
meta = read_string(gz).force_encoding('UTF-8')
next unless path.valid_encoding? && meta.valid_encoding?
next unless path =~ match_pattern
next if path =~ INVALID_PATH_PATTERN
entries[path] = JSON.parse(meta, symbolize_names: true)
rescue JSON::ParserError, Encoding::CompatibilityError
next
end
end
entries
end
def read_version
gzip do |gz|
version_string = read_string(gz)
unless version_string
raise ParserError, 'Artifacts metadata file empty!'
end
unless version_string =~ VERSION_PATTERN
raise ParserError, 'Invalid version!'
end
version_string.chomp
end
end
def read_uint32(gz)
binary = gz.read(4)
binary.unpack('L>')[0] if binary
end
def read_string(gz)
string_size = read_uint32(gz)
return nil unless string_size
gz.read(string_size)
end
def gzip(&block)
Zlib::GzipReader.open(@file, &block)
end
end
end
end
end
end
module Gitlab
module Ci::Build::Artifacts
class Metadata
##
# Class that represents an entry (path and metadata) to a file or
# directory in GitLab CI Build Artifacts binary file / archive
#
# This is IO-operations safe class, that does similar job to
# Ruby's Pathname but without the risk of accessing filesystem.
#
# This class is working only with UTF-8 encoded paths.
#
class Entry
attr_reader :path, :entries
attr_accessor :name
def initialize(path, entries)
@path = path.dup.force_encoding('UTF-8')
@entries = entries
if path.include?("\0")
raise ArgumentError, 'Path contains zero byte character!'
end
unless path.valid_encoding?
raise ArgumentError, 'Path contains non-UTF-8 byte sequence!'
end
end
def directory?
blank_node? || @path.end_with?('/')
end
def file?
!directory?
end
def has_parent?
nodes > 0
end
def parent
return nil unless has_parent?
self.class.new(@path.chomp(basename), @entries)
end
def basename
(directory? && !blank_node?) ? name + '/' : name
end
def name
@name || @path.split('/').last.to_s
end
def children
return [] unless directory?
return @children if @children
child_pattern = %r{^#{Regexp.escape(@path)}[^/]+/?$}
@children = select_entries { |path| path =~ child_pattern }
end
def directories(opts = {})
return [] unless directory?
dirs = children.select(&:directory?)
return dirs unless has_parent? && opts[:parent]
dotted_parent = parent
dotted_parent.name = '..'
dirs.prepend(dotted_parent)
end
def files
return [] unless directory?
children.select(&:file?)
end
def metadata
@entries[@path] || {}
end
def nodes
@path.count('/') + (file? ? 1 : 0)
end
def blank_node?
@path.empty? # "" is considered to be './'
end
def exists?
blank_node? || @entries.include?(@path)
end
def empty?
children.empty?
end
def to_s
@path
end
def ==(other)
@path == other.path && @entries == other.entries
end
def inspect
"#{self.class.name}: #{@path}"
end
private
def select_entries
selected = @entries.select { |path, _metadata| yield path }
selected.map { |path, _metadata| self.class.new(path, @entries) }
end
end
end
end
end
......@@ -37,15 +37,15 @@ module Gitlab
# Block user in GitLab if he/she was blocked in AD
if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
user.block
user.ldap_block
false
else
user.activate if user.blocked? && !ldap_config.block_auto_created_users
user.activate if user.ldap_blocked?
true
end
else
# Block the user if they no longer exist in LDAP/AD
user.block
user.ldap_block
false
end
rescue
......
require 'spec_helper'
describe Admin::IdentitiesController do
let(:admin) { create(:admin) }
before { sign_in(admin) }
describe 'UPDATE identity' do
let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') }
it 'repairs ldap blocks' do
expect_any_instance_of(RepairLdapBlockedUserService).to receive(:execute)
put :update, user_id: user.username, id: user.ldap_identity.id, identity: { provider: 'twitter' }
end
end
describe 'DELETE identity' do
let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') }
it 'repairs ldap blocks' do
expect_any_instance_of(RepairLdapBlockedUserService).to receive(:execute)
delete :destroy, user_id: user.username, id: user.ldap_identity.id
end
end
end
......@@ -34,17 +34,34 @@ describe Admin::UsersController do
end
describe 'PUT unblock/:id' do
let(:user) { create(:user) }
before do
user.block
context 'ldap blocked users' do
let(:user) { create(:omniauth_user, provider: 'ldapmain') }
before do
user.ldap_block
end
it 'will not unblock user' do
put :unblock, id: user.username
user.reload
expect(user.blocked?).to be_truthy
expect(flash[:alert]).to eq 'This user cannot be unlocked manually from GitLab'
end
end
it 'unblocks user' do
put :unblock, id: user.username
user.reload
expect(user.blocked?).to be_falsey
expect(flash[:notice]).to eq 'Successfully unblocked'
context 'manually blocked users' do
let(:user) { create(:user) }
before do
user.block
end
it 'unblocks user' do
put :unblock, id: user.username
user.reload
expect(user.blocked?).to be_falsey
expect(flash[:notice]).to eq 'Successfully unblocked'
end
end
end
......
# == Schema Information
#
# Table name: ci_variables
#
# id :integer not null, primary key
# project_id :integer not null
# key :string(255)
# value :text
# encrypted_value :text
# encrypted_value_salt :string(255)
# encrypted_value_iv :string(255)
# gl_project_id :integer
#
# Read about factories at https://github.com/thoughtbot/factory_girl
FactoryGirl.define do
factory :ci_variable, class: Ci::Variable do
sequence(:key) { |n| "VARIABLE_#{n}" }
value 'VARIABLE_VALUE'
end
end
......@@ -80,7 +80,11 @@ describe "Builds" do
visit namespace_project_build_path(@project.namespace, @project, @build)
end
it { expect(page).to have_content 'Download artifacts' }
it 'has button to download artifacts' do
page.within('.artifacts') do
expect(page).to have_content 'Download'
end
end
end
end
......@@ -111,7 +115,7 @@ describe "Builds" do
before do
@build.update_attributes(artifacts_file: artifacts_file)
visit namespace_project_build_path(@project.namespace, @project, @build)
click_link 'Download artifacts'
page.within('.artifacts') { click_link 'Download' }
end
it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) }
......
require 'spec_helper'
describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
let(:entries) do
{ 'path/' => {},
'path/dir_1/' => {},
'path/dir_1/file_1' => {},
'path/dir_1/file_b' => {},
'path/dir_1/subdir/' => {},
'path/dir_1/subdir/subfile' => {},
'path/second_dir' => {},
'path/second_dir/dir_3/file_2' => {},
'path/second_dir/dir_3/file_3'=> {},
'another_directory/'=> {},
'another_file' => {},
'/file/with/absolute_path' => {} }
end
def path(example)
entry(example.metadata[:path])
end
def entry(path)
described_class.new(path, entries)
end
describe '/file/with/absolute_path', path: '/file/with/absolute_path' do
subject { |example| path(example) }
it { is_expected.to be_file }
it { is_expected.to have_parent }
describe '#basename' do
subject { |example| path(example).basename }
it { is_expected.to eq 'absolute_path' }
end
end
describe 'path/dir_1/', path: 'path/dir_1/' do
subject { |example| path(example) }
it { is_expected.to have_parent }
it { is_expected.to be_directory }
describe '#basename' do
subject { |example| path(example).basename }
it { is_expected.to eq 'dir_1/' }
end
describe '#name' do
subject { |example| path(example).name }
it { is_expected.to eq 'dir_1' }
end
describe '#parent' do
subject { |example| path(example).parent }
it { is_expected.to eq entry('path/') }
end
describe '#children' do
subject { |example| path(example).children }
it { is_expected.to all(be_an_instance_of described_class) }
it do
is_expected.to contain_exactly entry('path/dir_1/file_1'),
entry('path/dir_1/file_b'),
entry('path/dir_1/subdir/')
end
end
describe '#files' do
subject { |example| path(example).files }
it { is_expected.to all(be_file) }
it { is_expected.to all(be_an_instance_of described_class) }
it do
is_expected.to contain_exactly entry('path/dir_1/file_1'),
entry('path/dir_1/file_b')
end
end
describe '#directories' do
context 'without options' do
subject { |example| path(example).directories }
it { is_expected.to all(be_directory) }
it { is_expected.to all(be_an_instance_of described_class) }
it { is_expected.to contain_exactly entry('path/dir_1/subdir/') }
end
context 'with option parent: true' do
subject { |example| path(example).directories(parent: true) }
it { is_expected.to all(be_directory) }
it { is_expected.to all(be_an_instance_of described_class) }
it do
is_expected.to contain_exactly entry('path/dir_1/subdir/'),
entry('path/')
end
end
describe '#nodes' do
subject { |example| path(example).nodes }
it { is_expected.to eq 2 }
end
describe '#exists?' do
subject { |example| path(example).exists? }
it { is_expected.to be true }
end
describe '#empty?' do
subject { |example| path(example).empty? }
it { is_expected.to be false }
end
end
end
describe 'empty path', path: '' do
subject { |example| path(example) }
it { is_expected.to_not have_parent }
describe '#children' do
subject { |example| path(example).children }
it { expect(subject.count).to eq 3 }
end
end
describe 'path/dir_1/subdir/subfile', path: 'path/dir_1/subdir/subfile' do
describe '#nodes' do
subject { |example| path(example).nodes }
it { is_expected.to eq 4 }
end
end
describe 'non-existent/', path: 'non-existent/' do
describe '#empty?' do
subject { |example| path(example).empty? }
it { is_expected.to be true }
end
describe '#exists?' do
subject { |example| path(example).exists? }
it { is_expected.to be false }
end
end
describe 'another_directory/', path: 'another_directory/' do
describe '#empty?' do
subject { |example| path(example).empty? }
it { is_expected.to be true }
end
end
describe '#metadata' do
let(:entries) do
{ 'path/' => { name: '/path/' },
'path/file1' => { name: '/path/file1' },
'path/file2' => { name: '/path/file2' } }
end
subject do
described_class.new('path/file1', entries).metadata[:name]
end
it { is_expected.to eq '/path/file1' }
end
end
require 'spec_helper'
describe Gitlab::Ci::Build::Artifacts::Metadata do
def metadata(path = '')
described_class.new(metadata_file_path, path)
end
let(:metadata_file_path) do
Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
end
context 'metadata file exists' do
describe '#find_entries! empty string' do
subject { metadata('').find_entries! }
it 'matches correct paths' do
expect(subject.keys).to contain_exactly 'ci_artifacts.txt',
'other_artifacts_0.1.2/',
'rails_sample.jpg',
'tests_encoding/'
end
it 'matches metadata for every path' do
expect(subject.keys.count).to eq 4
end
it 'return Hashes for each metadata' do
expect(subject.values).to all(be_kind_of(Hash))
end
end
describe '#find_entries! other_artifacts_0.1.2/' do
subject { metadata('other_artifacts_0.1.2/').find_entries! }
it 'matches correct paths' do
expect(subject.keys).
to contain_exactly 'other_artifacts_0.1.2/',
'other_artifacts_0.1.2/doc_sample.txt',
'other_artifacts_0.1.2/another-subdirectory/'
end
end
describe '#find_entries! other_artifacts_0.1.2/another-subdirectory/' do
subject { metadata('other_artifacts_0.1.2/another-subdirectory/').find_entries! }
it 'matches correct paths' do
expect(subject.keys).
to contain_exactly 'other_artifacts_0.1.2/another-subdirectory/',
'other_artifacts_0.1.2/another-subdirectory/empty_directory/',
'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif'
end
end
describe '#to_entry' do
subject { metadata('').to_entry }
it { is_expected.to be_an_instance_of(Gitlab::Ci::Build::Artifacts::Metadata::Entry) }
end
describe '#full_version' do
subject { metadata('').full_version }
it { is_expected.to eq 'GitLab Build Artifacts Metadata 0.0.1' }
end
describe '#version' do
subject { metadata('').version }
it { is_expected.to eq '0.0.1' }
end
describe '#errors' do
subject { metadata('').errors }
it { is_expected.to eq({}) }
end
end
context 'metadata file does not exist' do
let(:metadata_file_path) { '' }
describe '#find_entries!' do
it 'raises error' do
expect { metadata.find_entries! }.to raise_error(Errno::ENOENT)
end
end
end
end
......@@ -13,64 +13,58 @@ describe Gitlab::LDAP::Access, lib: true do
end
it { is_expected.to be_falsey }
it 'should block user in GitLab' do
access.allowed?
expect(user).to be_blocked
expect(user).to be_ldap_blocked
end
end
context 'when the user is found' do
before do
allow(Gitlab::LDAP::Person).
to receive(:find_by_dn).and_return(:ldap_user)
allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user)
end
context 'and the user is disabled via active directory' do
before do
allow(Gitlab::LDAP::Person).
to receive(:disabled_via_active_directory?).and_return(true)
allow(Gitlab::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(true)
end
it { is_expected.to be_falsey }
it "should block user in GitLab" do
it 'should block user in GitLab' do
access.allowed?
expect(user).to be_blocked
expect(user).to be_ldap_blocked
end
end
context 'and has no disabled flag in active diretory' do
before do
user.block
allow(Gitlab::LDAP::Person).
to receive(:disabled_via_active_directory?).and_return(false)
allow(Gitlab::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(false)
end
it { is_expected.to be_truthy }
context 'when auto-created users are blocked' do
before do
allow_any_instance_of(Gitlab::LDAP::Config).
to receive(:block_auto_created_users).and_return(true)
user.block
end
it "does not unblock user in GitLab" do
it 'does not unblock user in GitLab' do
access.allowed?
expect(user).to be_blocked
expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic
end
end
context "when auto-created users are not blocked" do
context 'when auto-created users are not blocked' do
before do
allow_any_instance_of(Gitlab::LDAP::Config).
to receive(:block_auto_created_users).and_return(false)
user.ldap_block
end
it "should unblock user in GitLab" do
it 'should unblock user in GitLab' do
access.allowed?
expect(user).not_to be_blocked
end
......@@ -80,8 +74,7 @@ describe Gitlab::LDAP::Access, lib: true do
context 'without ActiveDirectory enabled' do
before do
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
allow_any_instance_of(Gitlab::LDAP::Config).
to receive(:active_directory).and_return(false)
allow_any_instance_of(Gitlab::LDAP::Config).to receive(:active_directory).and_return(false)
end
it { is_expected.to be_truthy }
......
# == Schema Information
#
# Table name: builds
#
# id :integer not null, primary key
# project_id :integer
# status :string(255)
# finished_at :datetime
# trace :text
# created_at :datetime
# updated_at :datetime
# started_at :datetime
# runner_id :integer
# commit_id :integer
# coverage :float
# commands :text
# job_id :integer
# name :string(255)
# deploy :boolean default(FALSE)
# options :text
# allow_failure :boolean default(FALSE), not null
# stage :string(255)
# trigger_request_id :integer
#
require 'spec_helper'
describe Ci::Build, models: true do
......@@ -368,21 +343,75 @@ describe Ci::Build, models: true do
end
end
describe :download_url do
subject { build.download_url }
describe :artifacts_download_url do
subject { build.artifacts_download_url }
it "should be nil if artifact doesn't exist" do
build.update_attributes(artifacts_file: nil)
is_expected.to be_nil
end
it 'should be nil if artifact exist' do
it 'should not be nil if artifact exist' do
gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
build.update_attributes(artifacts_file: gif)
is_expected.to_not be_nil
end
end
describe :artifacts_browse_url do
subject { build.artifacts_browse_url }
it "should be nil if artifacts browser is unsupported" do
allow(build).to receive(:artifacts_browser_supported?).and_return(false)
is_expected.to be_nil
end
it 'should not be nil if artifacts browser is supported' do
allow(build).to receive(:artifacts_browser_supported?).and_return(true)
is_expected.to_not be_nil
end
end
describe :artifacts? do
subject { build.artifacts? }
context 'artifacts archive does not exist' do
before { build.update_attributes(artifacts_file: nil) }
it { is_expected.to be_falsy }
end
context 'artifacts archive exists' do
before do
gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
build.update_attributes(artifacts_file: gif)
end
it { is_expected.to be_truthy }
end
end
describe :artifacts_browser_supported? do
subject { build.artifacts_browser_supported? }
context 'artifacts metadata does not exist' do
it { is_expected.to be_falsy }
end
context 'artifacts archive is a zip file and metadata exists' do
before do
fixture_dir = Rails.root + 'spec/fixtures/'
archive = fixture_file_upload(fixture_dir + 'ci_build_artifacts.zip',
'application/zip')
metadata = fixture_file_upload(fixture_dir + 'ci_build_artifacts_metadata.gz',
'application/x-gzip')
build.update_attributes(artifacts_file: archive)
build.update_attributes(artifacts_metadata: metadata)
end
it { is_expected.to be_truthy }
end
end
describe :repo_url do
let(:build) { FactoryGirl.create :ci_build }
let(:project) { build.project }
......
# == Schema Information
#
# Table name: identities
#
# id :integer not null, primary key
# extern_uid :string(255)
# provider :string(255)
# user_id :integer
# created_at :datetime
# updated_at :datetime
#
require 'spec_helper'
RSpec.describe Identity, models: true do
describe 'relations' do
it { is_expected.to belong_to(:user) }
end
describe 'fields' do
it { is_expected.to respond_to(:provider) }
it { is_expected.to respond_to(:extern_uid) }
end
describe '#is_ldap?' do
let(:ldap_identity) { create(:identity, provider: 'ldapmain') }
let(:other_identity) { create(:identity, provider: 'twitter') }
it 'returns true if it is a ldap identity' do
expect(ldap_identity.ldap?).to be_truthy
end
it 'returns false if it is not a ldap identity' do
expect(other_identity.ldap?).to be_falsey
end
end
end
......@@ -569,27 +569,39 @@ describe User, models: true do
end
end
describe :ldap_user? do
it "is true if provider name starts with ldap" do
user = create(:omniauth_user, provider: 'ldapmain')
expect( user.ldap_user? ).to be_truthy
end
context 'ldap synchronized user' do
describe :ldap_user? do
it 'is true if provider name starts with ldap' do
user = create(:omniauth_user, provider: 'ldapmain')
expect(user.ldap_user?).to be_truthy
end
it "is false for other providers" do
user = create(:omniauth_user, provider: 'other-provider')
expect( user.ldap_user? ).to be_falsey
it 'is false for other providers' do
user = create(:omniauth_user, provider: 'other-provider')
expect(user.ldap_user?).to be_falsey
end
it 'is false if no extern_uid is provided' do
user = create(:omniauth_user, extern_uid: nil)
expect(user.ldap_user?).to be_falsey
end
end
it "is false if no extern_uid is provided" do
user = create(:omniauth_user, extern_uid: nil)
expect( user.ldap_user? ).to be_falsey
describe :ldap_identity do
it 'returns ldap identity' do
user = create :omniauth_user
expect(user.ldap_identity.provider).not_to be_empty
end
end
end
describe :ldap_identity do
it "returns ldap identity" do
user = create :omniauth_user
expect(user.ldap_identity.provider).not_to be_empty
describe '#ldap_block' do
let(:user) { create(:omniauth_user, provider: 'ldapmain', name: 'John Smith') }
it 'blocks user flaging the action caming from ldap' do
user.ldap_block
expect(user.blocked?).to be_truthy
expect(user.ldap_blocked?).to be_truthy
end
end
end
......
......@@ -8,6 +8,8 @@ describe API::API, api: true do
let(:key) { create(:key, user: user) }
let(:email) { create(:email, user: user) }
let(:omniauth_user) { create(:omniauth_user) }
let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') }
let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
describe "GET /users" do
context "when unauthenticated" do
......@@ -783,6 +785,12 @@ describe API::API, api: true do
expect(user.reload.state).to eq('blocked')
end
it 'should not re-block ldap blocked users' do
put api("/users/#{ldap_blocked_user.id}/block", admin)
expect(response.status).to eq(403)
expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
it 'should not be available for non admin users' do
put api("/users/#{user.id}/block", user)
expect(response.status).to eq(403)
......@@ -797,7 +805,9 @@ describe API::API, api: true do
end
describe 'PUT /user/:id/unblock' do
let(:blocked_user) { create(:user, state: 'blocked') }
before { admin }
it 'should unblock existing user' do
put api("/users/#{user.id}/unblock", admin)
expect(response.status).to eq(200)
......@@ -805,12 +815,15 @@ describe API::API, api: true do
end
it 'should unblock a blocked user' do
put api("/users/#{user.id}/block", admin)
expect(response.status).to eq(200)
expect(user.reload.state).to eq('blocked')
put api("/users/#{user.id}/unblock", admin)
put api("/users/#{blocked_user.id}/unblock", admin)
expect(response.status).to eq(200)
expect(user.reload.state).to eq('active')
expect(blocked_user.reload.state).to eq('active')
end
it 'should not unblock ldap blocked users' do
put api("/users/#{ldap_blocked_user.id}/unblock", admin)
expect(response.status).to eq(403)
expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
it 'should not be available for non admin users' do
......
require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) }
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
let!(:developer) { create(:project_member, user: user2, project: project, access_level: ProjectMember::DEVELOPER) }
let!(:variable) { create(:ci_variable, project: project) }
describe 'GET /projects/:id/variables' do
context 'authorized user with proper permissions' do
it 'should return project variables' do
get api("/projects/#{project.id}/variables", user)
expect(response.status).to eq(200)
expect(json_response).to be_a(Array)
end
end
context 'authorized user with invalid permissions' do
it 'should not return project variables' do
get api("/projects/#{project.id}/variables", user2)
expect(response.status).to eq(403)
end
end
context 'unauthorized user' do
it 'should not return project variables' do
get api("/projects/#{project.id}/variables")
expect(response.status).to eq(401)
end
end
end
describe 'GET /projects/:id/variables/:key' do
context 'authorized user with proper permissions' do
it 'should return project variable details' do
get api("/projects/#{project.id}/variables/#{variable.key}", user)
expect(response.status).to eq(200)
expect(json_response['value']).to eq(variable.value)
end
it 'should respond with 404 Not Found if requesting non-existing variable' do
get api("/projects/#{project.id}/variables/non_existing_variable", user)
expect(response.status).to eq(404)
end
end
context 'authorized user with invalid permissions' do
it 'should not return project variable details' do
get api("/projects/#{project.id}/variables/#{variable.key}", user2)
expect(response.status).to eq(403)
end
end
context 'unauthorized user' do
it 'should not return project variable details' do
get api("/projects/#{project.id}/variables/#{variable.key}")
expect(response.status).to eq(401)
end
end
end
describe 'POST /projects/:id/variables' do
context 'authorized user with proper permissions' do
it 'should create variable' do
expect do
post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2'
end.to change{project.variables.count}.by(1)
expect(response.status).to eq(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('VALUE_2')
end
it 'should not allow to duplicate variable key' do
expect do
post api("/projects/#{project.id}/variables", user), key: variable.key, value: 'VALUE_2'
end.to change{project.variables.count}.by(0)
expect(response.status).to eq(400)
end
end
context 'authorized user with invalid permissions' do
it 'should not create variable' do
post api("/projects/#{project.id}/variables", user2)
expect(response.status).to eq(403)
end
end
context 'unauthorized user' do
it 'should not create variable' do
post api("/projects/#{project.id}/variables")
expect(response.status).to eq(401)
end
end
end
describe 'PUT /projects/:id/variables/:key' do
context 'authorized user with proper permissions' do
it 'should update variable data' do
initial_variable = project.variables.first
value_before = initial_variable.value
put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP'
updated_variable = project.variables.first
expect(response.status).to eq(200)
expect(value_before).to eq(variable.value)
expect(updated_variable.value).to eq('VALUE_1_UP')
end
it 'should responde with 404 Not Found if requesting non-existing variable' do
put api("/projects/#{project.id}/variables/non_existing_variable", user)
expect(response.status).to eq(404)
end
end
context 'authorized user with invalid permissions' do
it 'should not update variable' do
put api("/projects/#{project.id}/variables/#{variable.key}", user2)
expect(response.status).to eq(403)
end
end
context 'unauthorized user' do
it 'should not update variable' do
put api("/projects/#{project.id}/variables/#{variable.key}")
expect(response.status).to eq(401)
end
end
end
describe 'DELETE /projects/:id/variables/:key' do
context 'authorized user with proper permissions' do
it 'should delete variable' do
expect do
delete api("/projects/#{project.id}/variables/#{variable.key}", user)
end.to change{project.variables.count}.by(-1)
expect(response.status).to eq(200)
end
it 'should responde with 404 Not Found if requesting non-existing variable' do
delete api("/projects/#{project.id}/variables/non_existing_variable", user)
expect(response.status).to eq(404)
end
end
context 'authorized user with invalid permissions' do
it 'should not delete variable' do
delete api("/projects/#{project.id}/variables/#{variable.key}", user2)
expect(response.status).to eq(403)
end
end
context 'unauthorized user' do
it 'should not delete variable' do
delete api("/projects/#{project.id}/variables/#{variable.key}")
expect(response.status).to eq(401)
end
end
end
end
......@@ -210,6 +210,52 @@ describe Ci::API::API do
end
end
context 'should post artifacts file and metadata file' do
let!(:artifacts) { file_upload }
let!(:metadata) { file_upload2 }
let(:stored_artifacts_file) { build.reload.artifacts_file.file }
let(:stored_metadata_file) { build.reload.artifacts_metadata.file }
before do
build.run!
post(post_url, post_data, headers_with_token)
end
context 'post data accelerated by workhorse is correct' do
let(:post_data) do
{ 'file.path' => artifacts.path,
'file.name' => artifacts.original_filename,
'metadata.path' => metadata.path,
'metadata.name' => metadata.original_filename }
end
it 'responds with valid status' do
expect(response.status).to eq(201)
end
it 'stores artifacts and artifacts metadata' do
expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
end
end
context 'no artifacts file in post data' do
let(:post_data) do
{ 'metadata' => metadata }
end
it 'is expected to respond with bad request' do
expect(response.status).to eq(400)
end
it 'does not store metadata' do
expect(stored_metadata_file).to be_nil
end
end
end
context "should fail to post too large artifact" do
before do
build.run!
......
require 'spec_helper'
describe RepairLdapBlockedUserService, services: true do
let(:user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
let(:identity) { user.ldap_identity }
subject(:service) { RepairLdapBlockedUserService.new(user) }
describe '#execute' do
it 'change to normal block after destroying last ldap identity' do
identity.destroy
service.execute
expect(user.reload).not_to be_ldap_blocked
end
it 'change to normal block after changing last ldap identity to another provider' do
identity.update_attribute(:provider, 'twitter')
service.execute
expect(user.reload).not_to be_ldap_blocked
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