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

pull/8379/head
Oliver Günther 5 years ago
commit a1b9a1415f
No known key found for this signature in database
GPG Key ID: A3A8BDAD7C0C552C
  1. 10
      app/assets/stylesheets/content/work_packages/single_view/_single_view.sass
  2. 10
      app/assets/stylesheets/openproject/_mixins.sass
  3. 11
      app/controllers/oauth/auth_base_controller.rb
  4. 11
      app/models/ldap_auth_source.rb
  5. 13
      app/views/enterprises/_current.html.erb
  6. 9
      config/configuration.yml.example
  7. 6
      config/initializers/secure_headers.rb
  8. 8
      config/locales/crowdin/js-sl.yml
  9. 2
      config/locales/crowdin/sl.yml
  10. 94
      docs/system-admin-guide/authentication/ldap-authentication/README.md
  11. BIN
      docs/system-admin-guide/authentication/ldap-authentication/Screenshot-from-2018-11-01-13-47-42.png
  12. BIN
      docs/system-admin-guide/authentication/ldap-authentication/ldap-attribute-mapping.png
  13. BIN
      docs/system-admin-guide/authentication/ldap-authentication/ldap-details.png
  14. BIN
      docs/system-admin-guide/authentication/ldap-authentication/ldap-host-and-security.png
  15. 0
      docs/system-admin-guide/authentication/ldap-authentication/ldap-index-page.png
  16. BIN
      docs/system-admin-guide/authentication/ldap-authentication/ldap-settings.png
  17. BIN
      docs/system-admin-guide/authentication/ldap-authentication/ldap-system-user.png
  18. 19
      docs/system-admin-guide/authentication/oauth-applications/README.md
  19. BIN
      docs/system-admin-guide/authentication/oauth-applications/Sys-admin-authentication-oauth-postman.png
  20. 4
      frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-saved-trial.component.ts
  21. 4
      frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.component.ts
  22. 7
      frontend/src/app/components/wp-new/wp-create.component.ts
  23. 2
      frontend/src/app/modules/common/editable-toolbar-title/editable-toolbar-title.html
  24. 10
      frontend/src/app/modules/fields/display/field-types/wp-spent-time-display-field.module.ts
  25. 8
      lib/open_project/configuration.rb
  26. 5
      lib/open_project/configuration/helpers.rb
  27. 14
      modules/bim/app/controllers/bim/bcf/issues_controller.rb
  28. 1
      modules/bim/config/locales/en.yml
  29. 10
      modules/bim/lib/api/bim/bcf_xml/v1/bcf_xml_api.rb
  30. 14
      modules/bim/lib/open_project/bim/bcf_xml/importer.rb
  31. BIN
      modules/bim/spec/fixtures/files/bcf_2_0_dummy.bcf
  32. 10
      modules/bim/spec/requests/api/bcf_xml/v1/bcf_xml_api_spec.rb
  33. 8
      modules/boards/config/locales/crowdin/js-sl.yml
  34. 2
      modules/costs/app/controllers/cost_types_controller.rb
  35. 12
      modules/costs/config/locales/crowdin/sl.yml
  36. 2
      modules/xls_export/lib/open_project/xls_export/xls_views.rb
  37. 64
      spec/features/oauth/authorization_code_flow_spec.rb
  38. 35
      spec/features/work_packages/spent_time_display_spec.rb
  39. 26
      spec/features/work_packages/table/context_menu_spec.rb
  40. 13
      spec/models/ldap_auth_source_spec.rb

@ -151,16 +151,6 @@ i
.-columns-2 .-columns-2
@include two-column-layout @include two-column-layout
@supports (column-span: all)
// Let some elements still span both columns
.attributes-key-value.-span-all-columns
column-span: all
.attributes-key-value--key
flex-basis: calc(22.5% - (4rem / 6))
.attributes-key-value--value-container
flex-basis: calc(77.5% + (4rem / 6))
max-width: calc(77.5% + (4rem / 6))
@supports (column-span: all) @supports (column-span: all)
// Remove the outline on focus since that breaks the column in chrome // Remove the outline on focus since that breaks the column in chrome
// Chrome bug https://bugs.chromium.org/p/chromium/issues/detail?id=565116 // Chrome bug https://bugs.chromium.org/p/chromium/issues/detail?id=565116

@ -135,6 +135,16 @@ $scrollbar-size: 10px
@mixin two-column-layout @mixin two-column-layout
column-count: 2 column-count: 2
column-gap: 3rem column-gap: 3rem
@supports (column-span: all)
// Let some elements still span both columns
.attributes-key-value.-span-all-columns
column-span: all
.attributes-key-value--key
flex-basis: calc(22.5% - (4rem / 6))
.attributes-key-value--value-container
flex-basis: calc(77.5% + (4rem / 6))
max-width: calc(77.5% + (4rem / 6))
.attributes-key-value .attributes-key-value
-webkit-column-break-inside: avoid -webkit-column-break-inside: avoid

@ -34,6 +34,17 @@ module OAuth
# See config/initializers/doorkeeper.rb # See config/initializers/doorkeeper.rb
class AuthBaseController < ::ApplicationController class AuthBaseController < ::ApplicationController
skip_before_action :check_if_login_required skip_before_action :check_if_login_required
after_action :extend_content_security_policy
layout 'only_logo' layout 'only_logo'
def extend_content_security_policy
use_content_security_policy_named_append(:oauth)
end
def allowed_forms
allowed_redirect_urls = pre_auth&.client&.application&.redirect_uri
urls = allowed_redirect_urls.to_s.split
urls.map { |url| URI.join(url, '/') }.map(&:to_s)
end
end end
end end

@ -134,11 +134,12 @@ class LdapAuthSource < AuthSource
end end
def ldap_encryption def ldap_encryption
if tls_mode == 'plain_ldap' return nil if tls_mode.to_s == 'plain_ldap'
nil
else {
tls_mode.to_sym method: tls_mode.to_sym,
end tls_options: OpenProject::Configuration.ldap_tls_options.with_indifferent_access
}
end end
# Check if a DN (user record) authenticates with the password # Check if a DN (user record) authenticates with the password

@ -1,8 +1,11 @@
<enterprise-active-saved-trial data-subscriber="<%= @current_token.subscriber %>" <enterprise-active-saved-trial
data-email="<%= @current_token.mail %>" data-subscriber="<%= @current_token.subscriber %>"
data-user-count="<%= @current_token.restrictions.nil? ? t('js.admin.enterprise.upsale.unlimited') : @current_token.restrictions[:active_user_count] %>" data-email="<%= @current_token.mail %>"
data-starts-at="<%= format_date @current_token.starts_at %>" data-company="<%= @current_token.has_attribute?(:company) ? @current_token.company : nil %>"
data-expires-at="<%= (!@current_token.will_expire?) ? t('js.admin.enterprise.upsale.unlimited') : (format_date @current_token.expires_at) %>"> data-domain="<%= @current_token.has_attribute?(:domain) ? @current_token.domain : nil %>"
data-user-count="<%= @current_token.restrictions.nil? ? t('js.admin.enterprise.upsale.unlimited') : @current_token.restrictions[:active_user_count] %>"
data-starts-at="<%= format_date @current_token.starts_at %>"
data-expires-at="<%= (!@current_token.will_expire?) ? t('js.admin.enterprise.upsale.unlimited') : (format_date @current_token.expires_at) %>">
</enterprise-active-saved-trial> </enterprise-active-saved-trial>
<%= form_tag({}, method: :delete) do %> <%= form_tag({}, method: :delete) do %>

@ -376,6 +376,15 @@ default:
# user: admin # user: admin
# password: admin # password: admin
# Overriding LDAP TLS configuration
# You can set other TLS options for the LDAP auth source connection
# They are passed as the `tls_options` to the Net::LDAP gem
# see the following resources for more information
# https://github.com/ruby-ldap/ruby-net-ldap/blob/master/lib/net/ldap.rb
# https://ruby.github.io/openssl/OpenSSL/SSL/SSLContext.html
# For example, to specify a CA file
# ldap_tls_options:
# ca_file: "/path/to/the/root-ca.crt"
# By default, the APIv3 allows authentication through basic auth. # By default, the APIv3 allows authentication through basic auth.
# Uncomment the following line to restrict APIv3 access to session. # Uncomment the following line to restrict APIv3 access to session.

@ -73,3 +73,9 @@ SecureHeaders::Configuration.default do |config|
connect_src: connect_src connect_src: connect_src
} }
end end
SecureHeaders::Configuration.named_append(:oauth) do |request|
hosts = request.controller_instance.try(:allowed_forms) || []
{ form_action: hosts }
end

@ -109,7 +109,7 @@ sl:
description_subwork_package: "Podrejen delovni paket #%{id}" description_subwork_package: "Podrejen delovni paket #%{id}"
editor: editor:
preview: 'Preklopi način predogleda' preview: 'Preklopi način predogleda'
source_code: 'Toggle Markdown source mode' source_code: 'Preklopite vhodni način označevanja'
error_saving_failed: 'Shranjevanje dokumenta ni uspelo zaradi napake: %{error}' error_saving_failed: 'Shranjevanje dokumenta ni uspelo zaradi napake: %{error}'
error_initialization_failed: 'Napaka pri inicializaciji CKEditorja' error_initialization_failed: 'Napaka pri inicializaciji CKEditorja'
mode: mode:
@ -461,12 +461,12 @@ sl:
overview: "Upravljajte svoje delo v <b>Backlogs</b> pogledu. <br> Na desni strani imate Produktne zaostanke ali zaostanke Napak, na levi imate ustrezne šprinte. Tukaj lahko ustvarite <b>epe, zgodbe uporabnikov in napake</b>, določite prednost preko drag'n'drop in jih dodajte v šprint." overview: "Upravljajte svoje delo v <b>Backlogs</b> pogledu. <br> Na desni strani imate Produktne zaostanke ali zaostanke Napak, na levi imate ustrezne šprinte. Tukaj lahko ustvarite <b>epe, zgodbe uporabnikov in napake</b>, določite prednost preko drag'n'drop in jih dodajte v šprint."
task_board_arrow: 'Za prikaz vaše <b>Tabela nalog</b>, odprite šprint spustni meni...' task_board_arrow: 'Za prikaz vaše <b>Tabela nalog</b>, odprite šprint spustni meni...'
task_board_select: '... in izberite <b>Tabela nalog</b> zapis.' task_board_select: '... in izberite <b>Tabela nalog</b> zapis.'
task_board: "The <b>Task board</b> visualizes the progress for this sprint. Add new tasks or impediments with the + icon next to a user story. Via drag'n'drop you can update the status." task_board: "Plošča<b>opravil</b> predstavlja napredek tega šprinta. Z ikono + zraven uporabniške zgodbe dodajte nove naloge ali ovire. Preko povleka 'stanje lahko posodobite stanje."
boards: boards:
overview: 'Organiziraj svoje delo z intuitivnim <b>Board</b> pogledom.' overview: 'Organiziraj svoje delo z intuitivnim <b>Board</b> pogledom.'
lists: 'You can create multiple lists (columns) within one Board view, e.g. to create a KANBAN board.' lists: 'V enem pogledu plošče lahko ustvarite več seznamov (stolpcev), npr. ustvarite tablo KANBAN.'
add: 'Klik na + bo <b>dodal novo kartico</b> na seznam v vaš Board.' add: 'Klik na + bo <b>dodal novo kartico</b> na seznam v vaš Board.'
drag: 'Drag & Drop your cards within a list to re-order, or to another list. A double click will open the details view.' drag: 'Povlecite in spustite svoje kartice znotraj seznama, če ga želite ponovno naročiti, ali na drug seznam. Z dvojnim klikom se odpre pogled s podrobnostmi.'
wp: wp:
toggler: "Zdaj pa poglejmo <b>Delovni paket</b> del, ki vam da bolj podroben pregled vašega dela. " toggler: "Zdaj pa poglejmo <b>Delovni paket</b> del, ki vam da bolj podroben pregled vašega dela. "
list: 'This is the <b>Work package</b> list with the important work within your project, such as tasks, features, milestones, bugs, and more. <br> You can create or edit a work package directly within this list. To see its details you can double click on a row.' list: 'This is the <b>Work package</b> list with the important work within your project, such as tasks, features, milestones, bugs, and more. <br> You can create or edit a work package directly within this list. To see its details you can double click on a row.'

@ -1341,7 +1341,7 @@ sl:
label_delete_project: "Brisanje projekta" label_delete_project: "Brisanje projekta"
label_deleted: "Izbrisano" label_deleted: "Izbrisano"
label_deleted_custom_field: "(izbrisano polje po meri)" label_deleted_custom_field: "(izbrisano polje po meri)"
label_deleted_custom_option: "(deleted option)" label_deleted_custom_option: "(izbrisana možnost)"
label_descending: "Padajoče" label_descending: "Padajoče"
label_details: "Podrobnosti" label_details: "Podrobnosti"
label_development_roadmap: "Načrt razvoja" label_development_roadmap: "Načrt razvoja"

@ -23,34 +23,106 @@ To create a new LDAP authentication click on the respective icon.
You will then be able to specify the LDAP configuration. This can be any directory service compatible with the LDAPv3 standard, such as Microsoft Active Directory or openLDAP. The configuration depends on the specific database/applications, through which the authentication with OpenProject is intended. You will then be able to specify the LDAP configuration. This can be any directory service compatible with the LDAPv3 standard, such as Microsoft Active Directory or openLDAP. The configuration depends on the specific database/applications, through which the authentication with OpenProject is intended.
The following screenshot contains an exemplary configuration for a new LDAP authentication mode. In the following, we will go through all available options. The following screenshots contain an exemplary configuration for a new LDAP authentication mode. In the following, we will go through all available options.
#### LDAP connection details and security
![Adding a new LDAP authentication server](ldap-host-and-security.png)
In the upper section, you have to specify the connection details of your LDAP server as well as the connection encryption to use.
![Adding a new LDAP authentication server](Screenshot-from-2018-11-01-13-47-42.png)
- **Name:** Arbitrary identifier used to show which authentication source a user is coming from (e.g., in the [Administration > Users view](https://www.openproject.org/help/administration/manage-users/)) - **Name:** Arbitrary identifier used to show which authentication source a user is coming from (e.g., in the [Administration > Users view](https://www.openproject.org/help/administration/manage-users/))
- **Host:** Full hostname to the LDAP server - **Host:** Full hostname to the LDAP server
- **Port :** LDAP port. Will usually be 389 for LDAP and 689 for LDAPS connections. - **Port :** LDAP port. Will usually be 389 for LDAP and StartTLS and 636 for LDAP over SSL connections.
- **LDAPS :** If checked, this will result in NET::LDAP connecting with the *simple_tls* option *enabled.* [Click here to read more details into what this means for connection security.](https://www.rubydoc.info/gems/ruby-net-ldap/Net/LDAP) - **Connection encryption**: Select the appropriate connection encryption.
- For unencrypted connections, select `none` . No TLS/SSL connection will be established, your connection will be unsecure
- For LDAPS connections (LDAP over SSL), use `simple_tls` , this is an older SSL encryption pattern that uses SSL certificates, but **DOES NOT VERIFY THEM**. Implicit trust in the connection will be placed, but the connection will be encrypted. Some older LDAP servers only support this option
- **Recommended option**: `start_tls` will use TLS to encrypt the connection after connecting to the LDAP server on the unencrypted PORT (`389` by default).
- [Click here to read more details into what these options mean for connection security.](https://www.rubydoc.info/gems/ruby-net-ldap/Net/LDAP)
**Allowing untrusted certifcates for LDAP connections**
If you use `start_tls` , certificate details and host names will be verified on connections as recommended for security. In case you use a custom untrusted certificate authority (CA) that your LDAP is connecting to, you can place this CA in your system's trusted CA store if possible. For some distributions, you will need to specify this CA manually to OpenProject.
You can do this by using the [advanced configuration](https://docs.openproject.org/installation-and-operations/configuration/) function of OpenProject. You can define the CA path by setting the following ENV variable:
```bash
OPENPROJECT_LDAP__TLS__OPTIONS_CA__FILE="/path/to/the/root-ca.crt"
```
or by extending your production configuration of `config/configuration.yml` with the following segment:
```
production:
# .. other settings ..
# ldap_tls_options:
# ca_file: "/path/to/the/root-ca.crt"
```
You can set other TLS options for the LDAP auth source connection. They are passed as the `tls_options` to the Net::LDAP gem and ultimately end up in the `SSLContext` setting of Ruby. You can define the TLS version and other advanced options in case your connections needs it. Most users will not need to change this however.
See the following resources for more information:
- https://github.com/ruby-ldap/ruby-net-ldap/blob/master/lib/net/ldap.rb
- https://ruby.github.io/openssl/OpenSSL/SSL/SSLContext.html
#### LDAP system user credentials
![Defining the system user of the connection](ldap-system-user.png)
Next, you will need to enter a system user that has READ access to the users for identification and synchronization purposes. Note that most operations to the LDAP during authentication will not be using these credentials, but the user-provided credentials in the login form in order to perform a regular user bind to the LDAP.
- **Account:** The full DN of a system users used for looking up user details in the LDAP. It must have read permissions under the Base DN. This will not be used for the user bind upon authentication. - **Account:** The full DN of a system users used for looking up user details in the LDAP. It must have read permissions under the Base DN. This will not be used for the user bind upon authentication.
- **Password:** The bind password of the system user’s DN above. - **Password:** The bind password of the system user’s DN above.
- **On-the-fly user creation:** Check to automatically create users in OpenProject when they first login in OpenProject. It will use the LDAP attribute mapping below to fill out required attributes. The user will be forwarded to a registration screen to complete required attributes if they are missing in the LDAP.
**Attribute mapping**
#### LDAP details
![Defining the details of the connection](ldap-details.png)
Next you can define what sections OpenProject will look for in the LDAP and also if users should be created automatically in OpenProject when they are accessing it. Let's look at the available options:
- **Base DN**: Enter the Base DN to search within for users and groups in the LDAP tree
- **Filter string**: Enter an optional [LDAP RFC4515 filter string](https://tools.ietf.org/search/rfc4515) to further reduce the returned set of users. This allows you to restrict access to OpenProject with a very flexible filter. For group synchronization, only users matching this filter will be added as well.
- **Automatic user creation:** Check to automatically create users in OpenProject when they first login in OpenProject. It will use the LDAP attribute mapping below to fill out required attributes. The user will be forwarded to a registration screen to complete required attributes if they are missing in the LDAP.
#### Attribute mapping
![Defining the attribute map for users](ldap-attribute-mapping.png)
The attribute mapping is used to identify attributes of OpenProject with attributes of the LDAP directory. At least the *login* attribute is required to create DNs from the login credentials. The attribute mapping is used to identify attributes of OpenProject with attributes of the LDAP directory. At least the *login* attribute is required to create DNs from the login credentials.
- **Login:** The login attribute in the ldap. Will be used to construct the DN from `login-attribute=value,`. Most often, this will be *uid.* - **Login:** The login attribute in the ldap. Will be used to construct the DN from `login-attribute=value,`. Most often, this will be *uid.*
- **First name:** The attribute name in the LDAP that maps to first name. Most often, this will be *givenName.* If left empty, user will be prompted to enter upon registration if **on-the-fly-creation** is true. - **First name:** The attribute name in the LDAP that maps to first name. Most often, this will be *givenName.* If left empty, user will be prompted to enter upon registration if **automatic user creation** is true.
- **Last name:** The attribute name in the LDAP that maps to last name. Most often, this will be *sn.* If left empty, user will be prompted to enter upon registration if **on-the-fly-creation** is true. - **Last name:** The attribute name in the LDAP that maps to last name. Most often, this will be *sn.* If left empty, user will be prompted to enter upon registration if **automatic user creation** is true.
- **Email:** The attribute name in the LDAP that maps to the user’s mail address. This will usually be *mail.* If left empty, user will be prompted to enter upon registration if **on-the-fly-creation** is true. - **Email:** The attribute name in the LDAP that maps to the user’s mail address. This will usually be *mail.* If left empty, user will be prompted to enter upon registration if **automatic user creation** is true.
- **Admin:** Specify an attribute that if it has a truthy value, results in the user in OpenProject becoming an admin account. Leave empty to never set admin status from LDAP attributes. - **Admin:** Specify an attribute that if it has a truthy value, results in the user in OpenProject becoming an admin account. Leave empty to never set admin status from LDAP attributes.
Lastly, click on *Create* to save the LDAP authentication mode. You will be redirected to the index page with the created authentication mode. Click the *test* button to create a test connection using the system user’s bind credentials. Lastly, click on *Create* to save the LDAP authentication mode. You will be redirected to the index page with the created authentication mode. Click the *test* button to create a test connection using the system user’s bind credentials.
![LDAP authentication mode created](Screenshot-from-2018-11-01-14-03-42.png) ![LDAP authentication mode created](ldap-index-page.png)
With the [OpenProject Enterprise Edition](https://www.openproject.org/enterprise-edition/) it is possible to [synchronize LDAP and OpenProject groups](./ldap-group-synchronization). With the [OpenProject Enterprise Edition](https://www.openproject.org/enterprise-edition/) it is possible to [synchronize LDAP and OpenProject groups](./ldap-group-synchronization).

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

@ -26,4 +26,21 @@ You can configure the following options to add your oauth application.
4. Choose **client credential flows** and define a user on whose behalf requests will be performed. 4. Choose **client credential flows** and define a user on whose behalf requests will be performed.
5. Press the blue **Create** button to add your oauth application. 5. Press the blue **Create** button to add your oauth application.
![Sys-admin-authentication-add-oauth-application](Sys-admin-authentication-add-oauth-application.png) ![Sys-admin-authentication-add-oauth-application](Sys-admin-authentication-add-oauth-application.png)
## Oauth endpoints
The authentication endpoints are at
* Auth URL: `https://example.com/oauth/authorize`
* Access Token URL: `https://example.com/oauth/token`
## Using Postman with oauth?
Set redirect URLs to `urn:ietf:wg:oauth:2.0:oob` in both, for your application (see step 2 above) and
within Postman.
In Postman the configuration should look like this (Replace `{{protocolHostPort}}` with your host,
i.e. `https://example.com`)
![Sys-admin-authentication-add-oauth-application](Sys-admin-authentication-oauth-postman.png)

@ -40,11 +40,11 @@ export const enterpriseActiveSavedTrialSelector = 'enterprise-active-saved-trial
export class EEActiveSavedTrialComponent extends EEActiveTrialBase { export class EEActiveSavedTrialComponent extends EEActiveTrialBase {
public subscriber = this.elementRef.nativeElement.dataset['subscriber']; public subscriber = this.elementRef.nativeElement.dataset['subscriber'];
public email = this.elementRef.nativeElement.dataset['email']; public email = this.elementRef.nativeElement.dataset['email'];
public company = this.elementRef.nativeElement.dataset['company'];
public domain = this.elementRef.nativeElement.dataset['domain'];
public userCount = this.elementRef.nativeElement.dataset['userCount']; public userCount = this.elementRef.nativeElement.dataset['userCount'];
public startsAt = this.elementRef.nativeElement.dataset['startsAt']; public startsAt = this.elementRef.nativeElement.dataset['startsAt'];
public expiresAt = this.elementRef.nativeElement.dataset['expiresAt']; public expiresAt = this.elementRef.nativeElement.dataset['expiresAt'];
public company:string;
public domain:string;
constructor(readonly elementRef:ElementRef, constructor(readonly elementRef:ElementRef,
readonly I18n:I18nService) { readonly I18n:I18nService) {

@ -42,11 +42,11 @@ import {GonService} from "core-app/modules/common/gon/gon.service";
export class EEActiveTrialComponent extends EEActiveTrialBase implements OnInit { export class EEActiveTrialComponent extends EEActiveTrialBase implements OnInit {
public subscriber:string; public subscriber:string;
public email:string; public email:string;
public company:string;
public domain:string;
public userCount:string; public userCount:string;
public startsAt:string; public startsAt:string;
public expiresAt:string; public expiresAt:string;
public company:string;
public domain:string;
constructor(readonly elementRef:ElementRef, constructor(readonly elementRef:ElementRef,
readonly cdRef:ChangeDetectorRef, readonly cdRef:ChangeDetectorRef,

@ -124,9 +124,10 @@ export class WorkPackageCreateComponent extends UntilDestroyedMixin implements O
this.setTitle(); this.setTitle();
if (this.stateParams['parent_id']) { if (this.stateParams['parent_id']) {
this.newWorkPackage.parent = { changeset.setValue(
href: this.pathHelper.api.v3.work_packages.id(this.stateParams['parent_id']).path 'parent',
}; { href: this.pathHelper.api.v3.work_packages.id(this.stateParams['parent_id']).path }
);
} }
// Load the parent simply to display the type name :-/ // Load the parent simply to display the type name :-/

@ -28,5 +28,5 @@
<h2 *ngIf="!editable" <h2 *ngIf="!editable"
[attr.title]="selectedTitle" [attr.title]="selectedTitle"
[ngClass]="{ '-disabled': disabled, '-small': smallHeader }" [ngClass]="{ '-disabled': disabled, '-small': smallHeader }"
class="editable-toolbar-title--fixed"> {{ selectedTitle | slice:0:50 }} class="editable-toolbar-title--fixed"> {{ selectedTitle }}
</h2> </h2>

@ -33,6 +33,7 @@ import { ProjectResource } from "core-app/modules/hal/resources/project-resource
import { InjectField } from "core-app/helpers/angular/inject-field.decorator"; import { InjectField } from "core-app/helpers/angular/inject-field.decorator";
import * as URI from 'urijs'; import * as URI from 'urijs';
import { TimeEntryCreateService } from 'core-app/modules/time_entries/create/create.service'; import { TimeEntryCreateService } from 'core-app/modules/time_entries/create/create.service';
import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource";
export class WorkPackageSpentTimeDisplayField extends DurationDisplayField { export class WorkPackageSpentTimeDisplayField extends DurationDisplayField {
public text = { public text = {
@ -81,16 +82,13 @@ export class WorkPackageSpentTimeDisplayField extends DurationDisplayField {
element.appendChild(timelogElement); element.appendChild(timelogElement);
let classContext = this; timelogElement.addEventListener('click', this.showTimelogWidget.bind(this, this.resource));
timelogElement.addEventListener('click', function() {
classContext.showTimelogWidget();
});
} }
} }
private showTimelogWidget() { private showTimelogWidget(wp:WorkPackageResource) {
this.timeEntryCreateService this.timeEntryCreateService
.create(moment(new Date()), this.resource, false) .create(moment(new Date()), wp, false)
.catch(() => { .catch(() => {
// do nothing, the user closed without changes // do nothing, the user closed without changes
}); });

@ -170,7 +170,10 @@ module OpenProject
'sentry_host' => 'https://sentry.openproject.com', 'sentry_host' => 'https://sentry.openproject.com',
# Allow connection to Augur # Allow connection to Augur
'enterprise_trial_creation_host' => 'https://augur.openproject.com' 'enterprise_trial_creation_host' => 'https://augur.openproject.com',
# Allow override of LDAP options
'ldap_auth_source_tls_options' => nil
} }
@config = nil @config = nil
@ -200,8 +203,7 @@ module OpenProject
# Replace config values for which an environment variable with the same key in upper case # Replace config values for which an environment variable with the same key in upper case
# exists # exists
def override_config!(config, source = default_override_source) def override_config!(config, source = default_override_source)
config.keys config.keys.select { |key| source.include? key.upcase }
.select { |key| source.include? key.upcase }
.each { |key| config[key] = extract_value key, source[key.upcase] } .each { |key| config[key] = extract_value key, source[key.upcase] }
config.deep_merge! merge_config(config, source) config.deep_merge! merge_config(config, source)

@ -106,6 +106,11 @@ module OpenProject
uploaders uploaders
end end
def ldap_tls_options
val = self['ldap_tls_options']
val.presence || {}
end
private private
## ##

@ -43,8 +43,9 @@ module Bim
before_action :set_import_options, only: %i[perform_import] before_action :set_import_options, only: %i[perform_import]
before_action :build_importer, only: %i[prepare_import perform_import] before_action :build_importer, only: %i[prepare_import perform_import]
before_action :check_bcf_version, only: %i[prepare_import]
menu_item :work_packages menu_item :ifc_models
def upload; def upload;
end end
@ -179,11 +180,11 @@ module Bim
end end
def build_importer def build_importer
@importer = ::OpenProject::Bim::BcfXml::Importer.new @bcf_xml_file, @project, current_user: current_user @importer = ::OpenProject::Bim::BcfXml::Importer.new(@bcf_xml_file, @project, current_user: current_user)
end end
def get_persisted_file def get_persisted_file
@bcf_attachment = Attachment.find_by! id: session[:bcf_file_id], author: current_user @bcf_attachment = Attachment.find_by!(id: session[:bcf_file_id], author: current_user)
@bcf_xml_file = File.new @bcf_attachment.local_path @bcf_xml_file = File.new @bcf_attachment.local_path
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
flash[:error] = I18n.t('bcf.bcf_xml.import.bcf_file_not_found') flash[:error] = I18n.t('bcf.bcf_xml.import.bcf_file_not_found')
@ -208,6 +209,13 @@ module Bim
redirect_to action: :upload redirect_to action: :upload
end end
end end
def check_bcf_version
unless @importer.bcf_version_valid?
flash[:error] = I18n.t('bcf.bcf_xml.import_failed_unsupported_bcf_version', minimal_version: OpenProject::Bim::BcfXml::Importer::MINIMUM_BCF_VERSION)
redirect_to action: :upload
end
end
end end
end end
end end

@ -25,6 +25,7 @@ en:
export: 'Export' export: 'Export'
import_update_comment: '(Updated in BCF import)' import_update_comment: '(Updated in BCF import)'
import_failed: 'Cannot import BCF file: %{error}' import_failed: 'Cannot import BCF file: %{error}'
import_failed_unsupported_bcf_version: 'Failed to read the BCF file: The BCF version is not supported. Please ensure the version is at least %{minimal_version} or higher.'
import_successful: 'Imported %{count} BCF issues' import_successful: 'Imported %{count} BCF issues'
import_canceled: 'BCF-XML import canceled.' import_canceled: 'BCF-XML import canceled.'
type_not_active: "The issue type is not activated for this project." type_not_active: "The issue type is not activated for this project."

@ -88,7 +88,17 @@ module API
importer = ::OpenProject::Bim::BcfXml::Importer.new(file, importer = ::OpenProject::Bim::BcfXml::Importer.new(file,
project, project,
current_user: User.current) current_user: User.current)
unless importer.bcf_version_valid?
error_message = I18n.t('bcf.bcf_xml.import_failed_unsupported_bcf_version',
minimal_version: OpenProject::Bim::BcfXml::Importer::MINIMUM_BCF_VERSION)
raise API::Errors::UnsupportedMediaType.new(error_message)
end
importer.import!(import_options) importer.import!(import_options)
rescue API::Errors::UnsupportedMediaType => e
raise e
rescue StandardError => e rescue StandardError => e
raise API::Errors::InternalError.new(e.message) raise API::Errors::InternalError.new(e.message)
ensure ensure

@ -4,6 +4,7 @@ require_relative 'aggregations'
module OpenProject::Bim::BcfXml module OpenProject::Bim::BcfXml
class Importer class Importer
MINIMUM_BCF_VERSION = "2.1"
attr_reader :file, :project, :current_user attr_reader :file, :project, :current_user
DEFAULT_IMPORT_OPTIONS = { DEFAULT_IMPORT_OPTIONS = {
@ -60,6 +61,19 @@ module OpenProject::Bim::BcfXml
raise raise
end end
def bcf_version_valid?
Zip::File.open(@file) do |zip|
zip_entry = zip.find { |entry| entry.name.end_with?('bcf.version') }
markup = zip_entry.get_input_stream.read
doc = Nokogiri::XML(markup, nil, 'UTF-8')
bcf_version = doc.xpath('/Version').first['VersionId']
return Gem::Version.new(bcf_version) >= Gem::Version.new(MINIMUM_BCF_VERSION)
end
rescue StandardError => e
# The uploaded file could be anything.
false
end
private private
def create_or_add_missing_members(options) def create_or_add_missing_members(options)

@ -151,6 +151,16 @@ describe 'BCF XML API v1 bcf_xml resource', type: :request do
expect(project.work_packages.count).to eql(1) expect(project.work_packages.count).to eql(1)
end end
end end
context "with unsupported BCF version (2.0)" do
let(:filename) { 'bcf_2_0_dummy.bcf' }
it "returns a status 415" do
expect(subject.status).to eql(415)
expect(subject.body).to match /BCF version is not supported/
expect(project.work_packages.count).to eql(1)
end
end
end end
def zip_has_file?(zip_string, filename) def zip_has_file?(zip_string, filename)

@ -46,10 +46,10 @@ sl:
add_list_modal: add_list_modal:
warning: warning:
status: | status: |
There is currently no status available. <br> Trenutno ni na voljo nobenega statusa <br>
Either there are none or they have all already been added to the board. Ali jih ni, ali pa so vsi že dodani na desko.
assignee: This project currently does not have any members that can be added. <br> assignee: Ta projekt trenutno nima članov, ki bi jih lahko dodali. <br>
add_members: <a href="%{link}">Add a new member to this project</a> to select users again. add_members: <a href="%{link}">V ta projekt dodajte novega člana </a>, da znova izberete uporabnike.
configuration_modal: configuration_modal:
title: 'Konfigurirajte to tabelo' title: 'Konfigurirajte to tabelo'
display_settings: display_settings:

@ -129,7 +129,7 @@ class CostTypesController < ApplicationController
cr.valid_from = today cr.valid_from = today
end end
rate.rate = parse_number_string_to_number(params[:rate]) rate.rate = CostRate.parse_number_string_to_number(params[:rate])
if rate.save if rate.save
flash[:notice] = t(:notice_successful_update) flash[:notice] = t(:notice_successful_update)
redirect_to action: 'index' redirect_to action: 'index'

@ -42,7 +42,7 @@ sl:
updated_on: "Posodobljeno" updated_on: "Posodobljeno"
cost_type: cost_type:
unit: "Ime enote" unit: "Ime enote"
unit_plural: "Pluralized unit name" unit_plural: "Pluralizirano ime enote"
work_package: work_package:
costs_by_type: "Porabljene enote" costs_by_type: "Porabljene enote"
cost_object_subject: "Naslov proračuna" cost_object_subject: "Naslov proračuna"
@ -172,21 +172,21 @@ sl:
permission_edit_hourly_rates: "Urejanje urnih postavk" permission_edit_hourly_rates: "Urejanje urnih postavk"
permission_edit_own_hourly_rate: "uredite lastne urne postavke" permission_edit_own_hourly_rate: "uredite lastne urne postavke"
permission_edit_rates: "Uredite cene" permission_edit_rates: "Uredite cene"
permission_log_costs: "Book unit costs" permission_log_costs: "Stroški knjižne enote"
permission_log_own_costs: "Book unit costs for oneself" permission_log_own_costs: "Stroški knjižne enote zase"
permission_view_cost_entries: "View booked costs" permission_view_cost_entries: "View booked costs"
permission_view_cost_objects: "Oglejte si proračune" permission_view_cost_objects: "Oglejte si proračune"
permission_view_cost_rates: "Oglejte si stopnje stroškov" permission_view_cost_rates: "Oglejte si stopnje stroškov"
permission_view_hourly_rates: "View all hourly rates" permission_view_hourly_rates: "Oglejte si vse urne postavke"
permission_view_own_cost_entries: "View own booked costs" permission_view_own_cost_entries: "View own booked costs"
permission_view_own_hourly_rate: "Oglejte si lastno urno postavko" permission_view_own_hourly_rate: "Oglejte si lastno urno postavko"
permission_view_own_time_entries: "Oglejte si svoj porabljen čas" permission_view_own_time_entries: "Oglejte si svoj porabljen čas"
project_module_costs_module: "Proračun" project_module_costs_module: "Proračun"
text_assign_time_and_cost_entries_to_project: "Projektu dodelite prijavljene ure in stroške" text_assign_time_and_cost_entries_to_project: "Projektu dodelite prijavljene ure in stroške"
text_cost_object_change_type_confirmation: "Ali si prepričan? Ta operacija bo uničila podatke o določeni vrsti proračuna." text_cost_object_change_type_confirmation: "Ali si prepričan? Ta operacija bo uničila podatke o določeni vrsti proračuna."
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?" text_destroy_cost_entries_question: "%{cost_entries} so bili prijavljeni v delovnih paketih, ki jih boste izbrisali. Kaj želiš narediti ?"
text_destroy_time_and_cost_entries: "Izbrišite prijavljene ure in stroške" text_destroy_time_and_cost_entries: "Izbrišite prijavljene ure in stroške"
text_destroy_time_and_cost_entries_question: "%{hours} hours, %{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?" text_destroy_time_and_cost_entries_question: "%{hours} ur , %{cost_entries} so bili prijavljeni v delovnih paketih, ki jih boste izbrisali. Kaj želiš narediti ?"
text_reassign_time_and_cost_entries: "Ponovno dodelite tem delovnemu paketu prijavljene ure in stroške:" text_reassign_time_and_cost_entries: "Ponovno dodelite tem delovnemu paketu prijavljene ure in stroške:"
text_warning_hidden_elements: "Nekateri vnosi so bili morda izključeni iz združevanja." text_warning_hidden_elements: "Nekateri vnosi so bili morda izključeni iz združevanja."
week: "teden" week: "teden"

@ -65,7 +65,7 @@ class OpenProject::XlsExport::XlsViews
end end
def number_format def number_format
"0.0" "0.00"
end end
def project_representation(value) def project_representation(value)

@ -28,21 +28,21 @@
require 'spec_helper' require 'spec_helper'
describe 'OAuth authorization code flow', type: :feature, js: true do describe 'OAuth authorization code flow',
type: :feature,
js: true do
let!(:user) { FactoryBot.create(:user) } let!(:user) { FactoryBot.create(:user) }
let!(:app) { FactoryBot.create(:oauth_application, name: 'Cool API app!') } let!(:redirect_uri) { 'urn:ietf:wg:oauth:2.0:oob' }
let!(:allowed_redirect_uri) { redirect_uri }
let!(:app) { FactoryBot.create(:oauth_application, name: 'Cool API app!', redirect_uri: allowed_redirect_uri) }
let(:client_secret) { app.plaintext_secret } let(:client_secret) { app.plaintext_secret }
def oauth_path(client_id) def oauth_path(client_id, redirect_url)
"/oauth/authorize?response_type=code&client_id=#{client_id}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=api_v3" "/oauth/authorize?response_type=code&client_id=#{client_id}&redirect_uri=#{CGI.escape(redirect_url)}&scope=api_v3"
end
before do
# Do not login, will do that in the oauth flow
end end
it 'can authorize and manage an OAuth application grant' do it 'can authorize and manage an OAuth application grant' do
visit oauth_path app.uid visit oauth_path app.uid, redirect_uri
# Expect we're guided to the login screen # Expect we're guided to the login screen
login_with user.login, 'adminADMIN!', visit_signin_path: false login_with user.login, 'adminADMIN!', visit_signin_path: false
@ -54,6 +54,20 @@ describe 'OAuth authorization code flow', type: :feature, js: true do
expect(page).to have_selector('li strong', text: I18n.t('oauth.scopes.api_v3')) expect(page).to have_selector('li strong', text: I18n.t('oauth.scopes.api_v3'))
expect(page).to have_selector('li', text: I18n.t('oauth.scopes.api_v3_text')) expect(page).to have_selector('li', text: I18n.t('oauth.scopes.api_v3_text'))
first = true
allow_any_instance_of(::OAuth::AuthBaseController)
.to receive(:allowed_forms).and_wrap_original do |m|
forms = m.call
# Multiple requests end up here with one not containing the request url
if first
expect(forms).to include redirect_uri
first = false
end
forms
end
# Authorize # Authorize
find('input.button[value="Authorize"]').click find('input.button[value="Authorize"]').click
@ -108,7 +122,7 @@ describe 'OAuth authorization code flow', type: :feature, js: true do
end end
it 'does not authenticate unknown applications' do it 'does not authenticate unknown applications' do
visit oauth_path 'WAT' visit oauth_path 'WAT', redirect_uri
# Expect we're guided to the login screen # Expect we're guided to the login screen
login_with user.login, 'adminADMIN!', visit_signin_path: false login_with user.login, 'adminADMIN!', visit_signin_path: false
@ -120,4 +134,34 @@ describe 'OAuth authorization code flow', type: :feature, js: true do
user.oauth_grants.reload user.oauth_grants.reload
expect(user.oauth_grants.count).to eq 0 expect(user.oauth_grants.count).to eq 0
end end
# Selenium can't return response headers
context 'in browser that can log response headers', js: false do
before do
login_as user
end
context 'with real urls as allowed redirect uris' do
let!(:redirect_uri) { "https://foo.com/foo " }
let!(:allowed_redirect_uri) { "#{redirect_uri} https://bar.com/bar" }
it 'can authorize and manage an OAuth application grant' do
visit oauth_path app.uid, redirect_uri
allow_any_instance_of(::OAuth::AuthBaseController)
.to receive(:allowed_forms).and_wrap_original do |m|
forms = m.call
expect(forms).to include 'https://foo.com/'
expect(forms).to include 'https://bar.com/'
forms
end
# Check that the hosts of allowed redirection urls are present in the content security policy
expect(page.response_headers['content-security-policy']).to(
include("form-action 'self' https://foo.com/ https://bar.com/;")
)
end
end
end
end end

@ -57,9 +57,6 @@ describe 'Logging time within the work package view', type: :feature, js: true d
# a click on save creates a time entry # a click on save creates a time entry
time_logging_modal.perform_action 'Create' time_logging_modal.perform_action 'Create'
wp_page.expect_and_dismiss_notification message: 'Successful creation.' wp_page.expect_and_dismiss_notification message: 'Successful creation.'
# the value is updated automatically
spent_time_field.expect_display_value '1 h'
end end
context 'as an admin' do context 'as an admin' do
@ -75,6 +72,9 @@ describe 'Logging time within the work package view', type: :feature, js: true d
spent_time_field.open_time_log_modal spent_time_field.open_time_log_modal
log_time_via_modal log_time_via_modal
# the value is updated automatically
spent_time_field.expect_display_value '1 h'
end end
it 'the context menu entry to log time leads to the modal' do it 'the context menu entry to log time leads to the modal' do
@ -83,6 +83,9 @@ describe 'Logging time within the work package view', type: :feature, js: true d
find('.menu-item', text: 'Log time').click find('.menu-item', text: 'Log time').click
log_time_via_modal log_time_via_modal
# the value is updated automatically
spent_time_field.expect_display_value '1 h'
end end
end end
@ -98,4 +101,30 @@ describe 'Logging time within the work package view', type: :feature, js: true d
spent_time_field.expect_display_value '-' spent_time_field.expect_display_value '-'
end end
end end
context 'within the table' do
let(:wp_table) { Pages::WorkPackagesTable.new(project) }
let(:second_work_package) { FactoryBot.create :work_package, project: project }
let(:query) { FactoryBot.create :public_query, project: project, column_names: ['subject', 'spent_hours'] }
before do
work_package
second_work_package
login_as(admin)
wp_table.visit_query query
loading_indicator_saveguard
end
it 'shows no logging button within the display field' do
wp_table.expect_work_package_listed work_package, second_work_package
find('tr:nth-of-type(1) .wp-table--cell-td.spentTime .icon-time').click
log_time_via_modal
expect(page).to have_selector('tr:nth-of-type(1) .wp-table--cell-td.spentTime', text: '1 h')
expect(page).to have_selector('tr:nth-of-type(2) .wp-table--cell-td.spentTime', text: '-')
end
end
end end

@ -130,6 +130,32 @@ describe 'Work package table context menu', js: true do
menu.open_for(work_package) menu.open_for(work_package)
menu.expect_options ['Add predecessor', 'Add follower'] menu.expect_options ['Add predecessor', 'Add follower']
end end
describe 'creating work packages' do
let!(:priority) { FactoryBot.create :issue_priority, is_default: true }
let!(:status) { FactoryBot.create :default_status }
let!(:type) { FactoryBot.create :type_task }
let!(:project) { FactoryBot.create :project, types: [type] }
let!(:work_package) { FactoryBot.create :work_package, project: project, type: type, status: status, priority: priority }
let(:wp_table) { Pages::WorkPackagesTable.new project }
it 'can create a new child from the context menu (Regression #33329)' do
goto_context_menu true
menu.choose('Create new child')
expect(page).to have_selector('.inline-edit--container.subject input')
expect(current_url).to match(/.*\/create_new\?.*(\&)*parent_id=#{work_package.id.to_s}/)
split_view = ::Pages::SplitWorkPackageCreate.new project: work_package.project
subject = split_view.edit_field(:subject)
subject.set_value 'Child task'
subject.submit_by_enter
split_view.expect_and_dismiss_notification message: 'Successful creation.'
expect(page).to have_selector('.wp-breadcrumb', text: "Parent:\n#{work_package.subject}")
wp = WorkPackage.last
expect(wp.parent).to eq work_package
end
end
end end
context 'in the card view' do context 'in the card view' do

@ -41,6 +41,19 @@ describe LdapAuthSource, type: :model do
expect(a.reload.attr_firstname).to eq 'givenName' expect(a.reload.attr_firstname).to eq 'givenName'
end end
describe 'overriding tls_options',
with_config: { ldap_tls_options: { ca_file: '/path/to/ca/file' } } do
it 'sets the encryption options for start_tls' do
ldap = LdapAuthSource.new tls_mode: :start_tls
expect(ldap.send(:ldap_encryption)).to eq(method: :start_tls, tls_options: { 'ca_file' => '/path/to/ca/file' })
end
it 'does nothing for plain_ldap' do
ldap = LdapAuthSource.new tls_mode: :plain_ldap
expect(ldap.send(:ldap_encryption)).to eq nil
end
end
describe 'with live LDAP' do describe 'with live LDAP' do
before(:all) do before(:all) do
ldif = Rails.root.join('spec/fixtures/ldap/users.ldif') ldif = Rails.root.join('spec/fixtures/ldap/users.ldif')

Loading…
Cancel
Save