Merge branch 'dev' into feature/adding-and-removing-watchers

Conflicts:
	app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js
	karma/tests/controllers/work-package-details-controller-test.js
	lib/api/v3/work_packages/work_package_representer.rb
pull/1611/head
Till Breuer 11 years ago
parent c083456661
commit 2c4483544c
  1. 6
      app/assets/javascripts/angular/helpers/components/path-helper.js
  2. 4
      app/assets/javascripts/angular/models/query.js
  3. 6
      app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js
  4. 26
      app/assets/javascripts/angular/work_packages/controllers/work-packages-list-controller.js
  5. 50
      app/assets/javascripts/angular/work_packages/tabs/attachment-title-cell-directive.js
  6. 47
      app/assets/javascripts/angular/work_packages/tabs/attachment-user-cell-directive.js
  7. 42
      app/assets/javascripts/angular/work_packages/tabs/attachments-table-directive.js
  8. 43
      app/assets/javascripts/angular/work_packages/tabs/attachments-title-directive.js
  9. 56
      app/assets/stylesheets/layout/_split_view.sass
  10. 6
      app/controllers/account_controller.rb
  11. 2
      app/controllers/api/experimental/concerns/query_loading.rb
  12. 2
      app/controllers/concerns/omniauth_login.rb
  13. 47
      app/controllers/sys_controller.rb
  14. 2
      app/models/work_package.rb
  15. 2
      app/views/settings/_repositories.html.erb
  16. 2
      app/views/work_packages/bulk/edit.html.erb
  17. 1
      config/locales/de.yml
  18. 1
      config/locales/en.yml
  19. 3
      config/locales/js-de.yml
  20. 3
      config/locales/js-en.yml
  21. 2
      config/settings.yml
  22. 2
      db/migrate/migration_utils/customizable_utils.rb
  23. 2
      doc/CHANGELOG.md
  24. 175
      extra/svn/OpenProjectAuthentication.pm
  25. 52
      extra/svn/create_views.sql
  26. 163
      extra/svn/reposman.rb
  27. 6
      karma/tests/controllers/work-package-details-controller-test.js
  28. 72
      karma/tests/directives/work_packages/attachment-title-cell-directive-test.js
  29. 79
      karma/tests/directives/work_packages/attachment-user-cell-directive-test.js
  30. 67
      karma/tests/directives/work_packages/attachments-title-directive-test.js
  31. 5
      lib/api/v3/work_packages/work_package_model.rb
  32. 1
      lib/api/v3/work_packages/work_package_representer.rb
  33. 18
      lib/open_project/repository_authentication.rb
  34. 6
      lib/plugins/acts_as_attachable/lib/acts_as_attachable.rb
  35. 8
      lib/redmine/menu_manager/top_menu_helper.rb
  36. 3
      public/templates/work_packages.list.details.html
  37. 1
      public/templates/work_packages/tabs/_attachment_title_cell.html
  38. 1
      public/templates/work_packages/tabs/_attachment_user_cell.html
  39. 19
      public/templates/work_packages/tabs/_attachments_table.html
  40. 1
      public/templates/work_packages/tabs/_attachments_title.html
  41. 6
      public/templates/work_packages/tabs/attachments.html
  42. 2
      spec/controllers/account_controller_spec.rb
  43. 10
      spec/controllers/api/experimental/work_packages_controller_spec.rb
  44. 229
      spec/controllers/sys_controller_spec.rb
  45. 5
      spec/lib/api/v3/work_packages/work_package_representer_spec.rb
  46. 2
      spec/views/layouts/base.html.erb_spec.rb

@ -52,6 +52,9 @@ angular.module('openproject.helpers')
assetPath: function(assetIdentifier) {
return '/assets/' + assetIdentifier;
},
attachmentPath: function(attachmentId, fileName) {
return '/attachments/' + attachmentId + '/' + fileName;
},
boardsPath: function(projectIdentifier) {
return PathHelper.projectPath(projectIdentifier) + '/boards';
},
@ -220,6 +223,9 @@ angular.module('openproject.helpers')
},
// Static
staticAttachmentPath: function(attachmentId, fileName) {
return PathHelper.staticBase + PathHelper.attachmentPath(attachmentId, fileName);
},
staticUserPath: function(userId) {
return PathHelper.staticBase + PathHelper.userPath(userId);
},

@ -96,7 +96,7 @@ angular.module('openproject.models')
unstar: function() {
this.starred = false;
},
},
getQueryString: function(){
return UrlParamsHelper.buildQueryString(this.toParams());
@ -187,7 +187,7 @@ angular.module('openproject.models')
*/
setDefaultFilter: function() {
var statusOpenFilterData = this.getExtendedFilterData({name: 'status_id', operator: 'o'});
this.filters = new Array(new Filter(statusOpenFilterData));
this.filters = [new Filter(statusOpenFilterData)];
},
/**

@ -127,6 +127,12 @@ angular.module('openproject.workPackages.controllers')
.then(refreshWorkPackage, outputError);
};
// Attachments
$scope.attachments = workPackage.embedded.attachments;
// Author
$scope.author = workPackage.embedded.author;
// work package properties
$scope.presentWorkPackageProperties = [];

@ -60,19 +60,23 @@ angular.module('openproject.workPackages.controllers')
$scope.disableFilters = false;
$scope.disableNewWorkPackage = true;
var getWorkPackages, params;
var fetchWorkPackages;
if($scope.query_id){
getWorkPackages = WorkPackageService.getWorkPackagesByQueryId($scope.projectIdentifier, $scope.query_id);
fetchWorkPackages = WorkPackageService.getWorkPackagesByQueryId($scope.projectIdentifier, $scope.query_id);
} else {
getWorkPackages = WorkPackageService.getWorkPackagesFromUrlQueryParams($scope.projectIdentifier, $location);
fetchWorkPackages = WorkPackageService.getWorkPackagesFromUrlQueryParams($scope.projectIdentifier, $location);
}
$scope.settingUpPage = getWorkPackages.then(setupPage);
loadProjectTypesAndQueries();
$scope.settingUpPage = fetchWorkPackages // put promise in scope for cg-busy
.then(setupPage)
.then(function() {
fetchAvailableColumns();
fetchProjectTypesAndQueries();
QueryService.loadAvailableGroupedQueries($scope.projectIdentifier);
});
}
function loadProjectTypesAndQueries() {
function fetchProjectTypesAndQueries() {
if ($scope.projectIdentifier) {
ProjectService.getProject($scope.projectIdentifier)
.then(function(project) {
@ -82,16 +86,12 @@ angular.module('openproject.workPackages.controllers')
});
}
QueryService.loadAvailableGroupedQueries($scope.projectIdentifier);
}
function setupPage(json) {
initQuery(json.meta);
setupWorkPackagesTable(json);
initAvailableColumns();
if (json.work_packages.length) {
$scope.preselectedWorkPackageId = json.work_packages[0].id;
}
@ -153,7 +153,7 @@ angular.module('openproject.workPackages.controllers')
AuthorisationService.initModelAuth("query", meta.query._links);
}
function initAvailableColumns() {
function fetchAvailableColumns() {
return QueryService.loadAvailableUnusedColumns($scope.projectIdentifier)
.then(function(data){
$scope.availableUnusedColumns = data;
@ -187,7 +187,7 @@ angular.module('openproject.workPackages.controllers')
$scope.setQueryState = function(query_id) {
$state.go('work-packages.list', { query_id: query_id });
}
};
// More

@ -0,0 +1,50 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.workPackages.directives')
.directive('attachmentTitleCell', ['PathHelper', function(PathHelper){
return {
restrict: 'A',
replace: false,
templateUrl: '/templates/work_packages/tabs/_attachment_title_cell.html',
scope: {
attachment: '='
},
link: function(scope, element, attributes) {
scope.attachmentPath = PathHelper.staticAttachmentPath(scope.attachment.props.id, scope.attachment.props.fileName);
scope.displayTitle = scope.attachment.props.fileName + " (" + formattedFilesize(scope.attachment.props.fileSize) + ")";
function formattedFilesize(fileSize) {
var size = parseFloat(fileSize);
return isNaN(size) ? "0kB" : (size / 1000).toFixed(2) + "kB";
};
}
};
}]);

@ -0,0 +1,47 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.workPackages.directives')
.directive('attachmentUserCell', ['PathHelper', function(PathHelper){
return {
restrict: 'A',
templateUrl: '/templates/work_packages/tabs/_attachment_user_cell.html',
scope: {
attachment: '='
},
link: function(scope, element, attributes) {
scope.attachment.links.author.fetch()
.then(function(author){
scope.authorName = author.props.name;
scope.authorId = author.props.id;
scope.userPath = PathHelper.staticUserPath(author.props.id);
});
}
};
}]);

@ -0,0 +1,42 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.workPackages.directives')
.directive('attachmentsTable', ['PathHelper', 'I18n', function(PathHelper, I18n){
return {
restrict: 'E',
templateUrl: '/templates/work_packages/tabs/_attachments_table.html',
scope: {
attachments: '='
},
link: function(scope) {
scope.I18n = I18n;
}
};
}]);

@ -0,0 +1,43 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.workPackages.directives')
.directive('attachmentsTitle', [function(){
return {
restrict: 'E',
replace: true,
templateUrl: '/templates/work_packages/tabs/_attachments_title.html',
scope: {
attachments: '='
},
link: function(scope, element, attributes) {
scope.attachmentsTitle = "Attachments (" + scope.attachments.length + ")";
}
};
}]);

@ -168,6 +168,62 @@ div.detail-panel-latest-activity
margin: 0 0 20px 0
padding: 0
.attachments-container
float: left
margin: 0 0 30px 0
width: 100%
.attachments-container ul
margin: 0
padding: 0
list-style-type: none
.attachments-container ul li
margin: 0
padding: 0
line-height: 20px
.add-file
float: left
padding: 8px 0 0 10px
.add-file i
font-size: 12px
padding: 0 2px 0 0
.upload-file
display: block
width: 100%
float: left
margin: 20px 0 0 0
padding: 20px 0 0 0
border-top: 1px solid #ddd
.attachments-container table
padding: 0
margin: 0px 0 10px 0
float: left
border-collapse: collapse
border: 0px solid #ddd
.attachments-container table tr th
text-align: left
font-family: 'LatoBold'
font-weight: normal
text-transform: uppercase
background: #fff
padding: 6px 10px 6px 0
border-bottom: 2px solid #eee
.attachments-container table tr td
text-align: left
font-weight: normal
border-bottom: 0px solid #ddd
padding: 6px 10px 6px 0
.attachments-container table tr:hover
background: #ffffae
img
&.avatar
width: 36px

@ -31,7 +31,7 @@ require 'concerns/omniauth_login'
class AccountController < ApplicationController
include CustomFieldsHelper
include OmniauthLogin
include Concerns::OmniauthLogin
# prevents login action to be filtered by check_if_login_required application scope filter
skip_before_filter :check_if_login_required
@ -44,8 +44,8 @@ class AccountController < ApplicationController
def login
if User.current.logged?
redirect_to home_url
elsif OmniauthLogin.direct_login?
redirect_to OmniauthLogin.direct_login_provider_url
elsif Concerns::OmniauthLogin.direct_login?
redirect_to Concerns::OmniauthLogin.direct_login_provider_url
elsif request.post?
authenticate_user
end

@ -35,7 +35,7 @@ module Api::Experimental::Concerns::QueryLoading
@query = Query.find(params[:query_id])
@query.project = @project if @query.project.nil?
else
@query = Query.new({name: "_", :project => @project})
@query = Query.new({ name: "_", :project => @project }, :initialize_with_default_filter => true)
end
prepare_query
@query

@ -1,6 +1,6 @@
##
# Intended to be used by the AccountController to handle omniauth logins
module OmniauthLogin
module Concerns::OmniauthLogin
def omniauth_login
auth_hash = request.env['omniauth.auth']

@ -29,6 +29,7 @@
class SysController < ActionController::Base
before_filter :check_enabled
before_filter :require_basic_auth, :only => [ :repo_auth ]
def projects
p = Project.active.has_module(:repository).find(:all, :include => :repository, :order => 'identifier')
@ -70,6 +71,19 @@ class SysController < ActionController::Base
render :nothing => true, :status => 404
end
def repo_auth
@project = Project.find_by_identifier(params[:repository])
if ( %w(GET PROPFIND REPORT OPTIONS).include?(params[:method]) &&
@authenticated_user.allowed_to?(:browse_repository, @project) ) ||
@authenticated_user.allowed_to?(:commit_access, @project)
render :text => "Access granted"
return
end
render :text => "Not allowed", :status => 403 # default to deny
end
protected
def check_enabled
@ -79,4 +93,37 @@ class SysController < ActionController::Base
return false
end
end
private
def require_basic_auth
authenticate_with_http_basic do |username, password|
@authenticated_user = cached_user_login(username, password)
return true if @authenticated_user
end
response.headers["WWW-Authenticate"] = 'Basic realm="Repository Authentication"'
render :text => "Authorization required", :status => 401
false
end
def user_login(username, password)
User.try_to_login(username, password)
end
def cached_user_login(username, password)
unless Setting.repository_authentication_caching_enabled?
return user_login(username, password)
end
user = nil
user_id = Rails.cache.fetch(OpenProject::RepositoryAuthentication::CACHE_PREFIX + Digest::SHA1.hexdigest("#{username}#{password}"),
:expires_in => OpenProject::RepositoryAuthentication::CACHE_EXPIRES_AFTER) do
user = user_login(username, password)
user ? user.id.to_s : '-1'
end
return nil if user_id.blank? or user_id == '-1'
user || User.find_by_id(user_id.to_i)
end
end

@ -150,7 +150,7 @@ class WorkPackage < ActiveRecord::Base
# test_destroying_root_projects_should_clear_data #
# for details. #
###################################################
acts_as_attachable :after_remove => :attachments_changed
acts_as_attachable :after_remove => :attachments_changed, :order => "#{Attachment.table_name}.filename"
after_validation :set_attachments_error_details, if: lambda {|work_package| work_package.errors.messages.has_key? :attachments}

@ -48,6 +48,8 @@ See doc/COPYRIGHT.rdoc for more details.
<em><%= l(:text_comma_separated) %></em></p>
<p><%= setting_text_field :repository_log_display_limit, :size => 6 %></p>
<p><%= setting_check_box :repository_authentication_caching_enabled %></p>
</div>
<fieldset class="box tabular settings"><legend><%= l(:text_work_packages_ref_in_commit_messages) %></legend>

@ -29,7 +29,7 @@ See doc/COPYRIGHT.rdoc for more details.
<h2><%= l(:label_bulk_edit_selected_work_packages) %></h2>
<ul><%= @work_packages.collect {|i| content_tag('li', link_to(h("#{i.type} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n").html_safe %></ul>
<ul><%= @work_packages.collect {|i| content_tag('li', link_to(h("#{i.type} ##{i.id}"), work_package_path(i)) + h(": #{i.subject}")) }.join("\n").html_safe %></ul>
<%= form_tag(url_for(controller: '/work_packages/bulk', action: :update, ids: @work_packages),
method: :put) do %>

@ -1267,6 +1267,7 @@ de:
setting_plain_text_mail: "Nur reinen Text (kein HTML) senden"
setting_protocol: "Protokoll"
setting_repositories_encodings: "Kodierungen der Projektarchive"
setting_repository_authentication_caching_enabled: "Aktiviere Cache für Authentifizierungsversuche von Versionskontrollsoftware"
setting_repository_log_display_limit: "Maximale Anzahl anzuzeigender Revisionen in der Historie einer Datei"
setting_rest_api_enabled: "REST-Schnittstelle aktivieren"
setting_self_registration: "Anmeldung ermöglicht"

@ -1255,6 +1255,7 @@ en:
setting_plain_text_mail: "Plain text mail (no HTML)"
setting_protocol: "Protocol"
setting_repositories_encodings: "Repositories encodings"
setting_repository_authentication_caching_enabled: "Enable caching for authentication request of version control software"
setting_repository_log_display_limit: "Maximum number of revisions displayed on file log"
setting_rest_api_enabled: "Enable REST web service"
setting_self_registration: "Self-registration"

@ -68,12 +68,15 @@ de:
label_collapse_all: "Alle zuklappen"
label_commented_on: "kommentiert am"
label_contains: "enthält"
label_date: "Datum"
label_descending: "Absteigend"
label_description: "Beschreibung"
label_equals: "ist"
label_expand: "Aufklappen"
label_expanded: "aufgeklappt"
label_expand_all: "Alle aufklappen"
label_filename: "Datei"
label_filesize: "Größe"
label_format_atom: "Atom"
label_format_csv: "CSV"
label_format_pdf: "PDF"

@ -62,6 +62,7 @@ en:
label_ascending: "Ascending"
label_board_locked: "Locked"
label_board_sticky: "Sticky"
label_date: "Date"
label_descending: "Descending"
label_description: "Description"
label_closed_work_packages: "closed"
@ -74,6 +75,8 @@ en:
label_expand: "Expand"
label_expanded: "expanded"
label_expand_all: "Expand all"
label_filename: "File"
label_filesize: "Size"
label_format_atom: "Atom"
label_format_csv: "CSV"
label_format_pdf: "PDF"

@ -133,6 +133,8 @@ sys_api_enabled:
default: 0
sys_api_key:
default: ''
repository_authentication_caching_enabled:
default: 1
commit_ref_keywords:
default: 'refs,references,IssueID'
commit_fix_keywords:

@ -68,7 +68,7 @@ module Migration::Utils
journal_ids.each do |journal_id|
insert <<-SQL
INSERT INTO customizable_journals (journal_id, custom_field_id, value)
VALUES (#{journal_id}, #{m.custom_field_id}, '#{m.value}')
VALUES (#{journal_id}, #{m.custom_field_id}, #{quote_value(m.value)})
SQL
end
end

@ -103,6 +103,8 @@ See doc/COPYRIGHT.rdoc for more details.
* Fix: Asset require for plug-ins
* Fix: at.who styling
* `#1030` Fix: New target version cannot be created from work package view
## 3.0.8
* new version scheme

@ -0,0 +1,175 @@
package Apache::Authn::OpenProject;
=head1 Apache::Authn::OpenProject
OpenProject - a mod_perl module to authenticate webdav subversion users
against an OpenProject web service
=head1 SYNOPSIS
This module allow anonymous users to browse public project and
registred users to browse and commit their project. Authentication is
done against an OpenProject web service.
=head1 INSTALLATION
For this to automagically work, you need to have a recent reposman.rb.
Sorry ruby users but you need some perl modules, at least mod_perl2 and apache2-svn.
On debian/ubuntu you must do :
aptitude install libapache2-mod-perl2 libapache2-svn
=head1 CONFIGURATION
## This module has to be in your perl path
## eg: /usr/lib/perl5/Apache/Authn/OpenProjectAuthentication.pm
PerlLoadModule Apache::Authn::OpenProjectAuthentication
<Location /svn>
DAV svn
SVNParentPath "/var/svn"
AuthType Basic
AuthName OpenProject
Require valid-user
PerlAccessHandler Apache::Authn::OpenProject::access_handler
PerlAuthenHandler Apache::Authn::OpenProject::authen_handler
OpenProjectUrl "http://example.com/openproject/"
OpenProjectApiKey "<API key>"
</Location>
To be able to browse repository inside openproject, you must add something
like that :
<Location /svn-private>
DAV svn
SVNParentPath "/var/svn"
Order deny,allow
Deny from all
# only allow reading orders
<Limit GET PROPFIND OPTIONS REPORT>
Allow from openproject.server.ip
</Limit>
</Location>
and you will have to use this reposman.rb command line to create repository :
reposman.rb --openproject my.openproject.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/
=cut
use strict;
use warnings FATAL => 'all', NONFATAL => 'redefine';
use Digest::SHA;
use Apache2::Module;
use Apache2::Access;
use Apache2::ServerRec qw();
use Apache2::RequestRec qw();
use Apache2::RequestUtil qw();
use Apache2::Const qw(:common :override :cmd_how);
use APR::Pool ();
use APR::Table ();
use HTTP::Request::Common qw(POST);
use LWP::UserAgent;
# use Apache2::Directive qw();
my @directives = (
{
name => 'OpenProjectUrl',
req_override => OR_AUTHCFG,
args_how => TAKE1,
errmsg => 'URL of your (local) OpenProject. (e.g. http://localhost/ or http://www.example.com/openproject/)',
},
{
name => 'OpenProjectApiKey',
req_override => OR_AUTHCFG,
args_how => TAKE1,
},
);
sub OpenProjectUrl { set_val('OpenProjectUrl', @_); }
sub OpenProjectApiKey { set_val('OpenProjectApiKey', @_); }
sub trim {
my $string = shift;
$string =~ s/\s{2,}/ /g;
return $string;
}
sub set_val {
my ($key, $self, $parms, $arg) = @_;
$self->{$key} = $arg;
}
Apache2::Module::add(__PACKAGE__, \@directives);
sub access_handler {
my $r = shift;
unless ($r->some_auth_required) {
$r->log_reason("No authentication has been configured");
return FORBIDDEN;
}
return OK
}
sub authen_handler {
my $r = shift;
my ($status, $password) = $r->get_basic_auth_pw();
my $login = $r->user;
return $status unless $status == OK;
my $identifier = get_project_identifier($r);
my $method = $r->method;
if( is_access_allowed( $login, $password, $identifier, $method, $r ) ) {
return OK;
} else {
$r->note_auth_failure();
return AUTH_REQUIRED;
}
}
# we send a request to the openproject sys api
# and use the user's given login and password for basic auth
# for accessing the openproject sys api an api key is needed
sub is_access_allowed {
my $login = shift;
my $password = shift;
my $identifier = shift;
my $method = shift;
my $r = shift;
my $cfg = Apache2::Module::get_config( __PACKAGE__, $r->server, $r->per_dir_config );
my $key = $cfg->{OpenProjectApiKey};
my $openproject_url = $cfg->{OpenProjectUrl} . '/sys/repo_auth';
my $openproject_req = POST $openproject_url , [ repository => $identifier, key => $key, method => $method ];
$openproject_req->authorization_basic( $login, $password );
my $ua = LWP::UserAgent->new;
my $response = $ua->request($openproject_req);
return $response->is_success();
}
sub get_project_identifier {
my $r = shift;
my $location = $r->location;
my ($identifier) = $r->uri =~ m{$location/*([^/]+)};
$identifier;
}
1;

@ -1,52 +0,0 @@
-- -- copyright
-- OpenProject is a project management system.
-- Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
--
-- This program is free software; you can redistribute it and/or
-- modify it under the terms of the GNU General Public License version 3.
--
-- OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
-- Copyright (C) 2006-2013 Jean-Philippe Lang
-- Copyright (C) 2010-2013 the ChiliProject Team
--
-- This program is free software; you can redistribute it and/or
-- modify it under the terms of the GNU General Public License
-- as published by the Free Software Foundation; either version 2
-- of the License, or (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program; if not, write to the Free Software
-- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
--
-- See doc/COPYRIGHT.rdoc for more details.
-- ++
/* ssh views */
CREATE OR REPLACE VIEW ssh_users as
select login as username, hashed_password as password
from users
where status = 1;
/* nss views */
CREATE OR REPLACE VIEW nss_groups AS
select identifier AS name, (id + 5000) AS gid, 'x' AS password
from projects;
CREATE OR REPLACE VIEW nss_users AS
select login AS username, CONCAT_WS(' ', firstname, lastname) as realname, (id + 5000) AS uid, 'x' AS password
from users
where status = 1;
CREATE OR REPLACE VIEW nss_grouplist AS
select (members.project_id + 5000) AS gid, users.login AS username
from users, members
where users.id = members.user_id
and users.status = 1;

@ -31,13 +31,16 @@
require 'optparse'
require 'find'
require 'etc'
require 'json'
require 'net/http'
require 'uri'
Version = "1.3"
Version = "1.4"
SUPPORTED_SCM = %w( Subversion Git Filesystem )
$verbose = 0
$quiet = false
$redmine_host = ''
$openproject_host = ''
$repos_base = ''
$svn_owner = 'root'
$svn_group = 'root'
@ -85,56 +88,57 @@ OptionParser.new do |opts|
opts.separator("Manages your repositories with OpenProject.")
opts.separator("")
opts.separator("Required arguments:")
opts.on("-s", "--svn-dir DIR", "use DIR as base directory for svn repositories") {|v| $repos_base = v}
opts.on("-r", "--redmine-host HOST", "assume Redmine is hosted on HOST. Examples:",
" -r redmine.example.net",
" -r http://redmine.example.net",
" -r https://redmine.example.net") {|v| $redmine_host = v}
opts.on("-k", "--key KEY", "use KEY as the Redmine API key") {|v| $api_key = v}
opts.on("-s", "--svn-dir DIR", "use DIR as base directory for svn repositories") {|v| $repos_base = v}
opts.on("-r", "--openproject-host HOST", "assume OpenProject is hosted on HOST. Examples:",
" -r openproject.example.net",
" -r http://openproject.example.net",
" -r https://openproject.example.net") {|v| $openproject_host = v}
opts.on('', "--redmine-host HOST", "DEPRECATED: please use --openproject-host instead") {|v| $openproject_host = v}
opts.on("-k", "--key KEY", "use KEY as the OpenProject API key") {|v| $api_key = v}
opts.separator("")
opts.separator("Options:")
opts.on("-o", "--owner OWNER", "owner of the repository. using the rails login",
"allows users to browse the repository within",
"Redmine even for private projects. If you want to",
"share repositories through Redmine.pm, you need",
"to use the apache owner.") {|v| $svn_owner = v; $use_groupid = false}
opts.on("-g", "--group GROUP", "group of the repository (default: root)") {|v| $svn_group = v; $use_groupid = false}
opts.on( "--public-mode MODE", "file mode for new public repositories (default: 0775)") {|v| $public_mode = v}
opts.on( "--private-mode MODE", "file mode for new private repositories (default: 0770)") {|v| $private_mode = v}
opts.on( "--scm SCM", "the kind of SCM repository you want to create",
"(and register) in Redmine (default: Subversion).",
"reposman is able to create Git and Subversion",
"repositories.",
"For all other kind, you must specify a --command",
"option") {|v| v.capitalize; log("Invalid SCM: #{v}", :exit => true) unless SUPPORTED_SCM.include?(v)}
opts.on("-u", "--url URL", "the base url Redmine will use to access your",
"repositories. This option is used to automatically",
"register the repositories in Redmine. The project ",
"identifier will be appended to this url.",
"Examples:",
" -u https://example.net/svn",
" -u file:///var/svn/",
"if this option isn't set, reposman won't register",
"the repositories in Redmine") {|v| $svn_url = v}
opts.on("-c", "--command COMMAND", "use this command instead of 'svnadmin create' to",
"create a repository. This option can be used to",
"create repositories other than subversion and git",
"kind.",
"This command override the default creation for git",
"and subversion.") {|v| $command = v}
opts.on("-f", "--force", "force repository creation even if the project",
"repository is already declared in Redmine") {$force = true}
opts.on("-t", "--test", "only show what should be done") {$test = true}
opts.on("-h", "--help", "show help and exit") {puts opts; exit 1}
opts.on("-v", "--verbose", "verbose") {$verbose += 1}
opts.on("-V", "--version", "print version and exit") {puts Version; exit}
opts.on("-q", "--quiet", "no log") {$quiet = true}
opts.on("-o", "--owner OWNER", "owner of the repository. using the rails login",
"allows users to browse the repository within",
"OpenProject even for private projects. If you want to",
"share repositories through OpenProject.pm, you need",
"to use the apache owner.") {|v| $svn_owner = v; $use_groupid = false}
opts.on("-g", "--group GROUP", "group of the repository (default: root)") {|v| $svn_group = v; $use_groupid = false}
opts.on( "--public-mode MODE", "file mode for new public repositories (default: 0775)") {|v| $public_mode = v}
opts.on( "--private-mode MODE", "file mode for new private repositories (default: 0770)") {|v| $private_mode = v}
opts.on( "--scm SCM", "the kind of SCM repository you want to create",
"(and register) in OpenProject (default: Subversion).",
"reposman is able to create Git and Subversion",
"repositories.",
"For all other kind, you must specify a --command",
"option") {|v| v.capitalize; log("Invalid SCM: #{v}", :exit => true) unless SUPPORTED_SCM.include?(v)}
opts.on("-u", "--url URL", "the base url OpenProject will use to access your",
"repositories. This option is used to automatically",
"register the repositories in OpenProject. The project ",
"identifier will be appended to this url.",
"Examples:",
" -u https://example.net/svn",
" -u file:///var/svn/",
"if this option isn't set, reposman won't register",
"the repositories in OpenProject") {|v| $svn_url = v}
opts.on("-c", "--command COMMAND", "use this command instead of 'svnadmin create' to",
"create a repository. This option can be used to",
"create repositories other than subversion and git",
"kind.",
"This command override the default creation for git",
"and subversion.") {|v| $command = v}
opts.on("-f", "--force", "force repository creation even if the project",
"repository is already declared in OpenProject") {$force = true}
opts.on("-t", "--test", "only show what should be done") {$test = true}
opts.on("-h", "--help", "show help and exit") {puts opts; exit 1}
opts.on("-v", "--verbose", "verbose") {$verbose += 1}
opts.on("-V", "--version", "print version and exit") {puts Version; exit}
opts.on("-q", "--quiet", "no log") {$quiet = true}
opts.separator("")
opts.separator("Examples:")
opts.separator(" reposman.rb --svn-dir=/var/svn --redmine-host=redmine.example.net --scm Subversion")
opts.separator(" reposman.rb -s /var/git -r redmine.example.net -u http://svn.example.net --scm Git")
opts.separator(" reposman.rb --svn-dir=/var/svn --openproject-host=openproject.example.net --scm Subversion")
opts.separator(" reposman.rb -s /var/git -r openproject.example.net -u http://svn.example.net --scm Git")
opts.separator("")
opts.separator("You can find more information on the redmine's wiki:\nhttp://www.redmine.org/projects/redmine/wiki/HowTos")
opts.separator("You might find more information on the openproject's wiki:\nhttps://www.openproject.org/projects/openproject/wiki/Support")
end.parse!
if $test
@ -152,7 +156,7 @@ end
$svn_url += "/" if $svn_url and not $svn_url.match(/\/$/)
if ($redmine_host.empty? or $repos_base.empty?)
if ($openproject_host.empty? or $repos_base.empty?)
puts "Required argument missing. Type 'reposman.rb --help' for usage."
exit 1
end
@ -161,28 +165,22 @@ unless File.directory?($repos_base)
log("directory '#{$repos_base}' doesn't exists", :exit => true)
end
begin
require 'active_resource'
rescue LoadError
log("This script requires activeresource.\nRun 'gem install activeresource' to install it.", :exit => true)
end
class Project < ActiveResource::Base
self.headers["User-agent"] = "Redmine repository manager/#{Version}"
end
log("querying Redmine for projects...", :level => 1);
log("querying OpenProject for projects...", :level => 1);
$redmine_host.gsub!(/^/, "http://") unless $redmine_host.match("^https?://")
$redmine_host.gsub!(/\/$/, '')
$openproject_host.gsub!(/^/, "http://") unless $openproject_host.match("^https?://")
$openproject_host.gsub!(/\/$/, '')
Project.site = "#{$redmine_host}/sys";
api_uri = URI.parse("#{$openproject_host}/sys")
http = Net::HTTP.new(api_uri.host, api_uri.port)
http.use_ssl = (api_uri.scheme == 'https')
http_headers = {'User-Agent' => "OpenProject-Repository-Manager/#{Version}"}
begin
# Get all active projects that have the Repository module enabled
projects = Project.find(:all, :params => {:key => $api_key})
response = http.get("#{api_uri.path}/projects.json?key=#{$api_key}", http_headers)
projects = JSON.parse(response.body)
rescue => e
log("Unable to connect to #{Project.site}: #{e}", :exit => true)
log("Unable to connect to #{$openproject_host}: #{e}", :exit => true)
end
if projects.nil?
@ -195,8 +193,8 @@ def set_owner_and_rights(project, repos_path, &block)
if mswin?
yield if block_given?
else
uid, gid = Etc.getpwnam($svn_owner).uid, ($use_groupid ? Etc.getgrnam(project.identifier).gid : Etc.getgrnam($svn_group).gid)
right = project.is_public ? $public_mode : $private_mode
uid, gid = Etc.getpwnam($svn_owner).uid, ($use_groupid ? Etc.getgrnam(project['identifier']).gid : Etc.getgrnam($svn_group).gid)
right = project['is_public'] ? $public_mode : $private_mode
right = right.to_i(8) & 007777
yield if block_given?
Find.find(repos_path) do |f|
@ -221,17 +219,17 @@ def mswin?
end
projects.each do |project|
log("treating project #{project.name}", :level => 1)
log("treating project #{project['name']}", :level => 1)
if project.identifier.empty?
log("\tno identifier for project #{project.name}")
if project['identifier'].empty?
log("\tno identifier for project #{project['name']}")
next
elsif not project.identifier.match(/^[a-z0-9\-_]+$/)
log("\tinvalid identifier for project #{project.name} : #{project.identifier}");
elsif not project['identifier'].match(/^[a-z0-9\-_]+$/)
log("\tinvalid identifier for project #{project['name']} : #{project['identifier']}");
next;
end
repos_path = File.join($repos_base, project.identifier).gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
repos_path = File.join($repos_base, project['identifier']).gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
if File.directory?(repos_path)
@ -239,7 +237,7 @@ projects.each do |project|
# rights before leaving
other_read = other_read_right?(repos_path)
owner = owner_name(repos_path)
next if project.is_public == other_read and owner == $svn_owner
next if project['is_public'] == other_read and owner == $svn_owner
if $test
log("\tchange mode on #{repos_path}")
@ -256,18 +254,18 @@ projects.each do |project|
log("\tmode change on #{repos_path}");
else
# if repository is already declared in redmine, we don't create
# if repository is already declared in openproject, we don't create
# unless user use -f with reposman
if $force == false and project.respond_to?(:repository)
log("\trepository for project #{project.identifier} already exists in Redmine", :level => 1)
if $force == false and project.has_key?('repository')
log("\trepository for project #{project['identifier']} already exists in OpenProject", :level => 1)
next
end
project.is_public ? File.umask(0002) : File.umask(0007)
project['is_public'] ? File.umask(0002) : File.umask(0007)
if $test
log("\tcreate repository #{repos_path}")
log("\trepository #{repos_path} registered in Redmine with url #{$svn_url}#{project.identifier}") if $svn_url;
log("\trepository #{repos_path} registered in OpenProject with url #{$svn_url}#{project['identifier']}") if $svn_url;
next
end
@ -286,10 +284,13 @@ projects.each do |project|
if $svn_url
begin
project.post(:repository, :vendor => $scm, :repository => {:url => "#{$svn_url}#{project.identifier}"}, :key => $api_key)
log("\trepository #{repos_path} registered in Redmine with url #{$svn_url}#{project.identifier}");
http.post("#{api_uri.path}/projects/#{project['identifier']}/repository.json?" +
"vendor=#{$scm}&repository[url]=#{$svn_url}#{project['identifier']}&key=#{$api_key}",
"", # empty data
http_headers)
log("\trepository #{repos_path} registered in OpenProject with url #{$svn_url}#{project['identifier']}");
rescue => e
log("\trepository #{repos_path} not registered in Redmine: #{e.message}");
log("\trepository #{repos_path} not registered in OpenProject: #{e.message}");
end
end

@ -51,7 +51,8 @@ describe('WorkPackageDetailsController', function() {
},
embedded: {
activities: [],
watchers: []
watchers: [],
attachments: []
},
links: {
availableWatchers: {
@ -89,6 +90,9 @@ describe('WorkPackageDetailsController', function() {
},
UserService: UserService,
CustomFieldHelper: CustomFieldHelper,
WorkPackagesDetailsHelper: {
attachmentsTitle: function() { return ''; }
},
workPackage: buildWorkPackageWithId(workPackageId),
});

@ -0,0 +1,72 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
describe('attachmentTitleCell Directive', function() {
var compile, element, rootScope, scope;
beforeEach(angular.mock.module('openproject.workPackages.directives'));
beforeEach(module('templates'));
beforeEach(inject(function($rootScope, $compile) {
var html;
html = '<td attachment-title-cell attachment="attachment"></td>';
element = angular.element(html);
rootScope = $rootScope;
scope = $rootScope.$new();
compile = function() {
$compile(element)(scope);
scope.$digest();
};
}));
describe('element', function() {
beforeEach(function() {
scope.attachment = {
props: {
id: 1,
fileName: 'hearmi.now',
fileSize: '12340'
}
};
compile();
});
it('should render element', function() {
expect(element.prop('tagName')).to.equal('TD');
});
it('should render link to attachment', function() {
var link = element.find('a');
expect(link.text()).to.equal('hearmi.now (12.34kB)');
expect(link.attr('href')).to.equal('/attachments/1/hearmi.now');
});
});
});

@ -0,0 +1,79 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
describe('attachmentUserCell Directive', function() {
var compile, element, rootScope, scope;
beforeEach(angular.mock.module('openproject.workPackages.directives'));
beforeEach(module('templates'));
beforeEach(inject(function($rootScope, $compile) {
var html;
html = '<td attachment-user-cell attachment="attachment"></td>';
element = angular.element(html);
rootScope = $rootScope;
scope = $rootScope.$new();
compile = function() {
$compile(element)(scope);
scope.$digest();
};
}));
describe('element', function() {
var userName = 'Big Phil Scolari';
var userId = 5;
beforeEach(inject(function($q) {
scope.attachment = {
links: {
author: {
fetch: function() {
deferred = $q.defer();
deferred.resolve({ props: { id: userId, name: userName} } );
return deferred.promise;
}
}
}
};
compile();
}));
it('should render element', function() {
expect(element.prop('tagName')).to.equal('TD');
});
it('should render link to user', function() {
var link = element.find('a');
expect(link.text()).to.equal(userName);
expect(link.attr('href')).to.equal('/users/' + userId);
});
});
});

@ -0,0 +1,67 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
describe('attachmentsTitle Directive', function() {
var compile, element, rootScope, scope;
beforeEach(angular.mock.module('openproject.workPackages.directives'));
beforeEach(module('templates'));
beforeEach(inject(function($rootScope, $compile) {
var html;
html = '<attachments-title attachments="attachments"></attachments-title>';
element = angular.element(html);
rootScope = $rootScope;
scope = $rootScope.$new();
compile = function() {
$compile(element)(scope);
scope.$digest();
};
}));
describe('element', function() {
beforeEach(function() {
scope.attachments = [
{ filename: 'bomba' },
{ filename: 'clat' }
];
compile();
});
it('should render element', function() {
expect(element.prop('tagName')).to.equal('H3');
});
it('should render title', function() {
expect(element.text()).to.equal('Attachments (2)');
});
});
});

@ -138,6 +138,11 @@ module API
work_package.journals.map{ |journal| ::API::V3::Activities::ActivityModel.new(journal) }
end
def attachments
work_package.attachments
.map{ |attachment| ::API::V3::Attachments::AttachmentModel.new(attachment) }
end
def watchers
work_package.watcher_users
.order(User::USER_FORMATS_STRUCTURE[Setting.user_format])

@ -136,6 +136,7 @@ module API
collection :activities, embedded: true, class: ::API::V3::Activities::ActivityModel, decorator: ::API::V3::Activities::ActivityRepresenter
property :watchers, embedded: true, exec_context: :decorator
# collection :watchers, embedded: true, class: ::API::V3::Users::UserModel, decorator: ::API::V3::Users::UserRepresenter
collection :attachments, embedded: true, class: ::API::V3::Attachments::AttachmentModel, decorator: ::API::V3::Attachments::AttachmentRepresenter
def _type
'WorkPackage'

@ -1,4 +1,4 @@
#!/usr/bin/perl
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
@ -27,13 +27,9 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
# modify to suit your repository base
my $repos_base = '/var/svn';
my $path = '/usr/bin/';
my %kwown_commands = map { $_ => 1 } qw/svnserve/;
umask 0002;
exec ('/usr/bin/svnserve', '-r', $repos_base, '-t');
module OpenProject
module RepositoryAuthentication
CACHE_PREFIX = "openproject/repository_authentication/login_"
CACHE_EXPIRES_AFTER = 10.minutes
end
end

@ -40,9 +40,9 @@ module Redmine
attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym
attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
has_many :attachments, options.merge(:as => :container,
:order => "#{Attachment.table_name}.created_on",
:dependent => :destroy)
has_many :attachments, options.reverse_merge!(:as => :container,
:order => "#{Attachment.table_name}.created_on",
:dependent => :destroy)
attr_accessor :unsaved_attachments
after_initialize :initialize_unsaved_attachments
send :include, Redmine::Acts::Attachable::InstanceMethods

@ -83,12 +83,10 @@ module Redmine::MenuManager::TopMenuHelper
def render_user_top_menu_node(items = menu_items_for(:account_menu))
if User.current.logged?
render_user_drop_down items
elsif Concerns::OmniauthLogin.direct_login?
render_direct_login
else
if OmniauthLogin.direct_login?
render_direct_login
else
render_login_drop_down
end
render_login_drop_down
end
end

@ -17,11 +17,10 @@
ui-sref-active="selected">
<a href ng-bind="I18n.t('js.work_packages.tabs.watchers')"/>
</li>
<!--
<li ui-sref="work-packages.list.details.attachments({})"
ui-sref-active="selected">
<a href ng-bind="I18n.t('js.work_packages.tabs.attachments')"/>
</li> -->
</li>
</ul>
</div>

@ -0,0 +1 @@
<a ng-href="{{ attachmentPath }}" ng-bind="displayTitle"></a>

@ -0,0 +1 @@
<a ng-href="{{ userPath }}" ng-bind="authorName"></a>

@ -0,0 +1,19 @@
<table ng-if="attachments.length">
<tbody>
<tr>
<th>{{ I18n.t('js.label_filename')}}</th>
<th>{{ I18n.t('js.label_filesize')}}</th>
<th>{{ I18n.t('js.label_description')}}</th>
<th>{{ I18n.t('js.label_date')}}</th>
<th></th>
</tr>
<tr ng-repeat="attachment in attachments"
attachment="attachment">
<td attachment-title-cell attachment="attachment"></td>
<td attachment-user-cell attachment="attachment"></td>
<td>{{ attachment.props.description }}</td>
<td><date-time date-value="attachment.props.createdAt"></date></td>
</tr>
</tbody>
</table>

@ -1,6 +1,8 @@
<div class="detail-panel-description">
<h3>Attachments</h3>
<attachments-title attachments="attachments"></attachments-title>
<div class="detail-panel-description-content">
...
<div class="attachments-container">
<attachments-table attachments="attachments"></attachments-table>
</div>
</div>
</div>

@ -158,7 +158,7 @@ describe AccountController do
describe '#login with omniauth_direct_login enabled' do
before do
OmniauthLogin.stub(:direct_login_provider).and_return('some_provider')
Concerns::OmniauthLogin.stub(:direct_login_provider).and_return('some_provider')
end
describe 'GET' do

@ -86,6 +86,16 @@ describe Api::Experimental::WorkPackagesController do
get 'index', format: 'xml'
expect(response).to render_template('api/experimental/work_packages/index', formats: %w(api))
end
it 'assigns a query which has the default filter arguments set' do
expected_query = Query.new
expect(Query).to receive(:new).with(anything, initialize_with_default_filter: true)
.and_return(expected_query)
get 'index', format: 'xml'
expect(assigns(:query)).to eql expected_query
end
end
context 'with work packages' do

@ -0,0 +1,229 @@
require 'spec_helper'
module OpenProjectRepositoryAuthenticationSpecs
describe SysController do
let(:commit_role) { FactoryGirl.create(:role, :permissions => [:commit_access,
:browse_repository]) }
let(:browse_role) { FactoryGirl.create(:role, :permissions => [:browse_repository]) }
let(:guest_role) { FactoryGirl.create(:role, :permissions => []) }
let(:valid_user_password) { "Top Secret Password" }
let(:valid_user) { FactoryGirl.create(:user, :login => "johndoe",
:password => valid_user_password,
:password_confirmation => valid_user_password)}
before(:each) do
FactoryGirl.create(:non_member, :permissions => [:browse_repository])
DeletedUser.first # creating it first in order to avoid problems with should_receive
random_project = FactoryGirl.create(:project, :is_public => false)
@member = FactoryGirl.create(:member, :user => valid_user,
:roles => [browse_role],
:project => random_project)
Setting.stub(:sys_api_key).and_return("12345678")
Setting.stub(:sys_api_enabled?).and_return(true)
Setting.stub(:repository_authentication_caching_enabled?).and_return(true)
end
describe :repo_auth, "for valid login, but no access to repo_auth" do
before(:each) do
@key = Setting.sys_api_key
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(valid_user.login, valid_user_password)
post "repo_auth", { :key => @key, :repository => "without-access", :method => "GET" }
end
it "should respond 403 not allowed" do
response.code.should == "403"
response.body.should == "Not allowed"
end
end
describe :repo_auth, "for valid login and user has browse repository permission (role reporter) for project" do
before(:each) do
@key = Setting.sys_api_key
@project = FactoryGirl.create(:project, :is_public => false)
@member = FactoryGirl.create(:member, :user => valid_user,
:roles => [browse_role],
:project => @project)
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(valid_user.login, valid_user_password)
end
it "should respond 200 okay dokay for GET" do
post "repo_auth", { :key => @key, :repository => @project.identifier, :method => "GET" }
response.code.should == "200"
end
it "should respond 403 not allowed for POST" do
post "repo_auth", { :key => @key, :repository => @project.identifier, :method => "POST" }
response.code.should == "403"
end
end
describe :repo_auth, "for valid login and user has commit access permission (role developer) for project" do
before(:each) do
@key = Setting.sys_api_key
@project = FactoryGirl.create(:project, :is_public => false)
@member = FactoryGirl.create(:member, :user => valid_user,
:roles => [commit_role],
:project => @project )
valid_user.save
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(valid_user.login, valid_user_password)
end
it "should respond 200 okay dokay for GET" do
post "repo_auth", { :key => @key, :repository => @project.identifier, :method => "GET" }
response.code.should == "200"
end
it "should respond 200 okay dokay for POST" do
post "repo_auth", { :key => @key, :repository => @project.identifier, :method => "POST" }
response.code.should == "200"
end
end
describe :repo_auth, "for invalid login and user has role manager for project" do
before(:each) do
@key = Setting.sys_api_key
@project = FactoryGirl.create(:project, :is_public => false )
@member = FactoryGirl.create(:member, :user => valid_user,
:roles => [commit_role],
:project => @project)
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(valid_user.login, valid_user_password + "made invalid")
post "repo_auth", { :key => @key, :repository => @project.identifier, :method => "GET" }
end
it "should respond 401 auth required" do
response.code.should == "401"
end
end
describe :repo_auth, "for valid login and user is not member for project" do
before(:each) do
@key = Setting.sys_api_key
@project = FactoryGirl.create(:project, :is_public => false)
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(valid_user.login, valid_user_password)
post "repo_auth", { :key => @key, :repository => @project.identifier, :method => "GET" }
end
it "should respond 403 not allowed" do
response.code.should == "403"
end
end
describe :repo_auth, "for valid login and project is public" do
before(:each) do
@key = Setting.sys_api_key
@project = FactoryGirl.create(:project, :is_public => true)
random_project = FactoryGirl.create(:project, :is_public => false)
@member = FactoryGirl.create(:member, :user => valid_user,
:roles => [browse_role],
:project => random_project)
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(valid_user.login, valid_user_password)
post "repo_auth", { :key => @key, :repository => @project.identifier, :method => "GET" }
end
it "should respond 200 OK" do
response.code.should == "200"
end
end
describe :repo_auth, "for invalid credentials" do
before(:each) do
@key = Setting.sys_api_key
post "repo_auth", { :key => @key, :repository => "any-repo", :method => "GET" }
end
it "should respond 401 auth required" do
response.code.should == "401"
response.body.should == "Authorization required"
end
end
describe :repo_auth, "for invalid api key" do
before(:each) do
@key = "invalid"
end
it "should respond 403 for valid username/password" do
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(valid_user.login, valid_user_password)
post "repo_auth", { :key => @key, :repository => "any-repo", :method => "GET" }
response.code.should == "403"
response.body.should == "Access denied. Repository management WS is disabled or key is invalid."
end
it "should respond 403 for invalid username/password" do
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials("invalid", "invalid")
post "repo_auth", { :key => @key, :repository => "any-repo", :method => "GET" }
response.code.should == "403"
response.body.should == "Access denied. Repository management WS is disabled or key is invalid."
end
end
before(:each) do
Rails.cache.clear
Rails.cache.stub(:kind_of?).with(anything).and_return(false)
end
describe :cached_user_login do
let(:cache_key) { OpenProject::RepositoryAuthentication::CACHE_PREFIX +
Digest::SHA1.hexdigest("#{valid_user.login}#{valid_user_password}") }
let(:cache_expiry) { OpenProject::RepositoryAuthentication::CACHE_EXPIRES_AFTER }
it "should call user_login only once when called twice" do
controller.should_receive(:user_login).once.and_return(valid_user)
2.times { controller.send(:cached_user_login, valid_user.login, valid_user_password) }
end
it "should return the same as user_login for valid creds" do
controller.send(:cached_user_login, valid_user.login, valid_user_password).should ==
controller.send(:user_login, valid_user.login, valid_user_password)
end
it "should return the same as user_login for invalid creds" do
controller.send(:cached_user_login, "invalid", "invalid").should ==
controller.send(:user_login, "invalid", "invalid")
end
it "should use cache" do
# allow the cache to return something reasonable for
# other requests, while ensuring that it is not queried
# with the cache key in question
# unfortunately, and_call_original currently fails
Rails.cache.stub(:fetch) do |*args|
args.first.should_not == cache_key
name = args.first.split("/").last
Marshal.dump(Setting.send(:find_or_default, name).value)
end
#Rails.cache.should_receive(:fetch).with(anything).and_call_original
Rails.cache.should_receive(:fetch).with(cache_key, :expires_in => cache_expiry) \
.and_return(Marshal.dump(valid_user.id.to_s))
controller.send(:cached_user_login, valid_user.login, valid_user_password)
end
describe "with caching disabled" do
before do
Setting.stub(:repository_authentication_caching_enabled?).and_return(false)
end
it 'should not use a cache' do
# allow the cache to return something reasonable for
# other requests, while ensuring that it is not queried
# with the cache key in question
#
# unfortunately, and_call_original currently fails
Rails.cache.stub(:fetch) do |*args|
args.first.should_not == cache_key
name = args.first.split("/").last
Marshal.dump(Setting.send(:find_or_default, name).value)
end
controller.send(:cached_user_login, valid_user.login, valid_user_password)
end
end
end
end
end

@ -159,6 +159,11 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do
it { should have_json_type(Array).at_path('_embedded/activities') }
it { should have_json_size(0).at_path('_embedded/activities') }
end
describe 'attachments' do
it { should have_json_type(Array).at_path('_embedded/attachments') }
it { should have_json_size(0).at_path('_embedded/attachments') }
end
end
end
end

@ -56,7 +56,7 @@ describe "layouts/base" do
context 'with omni_auth_direct_login enabled' do
before do
expect(OmniauthLogin).to receive(:direct_login_provider).and_return('some_provider')
expect(Concerns::OmniauthLogin).to receive(:direct_login_provider).and_return('some_provider')
render
end

Loading…
Cancel
Save