Merge remote-tracking branch 'origin/release/6.1' into dev

pull/5125/merge
Oliver Günther 8 years ago
commit 1768b82934
No known key found for this signature in database
GPG Key ID: 88872239EB414F99
  1. 133
      CONTRIBUTING.md
  2. 4
      Gemfile.lock
  3. 4
      app/assets/stylesheets/_misc_legacy.sass
  4. 4
      app/assets/stylesheets/content/_attributes_table.sass
  5. 7
      app/assets/stylesheets/content/_tables.sass
  6. 1
      app/assets/stylesheets/content/_tabs.sass
  7. 13
      app/contracts/users/base_contract.rb
  8. 14
      app/contracts/users/create_contract.rb
  9. 5
      app/models/auth_source.rb
  10. 2
      app/models/category.rb
  11. 5
      app/models/ldap_auth_source.rb
  12. 46
      app/models/mixins/unique_finder.rb
  13. 1
      app/models/queries/users.rb
  14. 38
      app/models/queries/users/filters/login_filter.rb
  15. 13
      app/models/queries/users/filters/status_filter.rb
  16. 4
      app/models/query/results.rb
  17. 5
      app/models/user.rb
  18. 3
      app/models/work_package.rb
  19. 1
      app/views/ldap_auth_sources/_form.html.erb
  20. 2
      app/views/projects/form/_custom_fields.html.erb
  21. 8
      app/views/types/_form.html.erb
  22. 2
      config/locales/en.yml
  23. 34
      db/migrate/20161219134700_add_attr_admin_to_ldap.rb
  24. 58
      doc/apiv3/endpoints/users.apib
  25. 36
      frontend/app/components/common/filters/html-escape.filter.ts
  26. 2
      frontend/app/components/wp-panels/watchers-panel/watchers-panel.directive.html
  27. 2
      frontend/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.directive.ts
  28. 54
      lib/api/v3/users/user_representer.rb
  29. 2
      lib/api/v3/users/users_api.rb
  30. 2
      lib/api/v3/work_packages/work_package_collection_representer.rb
  31. 56
      lib/tasks/ldap.rake
  32. 4
      spec/controllers/settings_controller_spec.rb
  33. 8
      spec/features/support/components/typeahead.rb
  34. 17
      spec/features/work_packages/tabs/watcher_tab_spec.rb
  35. 6
      spec/models/queries/users/filters/status_filter_spec.rb
  36. 2
      spec/models/queries/users/user_query_spec.rb
  37. 15
      spec/models/user_spec.rb
  38. 52
      spec/models/work_package/work_package_visibility_spec.rb
  39. 123
      spec/requests/api/v3/user/create_user_resource_spec.rb
  40. 90
      spec/requests/api/v3/user/filters_spec.rb
  41. 16
      spec/requests/api/v3/user/user_resource_spec.rb

@ -1,17 +1,51 @@
OpenProject is an open source project and we encourage you to help us out.
For contributing to OpenProject, please read the following guidelines.
We are pleased that you are thinking about contributing to OpenProject! This guide details how to contribute to OpenProject in a way that is efficient and fun for everyone.
*Please also note that these rules should be acknowledged by everyone,
but repository contributors might occasionally deviate from them for practical purposes,
e.g. not fork the repo, but have a branch on the main repository.
This should however stay an exception.*
### Get in touch
## Contributors License Agreement
Please get in touch with us using our [develompment forum](https://community.openproject.com/projects/openproject/boards/7) or send us an email to info@openproject.org.
### Contributor Code of Conduct
As contributors and maintainers of this project, we pledge to respect all people
who contribute through reporting issues, posting feature requests,
updating documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for everyone,
regardless of level of experience, gender, gender identity and expression, sexual orientation,
disability, personal appearance, body size, race, age, or religion.
Examples of unacceptable behavior by participants include the use of sexual language
or imagery, derogatory comments or personal attacks, trolling, public or private harassment,
insults, or other unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits,
code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct.
Project maintainers who do not follow the Code of Conduct may be removed from the project team.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported
by opening an issue or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the
[Contributor Covenant](http:contributor-covenant.org),
version 1.0.0, available at
[http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
### Issue tracking and coordination
We use OpenProject for development coordination and roadmap planning. Please have a look at the following lists:
- [development timeline](https://community.openproject.com/projects/openproject/timelines/36)
- [product roadmap and release planning](https://community.openproject.com/projects/openproject/roadmap)
- [wish list](https://community.openproject.com/versions/26)
- [bug backlog](https://community.openproject.com/versions/136)
### Branching model
The main development branch for upcoming releases is `dev`.
If in doubt, create your pull request against `dev`.
All new features, gem updates and bugfixes for the upcoming release should go into the `dev` branch.
External contributors have to sign a CLA before contributing to OpenProject.
The [CLA can be found here](https://www.openproject.org/wp-content/uploads/2014/09/OPF-Contributor-License-Agreement_v.2.pdf)
and has to be filled out and sent to cla@openproject.org.
Additionally, a GPG signature has to be provided.
## Development flow
@ -52,29 +86,21 @@ git push origin <your feature branch>
If your pull request **does not contain a description** for what it does and what it's intentions are,
we will reject it.
If you are working on a specific work package from the [list](https://community.openproject.org/projects/openproject/work_packages?query_props=%7B%22c%22:%5B%22type%22,%22status%22,%22subject%22,%22assigned_to%22%5D,%22t%22:%22parent:desc%22,%22f%22:%5B%7B%22n%22:%22status_id%22,%22o%22:%22!%22,%22t%22:%22list_status%22,%22v%22:%5B%2217%22,%2223%22,%223%22,%2214%22,%226%22%5D%7D%5D,%22pa%22:1,%22pp%22:20%7D),
If you are working on a specific work package from the [list](https://community.openproject.com/projects/openproject/work_packages),
you may include a link to that work package in the description, so we can track your work.
We will then review your pull request.
The core contributor team will then review your pull request according to our [code review guideline](https://www.openproject.org/open-source/development-free-project-management-software/code-review-guideliness/).
Please note that you can add commits after the pull request has been created by pushing
to the branch in your fork.
## Translations
Beginning with OpenProject 4.2.0 the OpenProject core only holds the
english locales and all other locales are stored in
OpenProject-Translations. But since this plugin is hardwired in the
Gemfile, german and other locales are available again.
If you want to contribute to the localization of OpenProject and its
plugins you can do so on [Crowdin](https://crowdin.com/projects/opf).
Once a day we will fetch those locales and upload them to GitHub.
Once a day we fetch those locales and upload them to GitHub.
More on this topic can be found [in our blog](https://www.openproject.org/2015/07/10/help-translate-openproject-into-your-language/).
More on this topic can be found in our [blog post](https://www.openproject.org/help-translate-openproject-into-your-language/).
## Important notes
To ensure a smooth workflow for everyone, please take note of the following:
### Testing
@ -84,69 +110,30 @@ Pull requests will be verified by TravisCI as well,
but please run them locally as well and make sure they are green before creating your pull request.
We have a lot of pull requests coming in and it takes some time to run the complete suite for each one.
### Branching model
If you push to your branch in quick sucession, please consider stopping the associated Travis builds, as Travis will run for each commit. This is especially true if you force push to the branch.
Please also use `[ci skip]` in your commit message to suppress builds which are not necessary
(e.g. after fixing a typo in the `README`).
The main development branch for upcoming releases is `dev`.
If in doubt, create your pull request against `dev`.
All new features, gem updates and bugfixes for the upcoming release should go into the `dev` branch.
#### Bugs and hotfixes
Bugfixes for one of the actively supported versions of OpenProject
should be issued against the respective branch.
A fix for the current version (called "Hotfix" and the branch ideally being named `hotfix/XYZ`)
Bugfixes for one of the actively supported versions of OpenProject should be issued against the respective branch.
A fix for the current version (called "Hotfix" and the branch ideally being named `hotfix/XYZ`)
should target `release/*` and a fix for the former version
(called "Backport" and the branch ideally being named `backport/XYZ`)
should target `backport/*`. We will try to merge hotfixes into dev branch
but if that is no trivial task, we might ask you to create another PR for that.
#### Travis CI
If you push to your branch in quick sucession, please consider stopping the associated Travis builds,
as Travis will run for each commit. This is especially true if you force push to the branch.
Please also use `[ci skip]` in your commit message to suppress builds which are not necessary
(e.g. after fixing a typo in the `README`).
### Inactive pull requests
We want to keep the Pull request list as cleaned up as possible - we will aim close pull requests
after an **inactivity period of 72 hours** (no comments, no further pushes)
after an **inactivity period of 30 days** (no comments, no further pushes)
which are not labelled as `work in progress` by us.
### Issue tracking and coordination
We use OpenProject for development coordination - please have a look at
[the work packages list](https://community.openproject.org/projects/openproject/work_packages?query_props=%7B%22c%22:%5B%22type%22,%22status%22,%22subject%22,%22assigned_to%22%5D,%22t%22:%22parent:desc%22,%22f%22:%5B%7B%22n%22:%22status_id%22,%22o%22:%22!%22,%22t%22:%22list_status%22,%22v%22:%5B%2217%22,%2223%22,%223%22,%2214%22,%226%22%5D%7D%5D,%22pa%22:1,%22pp%22:20%7D)
for upcoming features and reported bugs.
### Contributor Code of Conduct
As contributors and maintainers of this project, we pledge to respect all people
who contribute through reporting issues, posting feature requests,
updating documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for everyone,
regardless of level of experience, gender, gender identity and expression, sexual orientation,
disability, personal appearance, body size, race, age, or religion.
Examples of unacceptable behavior by participants include the use of sexual language
or imagery, derogatory comments or personal attacks, trolling, public or private harassment,
insults, or other unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits,
code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct.
Project maintainers who do not follow the Code of Conduct may be removed from the project team.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported
by opening an issue or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the
[Contributor Covenant](http:contributor-covenant.org),
version 1.0.0, available at
[http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
### Get in touch
## Contributors License Agreement
If you want to get in touch with us, there is also a
[Gitter channel](https://gitter.im/opf/openproject) to talk to us directly.
Contributors have to sign a CLA before contributing to OpenProject.
The [CLA can be found here](https://www.openproject.org/wp-content/uploads/2015/08/Contributor-License-Agreement.pdf)
and has to be filled out and sent to info@openproject.org.

@ -60,10 +60,10 @@ GIT
GIT
remote: https://github.com/opf/openproject-translations.git
revision: 495075444d0227625ea064907a629197e107b3e9
revision: be911c234d54f5552d544bd16053f9d89a99df5d
branch: release/6.1
specs:
openproject-translations (6.1.2)
openproject-translations (6.1.4)
crowdin-api (~> 0.4.1)
mixlib-shellout (~> 2.1.0)
rails (~> 5.0.0)

@ -415,11 +415,11 @@ a.has-thumb
padding-left: 3px
/* Cut of text with '...' - working on all major browsers and IE6+
* not working for Firefox < 7 */
.ellipsis
.ellipsis,
.form--field.ellipsis .form--label
white-space: nowrap
overflow: hidden
text-overflow: ellipsis

@ -53,6 +53,10 @@
&.-hidden
display: none
&.-two-options
.attributes-table--option
width: 25%
.delete-item
cursor: pointer
float: right

@ -97,11 +97,8 @@ table
white-space: nowrap
transform: rotate(270deg)
position: absolute
top: 130px
left: -40px
transform-origin: center center
margin-top: 8px
font-size: 0.875rem
top: 235px
transform-origin: 0 0
text-transform: uppercase
font-weight: bold

@ -75,6 +75,7 @@
#content .tab-content
overflow-x: auto
overflow-y: hidden
div.tabs-buttons
position: absolute

@ -37,7 +37,12 @@ module Users
attribute :lastname
attribute :name
attribute :mail
attribute :status
attribute :auth_source_id
attribute :identity_url
attribute :password
validate :existing_auth_source
def initialize(user, current_user)
super(user)
@ -48,5 +53,11 @@ module Users
private
attr_reader :current_user
def existing_auth_source
if auth_source_id && AuthSource.find_by_unique(auth_source_id).nil?
errors.add :auth_source, :error_not_found
end
end
end
end

@ -32,11 +32,7 @@ require 'users/base_contract'
module Users
class CreateContract < BaseContract
validate :user_allowed_to_add
attribute :password do
# when user's are created as 'active', a password must be set
errors.add :password, :blank if model.active? && model.password.blank?
end
validate :authentication_defined
attribute :status do
unless model.active? || model.invited?
@ -47,6 +43,14 @@ module Users
private
def authentication_defined
errors.add :password, :blank if model.active? && no_auth?
end
def no_auth?
model.password.blank? && model.auth_source_id.blank? && model.identity_url.blank?
end
##
# Users can only be created by Admins
def user_allowed_to_add

@ -36,6 +36,11 @@ class AuthSource < ActiveRecord::Base
validates_uniqueness_of :name
validates_length_of :name, maximum: 60
def self.unique_attribute
:name
end
prepend ::Mixins::UniqueFinder
def authenticate(_login, _password)
end

@ -34,7 +34,7 @@ class Category < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :name, scope: [:project_id]
validates_length_of :name, maximum: 256
validates_length_of :name, maximum: 255
# validates that assignee is member of the issue category's project
validates_each :assigned_to_id do |record, attr, value|

@ -33,7 +33,7 @@ class LdapAuthSource < AuthSource
validates_presence_of :host, :port, :attr_login
validates_length_of :name, :host, maximum: 60, allow_nil: true
validates_length_of :account, :account_password, :base_dn, maximum: 255, allow_nil: true
validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, maximum: 30, allow_nil: true
validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :attr_admin, maximum: 30, allow_nil: true
validates_numericality_of :port, only_integer: true
before_validation :strip_ldap_attributes
@ -67,7 +67,7 @@ class LdapAuthSource < AuthSource
private
def strip_ldap_attributes
[:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr|
[:attr_login, :attr_firstname, :attr_lastname, :attr_mail, :attr_admin].each do |attr|
write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil?
end
end
@ -88,6 +88,7 @@ class LdapAuthSource < AuthSource
firstname: LdapAuthSource.get_attr(entry, attr_firstname),
lastname: LdapAuthSource.get_attr(entry, attr_lastname),
mail: LdapAuthSource.get_attr(entry, attr_mail),
admin: !!LdapAuthSource.get_attr(entry, attr_admin),
auth_source_id: id
}
end

@ -0,0 +1,46 @@
module Mixins
module UniqueFinder
def self.prepended(model_class)
unless model_class.respond_to? :unique_attribute
raise "Missing :unique_attribute accessor on ##{model_class}"
end
model_class.singleton_class.prepend ClassMethods
end
module ClassMethods
##
# Returns the first model that matches (in this order), either:
# 1. The given ID
# 2. The given unique attribute
def find_by_unique(unique_or_id)
matches = where(id: unique_or_id).or(where(unique_attribute => unique_or_id)).to_a
case matches.length
when 0
nil
when 1
matches.first
else
matches.find { |user| user.id.to_s == unique_or_id.to_s }
end
end
##
# Returns the first model that matches (in this order), either:
# 1. The given ID
# 2. The given unique attribute
#
# Raise ActiveRecord::RecordNotFound when no match is found.
def find_by_unique!(unique_or_id)
match = find_by_unique(unique_or_id)
if match.nil?
raise ActiveRecord::RecordNotFound
else
match
end
end
end
end
end

@ -31,6 +31,7 @@ module Queries::Users
Queries::Register.filter Queries::Users::UserQuery, Queries::Users::Filters::NameFilter
Queries::Register.filter Queries::Users::UserQuery, Queries::Users::Filters::GroupFilter
Queries::Register.filter Queries::Users::UserQuery, Queries::Users::Filters::StatusFilter
Queries::Register.filter Queries::Users::UserQuery, Queries::Users::Filters::LoginFilter
Queries::Register.order Queries::Users::UserQuery, Queries::Users::Orders::DefaultOrder
Queries::Register.order Queries::Users::UserQuery, Queries::Users::Orders::NameOrder

@ -0,0 +1,38 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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.
#++
class Queries::Users::Filters::LoginFilter < Queries::Users::Filters::UserFilter
def type
:string
end
def self.key
:login
end
end

@ -41,4 +41,17 @@ class Queries::Users::Filters::StatusFilter < Queries::Users::Filters::UserFilte
def self.key
:status
end
def status_values
values.map { |value| Principal::STATUSES[value.to_sym] }
end
def where
case operator
when "="
["users.status IN (?)", status_values.join(", ")]
when "!"
["users.status NOT IN (?)", status_values.join(", ")]
end
end
end

@ -87,12 +87,8 @@ class ::Query::Results
includes = ([:status, :project] +
includes_for_columns(query.involved_columns) + (options[:include] || [])).uniq
# A 'distinct' is added by the visible scope which is not necessary for
# filtering the work packages and which might conflict with ordering in
# mysql.
WorkPackage
.visible
.distinct(false)
.where(::Query.merge_conditions(query.statement, options[:conditions]))
.includes(includes)
.joins((query.group_by_column ? query.group_by_column.join : nil))

@ -149,6 +149,11 @@ class User < Principal
scope :newest, -> { not_builtin.order(created_on: :desc) }
def self.unique_attribute
:login
end
prepend ::Mixins::UniqueFinder
def sanitize_mail_notification_setting
self.mail_notification = Setting.default_notification_option if mail_notification.blank?
true

@ -69,8 +69,7 @@ class WorkPackage < ActiveRecord::Base
}
scope :visible, ->(*args) {
joins(:project)
.merge(Project.allowed_to(args.first || User.current, :view_work_packages))
where(project_id: Project.allowed_to(args.first || User.current, :view_work_packages))
}
scope :in_status, -> (*args) do

@ -53,4 +53,5 @@ See doc/COPYRIGHT.rdoc for more details.
<div class="form--field"><%= f.text_field 'attr_firstname', label: AuthSource.human_attribute_name(:firstname), size: 20 %></div>
<div class="form--field"><%= f.text_field 'attr_lastname', label: AuthSource.human_attribute_name(:lastname), size: 20 %></div>
<div class="form--field"><%= f.text_field 'attr_mail', label: AuthSource.human_attribute_name(:mail), size: 20 %></div>
<div class="form--field"><%= f.text_field 'attr_admin', label: AuthSource.human_attribute_name(:admin), size: 20 %></div>
</fieldset>

@ -41,7 +41,7 @@ See doc/COPYRIGHT.rdoc for more details.
css_classes[:lang] = custom_field.name_locale
%>
<div class="form--field">
<div class="form--field -wide-label ellipsis">
<%= form.collection_check_box :work_package_custom_field_ids,
custom_field.id,
@project.all_work_package_custom_fields.include?(custom_field),

@ -60,12 +60,12 @@ See doc/COPYRIGHT.rdoc for more details.
</legend>
<div>
<p><%= I18n.t('text_form_configuration') %></p>
<table class="attributes-table">
<table class="attributes-table -two-options">
<thead>
<tr>
<th><%= I18n.t('label_attribute') %></th>
<th><%= I18n.t('label_active') %></th>
<th><%= I18n.t('label_always_visible') %></th>
<th class="attributes-table--option"><%= I18n.t('label_active') %></th>
<th class="attributes-table--option"><%= I18n.t('label_always_visible') %></th>
</tr>
</thead>
<tbody>
@ -87,7 +87,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= label_tag "type_attribute_visibility_#{name}",
translated_attribute_name(name, attr),
value: "type_attribute_visibility[#{name}]",
class: 'form--label' %>
class: 'ellipsis' %>
</td>
<td>
<input name="<%= "type[attribute_visibility][#{name}]" %>" type="hidden" value="hidden" />

@ -227,6 +227,8 @@ en:
attributes:
status:
invalid_on_create: "is not a valid status for new users."
auth_source:
error_not_found: "not found"
activerecord:
attributes:
announcements:

@ -0,0 +1,34 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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.
#++
class AddAttrAdminToLdap < ActiveRecord::Migration[5.0]
def change
add_column :auth_sources, :attr_admin, :string
end
end

@ -10,37 +10,48 @@
| delete | Permanently remove a user from the instance | **Permission**: Administrator, self-delete |
## Linked Properties
| Link | Description | Type | Constraints | Supported operations |
|:---------:|-------------------------------------------- | ------------- | --------------------- | -------------------- |
| self | This user | User | not null | READ |
| Link | Description | Type | Constraints | Supported operations | Condition |
|:-----------:|-------------------------------------------------------------- | ------------- | --------------------- | -------------------- | ----------------------------------------- |
| self | This user | User | not null | READ | |
| auth_source | Link to the user's auth source (endpoint not yet implemented) | AuthSource | | READ / WRITE | **Permission**: Administrator |
## Local Properties
| Property | Description | Type | Constraints | Supported operations | Condition |
| :---------: | --------------------------------------------------------- | -------- | ---------------------------------------------------- | -------------------- | ----------------------------------------------------------- |
| id | User's id | Integer | x > 0 | READ | |
| login | User's login name | String | unique, 256 max length | READ / WRITE | **Permission**: Administrator |
| firstName | User's first name | String | 30 max length | READ / WRITE | **Permission**: Administrator |
| lastName | User's last name | String | 30 max length | READ / WRITE | **Permission**: Administrator |
| name | User's full name, formatting depends on instance settings | String | | READ | |
| email | User's E-Mail address | String | unique, 60 max length | READ / WRITE | E-Mail address not hidden, **Permission**: Administrator |
| admin | Flag indicating whether or not the user is an admin | Boolean | in: [true, false] | READ / WRITE | **Permission**: Administrator |
| avatar | URL to user's avatar | Url | | READ | |
| status | The current activation status of the user (see below) | String | in: ["active", "registered", "locked", "invited"] | READ | |
| language | User's language | String | ISO 639-1 | READ / WRITE | **Permission**: Administrator |
| password | User's password | String | | WRITE | **Permission**: Administrator |
| createdAt | Time of creation | DateTime | | READ | |
| updatedAt | Time of the most recent change to the user | DateTime | | READ | |
| Property | Description | Type | Constraints | Supported operations | Condition |
| :----------: | --------------------------------------------------------- | -------- | ---------------------------------------------------- | -------------------- | ----------------------------------------------------------- |
| id | User's id | Integer | x > 0 | READ | |
| login | User's login name | String | unique, 256 max length | READ / WRITE | **Permission**: Administrator |
| firstName | User's first name | String | 30 max length | READ / WRITE | **Permission**: Administrator |
| lastName | User's last name | String | 30 max length | READ / WRITE | **Permission**: Administrator |
| name | User's full name, formatting depends on instance settings | String | | READ | |
| email | User's E-Mail address | String | unique, 60 max length | READ / WRITE | E-Mail address not hidden, **Permission**: Administrator |
| admin | Flag indicating whether or not the user is an admin | Boolean | in: [true, false] | READ / WRITE | **Permission**: Administrator |
| avatar | URL to user's avatar | Url | | READ | |
| status | The current activation status of the user (see below) | String | in: ["active", "registered", "locked", "invited"] | READ | |
| language | User's language | String | ISO 639-1 | READ / WRITE | **Permission**: Administrator |
| password | User's password for the default password authentication | String | | WRITE | **Permission**: Administrator |
| identity_url | User's identity_url for OmniAuth authentication | String | | READ / WRITE | **Permission**: Administrator |
| createdAt | Time of creation | DateTime | | READ | |
| updatedAt | Time of the most recent change to the user | DateTime | | READ | |
The `status` of a user can be one of:
* `active` - the user can log in with the account
* `registered` - the user just registered to the instance, he can't log in yet, but will be able to, once the registration is completed
* `locked` - the user is locked and can't log in
* `invited` - the user has been invited and is pending registration
* `active` - the user can log in with the account right aways
* `invited` - the user is invited and is pending registration
If the user's `status` is set to `active` during creation a means of authentication
has to be provided which is one of the following:
* `password` - The password with which the user logs in.
* `auth_source` - Link to an LDAP auth source.
* `identity_url` - The identity URL of an OmniAuth authentication provider.
If all of these are missing the creation will fail with an "missing password" error.
The `language` is limited to those activated in the system.
Due to data privacy, the user's properties are limited to reveal as little about the user as possible. Thus `login`, `firstName`, `lastName`, `language`, `createdAt` and `updatedAt` are hidden for all users except for admins or the user themselves.
Due to data privacy, the user's properties are limited to reveal as little about the user as possible.
Thus `login`, `firstName`, `lastName`, `language`, `createdAt` and `updatedAt` are hidden for all
users except for admins or the user themselves.
Please note that custom fields are not yet supported by the api although the backend supports them.
@ -409,6 +420,7 @@ Lists users. Only administrators have permission to do this.
+ status: Status the user has
+ group: Name of the group in which to-be-listed users are members.
+ name: Filter users in whose first or last names, or email addresses the given string occurs.
+ login: User's login
+ sortBy (optional, string, `[["status", "asc"]]`) ... JSON specifying sort criteria.
Accepts the same format as returned by the [queries](#queries) endpoint.

@ -0,0 +1,36 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 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.
// ++
import {filtersModule} from './../../../angular-modules';
function htmlEscape() {
return function(string) {
return _.escape(string);
};
}
filtersModule.filter('htmlEscape', htmlEscape);

@ -24,7 +24,7 @@
typeahead-wait-ms="100"
typeahead-template-url="/components/common/typeahead/users/typeahead-match.html"
placeholder="{{ ::vm.text.autocomplete.placeholder }}"
uib-typeahead="item as item.name for item in vm.autocompleteWatchers($viewValue)">
uib-typeahead="item as (item.name | htmlEscape) for item in vm.autocompleteWatchers($viewValue)">
</form>
</div>
</div>

@ -56,7 +56,7 @@ function wpRelationsAutocompleteDirective($q, PathHelper, $http, I18n) {
scope.getIdentifier = function(workPackage){
if (workPackage) {
return `#${workPackage.id} - ${workPackage.subject}`;
return _.escape(`#${workPackage.id} - ${workPackage.subject}`);
}
};

@ -147,7 +147,23 @@ module API
setter: -> (value, *) { self.status = User::STATUSES[value.to_sym] },
render_nil: true
link :auth_source do
{
href: "/api/v3/auth_sources/#{represented.auth_source_id}",
title: represented.auth_source.name
} if represented.is_a?(User) && represented.auth_source && current_user.admin?
end
property :identity_url,
as: 'identityUrl',
exec_context: :decorator,
getter: -> (*) { represented.identity_url },
setter: -> (value, *) { represented.identity_url = value },
render_nil: true,
if: ->(*) { represented.is_a?(User) && current_user_is_admin_or_self }
# Write-only properties
property :password,
getter: -> (*) { nil },
render_nil: false,
@ -155,6 +171,44 @@ module API
self.password = self.password_confirmation = value
}
##
# Used while parsing JSON to initialize `auth_source_id` through the given link.
def initialize_embedded_links!(data)
auth_source_id = parse_auth_source_id data, "authSource"
if auth_source_id
auth_source = AuthSource.find_by_unique auth_source_id
id = auth_source ? auth_source.id : 0
# set id to 0 (as opposed to nil) to produce an auth source not found
# error further down the line in the user's base contract
represented.auth_source_id = id
end
end
##
# Overrides Roar::JSON::HAL::Resources#from_hash
def from_hash(hash, *)
if hash["_links"]
initialize_embedded_links! hash
end
super
end
def parse_auth_source_id(data, link_name)
value = data.dig("_links", link_name, "href")
if value
::API::Utilities::ResourceLinkParser.parse_id(
value,
property: :auth_source,
expected_version: "3",
expected_namespace: "auth_sources"
)
end
end
def _type
'User'
end

@ -89,7 +89,7 @@ module API
helpers ::API::V3::Users::UpdateUser
before do
@user = User.find(params[:id])
@user = User.find_by_unique!(params[:id])
end
get do

@ -139,7 +139,7 @@ module API
end
def paged_models(models)
models.page(@page).per_page(@per_page).pluck(:id).uniq
models.page(@page).per_page(@per_page).pluck(:id)
end
def full_work_packages(ids_in_order)

@ -29,10 +29,7 @@
namespace :ldap do
desc 'Register a LDAP auth source for the given LDAP URL and attribute mapping: ' \
'rake ldap:register["url=<URL>, name=<Name>, onthefly=<true,false>, map_{login,firstname,lastname,mail}=attribute"]'
task register: :environment do
def parse_args
# Rake croaks when using commas in default args without properly escaping
args = {}
ARGV.drop(1).each do |arg|
@ -40,11 +37,59 @@ namespace :ldap do
args[key.to_sym] = val
end
args
end
desc 'Synchronize users from the LDAP auth source with an optional filter' \
'rake ldap:sync["name=<LdapAuthSource Name>", filter=<Optional RFC2254 filter string>]'
task sync: :environment do
args = parse_args
ldap = LdapAuthSource.find_by!(name: args.fetch(:name))
# Only get the required args for syncing
attributes = ['dn', ldap.attr_firstname, ldap.attr_lastname, ldap.attr_mail, ldap.attr_login]
# Map user attributes to their ldap counterpart
ar_map = Hash[ %w(firstname lastname mail login).zip(attributes.drop(1)) ]
# Parse filter string if available
filter = Net::LDAP::Filter.from_rfc2254 args.fetch(:filter, 'objectClass = *')
# Open LDAP connection
ldap_con = ldap.send(:initialize_ldap_con, ldap.account, ldap.account_password)
User.transaction do
results = ldap_con.search(base: ldap.base_dn, filter: filter) do |entry|
user = User.find_or_initialize_by(login: entry[ldap.attr_login])
user.attributes = {
firstname: entry[ldap.attr_firstname],
lastname: entry[ldap.attr_lastname],
mail: entry[ldap.attr_mail],
admin: entry[ldap.attr_admin],
auth_source: ldap
}
if user.changed?
Rails.logger.info "Updated user #{user.login} due to ldap synchronization"
user.save
end
end
end
end
desc 'Register a LDAP auth source for the given LDAP URL and attribute mapping: ' \
'rake ldap:register["url=<URL> name=<Name> onthefly=<true,false>map_{login,firstname,lastname,mail,admin}=attribute"]'
task register: :environment do
args = parse_args
url = URI.parse(args[:url])
unless %w(ldap ldaps).include?(url.scheme)
raise "Expected #{args[:url]} to be a valid ldap(s) URI."
end
source = LdapAuthSource.new name: args[:name],
host: url.host,
port: url.port,
@ -56,7 +101,8 @@ namespace :ldap do
attr_login: args[:map_login],
attr_firstname: args[:map_firstname],
attr_lastname: args[:map_lastname],
attr_mail: args[:map_mail]
attr_mail: args[:map_mail],
attr_admin: args[:map_admin]
if source.save
puts "Saved new LDAP auth source #{args[:name]}."

@ -40,11 +40,11 @@ describe SettingsController, type: :controller do
describe 'edit' do
render_views
before(:all) do
before do
@previous_projects_modules = Setting.default_projects_modules
end
after(:all) do
after do
Setting.default_projects_modules = @previous_projects_modules
end

@ -27,7 +27,7 @@
#++
shared_context 'typeahead helpers' do
def select_typeahead(element, query:, select_text: nil)
def search_typeahead(element, query:)
# Open the element
element.click
# Insert the text to find
@ -35,7 +35,11 @@ shared_context 'typeahead helpers' do
##
# Find the dropdown by reference
target_dropdown = element['aria-owns']
element['aria-owns']
end
def select_typeahead(element, query:, select_text: nil)
target_dropdown = search_typeahead(element, query: query)
##
# If a specific select_text is given, use that to locate the match,

@ -75,6 +75,23 @@ describe 'Watcher tab', js: true, selenium: true do
expect_button_is_not_watching
end
context 'with a user with arbitrary characters' do
let!(:html_user) {
FactoryGirl.create :user,
firstname: '<em>foo</em>',
member_in_project: project,
member_through_role: role
}
it 'escapes the user name' do
typeahead = find('.wp-watcher--autocomplete')
target_dropdown = search_typeahead(typeahead, query: 'foo')
expect(page).to have_selector("##{target_dropdown} .uib-typeahead-match", text: html_user.firstname)
expect(page).to have_no_selector("##{target_dropdown} .uib-typeahead-match em")
end
end
context 'with all permissions' do
it_behaves_like 'watch and unwatch with button'
end

@ -44,10 +44,4 @@ describe Queries::Users::Filters::StatusFilter, type: :model do
end
end
end
it_behaves_like 'list query filter' do
let(:attribute) { :status }
let(:model) { User }
let(:valid_values) { [Principal::STATUSES.keys.first] }
end
end

@ -78,7 +78,7 @@ describe Queries::Users::UserQuery, type: :model do
describe '#results' do
it 'is the same as handwriting the query' do
expected = base_scope.merge(User.where(["users.status IN (?)", "active"]))
expected = base_scope.merge(User.where(["users.status IN (?)", "1"]))
expect(instance.results.to_sql).to eql expected.to_sql
end

@ -46,21 +46,18 @@ describe User, type: :model do
}
describe 'a user with a long login (<= 256 chars)' do
let(:login) { 'a' * 256 }
it 'is valid' do
user.login = 'a' * 256
user.login = login
expect(user).to be_valid
end
it 'may be stored in the database' do
user.login = 'a' * 256
expect(user.save).to be_truthy
end
it 'may be loaded from the database' do
user.login = 'a' * 256
user.save
user.login = login
expect(user.save).to be_truthy
expect(User.find_by_login('a' * 256)).to eq(user)
expect(User.find_by_login(login)).to eq(user)
expect(User.find_by_unique(login)).to eq(user)
end
end

@ -29,51 +29,71 @@
require 'spec_helper'
describe 'WorkPackage-Visibility', type: :model do
let(:admin) { FactoryGirl.create(:admin) }
let(:admin) { FactoryGirl.create(:admin) }
let(:anonymous) { FactoryGirl.create(:anonymous) }
let(:user) { FactoryGirl.create(:user) }
let(:user) { FactoryGirl.create(:user) }
let(:public_project) { FactoryGirl.create(:project, is_public: true) }
let(:private_project) { FactoryGirl.create(:project, is_public: false) }
let(:other_project) { FactoryGirl.create(:project, is_public: true) }
let(:view_work_packages) { FactoryGirl.create(:role, permissions: [:view_work_packages]) }
let(:view_work_packages_role2) { FactoryGirl.create(:role, permissions: [:view_work_packages]) }
describe 'of public projects' do
subject { FactoryGirl.create(:work_package, project: public_project) }
it 'should be viewable by anonymous users, when the anonymous-role has the permission to view packages' do
it 'is viewable by anonymous, with the view_work_packages permissison' do
# it is not really clear, where these kind of "preconditions" belong to: This setting
# is a default in Redmine::DefaultData::Loader - but this not loaded in the tests: here we
# just make sure, that the workpackage is visible, when this permission is set
Role.anonymous.add_permission! :view_work_packages
expect(WorkPackage.visible(anonymous)).to include subject
expect(WorkPackage.visible(anonymous)).to match_array [subject]
end
end
describe 'of private projects' do
subject { FactoryGirl.create(:work_package, project: private_project) }
it 'should be visible for the admin, even if the project is private' do
expect(WorkPackage.visible(admin)).to include subject
it 'is visible for the admin, even if the project is private' do
expect(WorkPackage.visible(admin)).to match_array [subject]
end
it 'should not be visible for anonymous users, when the project is private' do
expect(WorkPackage.visible(anonymous)).not_to include subject
it 'is not visible for anonymous users, when the project is private' do
expect(WorkPackage.visible(anonymous)).to match_array []
end
it 'should be visible for members of the project, that are allowed to view workpackages' do
member = FactoryGirl.create(:member, user: user, project: private_project, role_ids: [view_work_packages.id])
expect(WorkPackage.visible(user)).to include subject
it 'is visible for members of the project, with the view_work_packages permissison' do
FactoryGirl.create(:member,
user: user,
project: private_project,
role_ids: [view_work_packages.id])
expect(WorkPackage.visible(user)).to match_array [subject]
end
it 'is only returned once for members with two roles having view_work_packages permission' do
subject
FactoryGirl.create(:member,
user: user,
project: private_project,
role_ids: [view_work_packages.id,
view_work_packages_role2.id])
expect(WorkPackage.visible(user).pluck(:id)).to match_array [subject.id]
end
it 'should __not__ be visible for non-members of the project without the permission to view workpackages' do
expect(WorkPackage.visible(user)).not_to include subject
it 'is not visible for non-members of the project without the view_work_packages permissison' do
expect(WorkPackage.visible(user)).to match_array []
end
it 'should __not__ be visible for members of the project, without the right to view work_packages' do
it 'is not visible for members of the project, without the view_work_packages permissison' do
no_permission = FactoryGirl.create(:role, permissions: [:no_permission])
member = FactoryGirl.create(:member, user: user, project: private_project, role_ids: [no_permission.id])
FactoryGirl.create(:member,
user: user,
project: private_project,
role_ids: [no_permission.id])
expect(WorkPackage.visible(user)).not_to include subject
expect(WorkPackage.visible(user)).to match_array []
end
end
end

@ -83,39 +83,128 @@ describe ::API::V3::Users::UsersAPI, type: :request do
describe 'active status' do
let(:status) { 'active' }
let(:password) { 'admin!admin!' }
let(:parameters) {
{
status: status,
login: 'myusername',
firstName: 'Foo',
lastName: 'Bar',
email: 'foobar@example.org',
password: password
email: 'foobar@example.org'
}
}
it 'returns the represented user' do
send_request
context 'with auth_source' do
let(:auth_source_id) { 'some_ldap' }
let(:auth_source) { FactoryGirl.create :auth_source, name: auth_source_id }
context 'ID' do
before do
parameters[:_links] = {
authSource: {
href: "/api/v3/auth_sources/#{auth_source.id}"
}
}
end
expect(response.body).not_to have_json_path("_embedded/errors")
expect(response.body).to have_json_type(Object).at_path('_links')
expect(response.body)
.to be_json_eql('User'.to_json)
.at_path('_type')
it 'creates the user with the given auth_source ID' do
send_request
user = User.find_by(login: parameters[:login])
expect(user.auth_source).to eq auth_source
end
it_behaves_like 'represents the created user'
end
context 'name' do
before do
parameters[:_links] = {
authSource: {
href: "/api/v3/auth_sources/#{auth_source.name}"
}
}
end
it 'creates the user with the given auth_source ID' do
send_request
user = User.find_by(login: parameters[:login])
expect(user.auth_source).to eq auth_source
end
it_behaves_like 'represents the created user'
end
context 'invalid identifier' do
before do
parameters[:_links] = {
authSource: {
href: "/api/v3/auth_sources/foobar"
}
}
end
it 'returns an error for the authSource attribute' do
send_request
attr = JSON.parse(response.body).dig "_embedded", "details", "attribute"
expect(response.status).to eq 422
expect(attr).to eq "authSource"
end
end
end
it_behaves_like 'represents the created user'
context 'with identity_url' do
let(:identity_url) { 'google:3289272389298' }
context 'empty password' do
let(:password) { '' }
before do
parameters[:identityUrl] = identity_url
end
it 'marks the password missing and too short' do
it 'creates the user with the given identity_url' do
send_request
expect(errors.count).to eq(2)
expect(errors.collect { |el| el['_embedded']['details']['attribute'] })
.to match_array %w(password password)
user = User.find_by(login: parameters[:login])
expect(user.identity_url).to eq identity_url
end
it_behaves_like 'represents the created user'
end
context 'with password' do
let(:password) { 'admin!admin!' }
before do
parameters[:password] = password
end
it 'returns the represented user' do
send_request
expect(response.body).not_to have_json_path("_embedded/errors")
expect(response.body).to have_json_type(Object).at_path('_links')
expect(response.body)
.to be_json_eql('User'.to_json)
.at_path('_type')
end
it_behaves_like 'represents the created user'
context 'empty password' do
let(:password) { '' }
it 'marks the password missing and too short' do
send_request
expect(errors.count).to eq(2)
expect(errors.collect { |el| el['_embedded']['details']['attribute'] })
.to match_array %w(password password)
end
end
end
end

@ -0,0 +1,90 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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.
require 'spec_helper'
describe 'GET /api/v3/users', type: :request do
let!(:users) do
[
FactoryGirl.create(:admin, login: 'admin', status: Principal::STATUSES[:active]),
FactoryGirl.create(:user, login: 'h.wurst', status: Principal::STATUSES[:active]),
FactoryGirl.create(:user, login: 'h.heine', status: Principal::STATUSES[:locked]),
FactoryGirl.create(:user, login: 'm.mario', status: Principal::STATUSES[:active])
]
end
before do
login_as users.first
end
def filter_users(name, operator, values)
filter = {
name => {
"operator" => operator,
"values" => Array(values)
}
}
params = {
filters: [filter].to_json
}
get "/api/v3/users", params: params
json = JSON.parse response.body
Array(Hash(json).dig("_embedded", "elements")).map { |e| e["login"] }
end
describe 'status filter' do
it '=' do
expect(filter_users("status", "=", :active)).to eq ["admin", "h.wurst", "m.mario"]
end
it '!' do
expect(filter_users("status", "!", :active)).to eq ["h.heine"]
end
end
describe 'login filter' do
it '=' do
expect(filter_users("login", "=", "admin")).to eq ["admin"]
end
it '!' do
expect(filter_users("login", "!", "admin")).to eq ["h.wurst", "h.heine", "m.mario"]
end
it '~' do
expect(filter_users("login", "~", "h.")).to eq ["h.wurst", "h.heine"]
end
it '!~' do
expect(filter_users("login", "!~", "h.")).to eq ["admin", "m.mario"]
end
end
end

@ -182,6 +182,22 @@ describe 'API v3 User resource', type: :request do
end
end
context 'get with login' do
let(:get_path) { api_v3_paths.user user.login }
before do
allow(User).to receive(:current).and_return current_user
get get_path
end
it 'should respond with 200' do
expect(subject.status).to eq(200)
end
it 'should respond with correct body' do
expect(subject.body).to be_json_eql(user.name.to_json).at_path('name')
end
end
it_behaves_like 'handling anonymous user' do
let(:path) { api_v3_paths.user user.id }
end

Loading…
Cancel
Save