LDAP authentication support (#815)
This commit is contained in:
@@ -7,4 +7,12 @@ end_of_line = LF
|
||||
|
||||
[*.php]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
indent_size = 4
|
||||
|
||||
[*.yaml]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1 +1,2 @@
|
||||
github: kevinpapst
|
||||
github: [kevinpapst]
|
||||
custom: https://www.kimai.org/donate/
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -7,6 +7,6 @@ A clear and concise description of what this pull request adds or changes.
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||
|
||||
## Checklist
|
||||
- [ ] I verified that my code applies to the guidelines (`composer code-check`)
|
||||
- [ ] I verified that my code applies to the guidelines (`composer kimai:code-check`)
|
||||
- [ ] I updated the documentation (see [here](https://github.com/kimai/www.kimai.org/tree/master/_documentation))
|
||||
- [ ] I agree that this code is used in Kimai and will be published under the [MIT license](https://github.com/kevinpapst/kimai2/blob/master/LICENSE)
|
||||
|
||||
@@ -10,6 +10,7 @@ build:
|
||||
tests:
|
||||
override:
|
||||
- js-scrutinizer-run
|
||||
- php-scrutinizer-run
|
||||
tests:
|
||||
tests:
|
||||
override:
|
||||
|
||||
@@ -31,8 +31,8 @@ install:
|
||||
- composer install
|
||||
|
||||
script:
|
||||
- composer codestyle
|
||||
- composer phpstan
|
||||
- composer kimai:codestyle
|
||||
- composer kimai:phpstan
|
||||
- if [[ $CODECOVERAGE == 1 ]]; then vendor/bin/phpunit tests/ --coverage-clover=coverage.xml; else vendor/bin/phpunit tests/; fi;
|
||||
- cp tests/.env.dist.sqlite .env
|
||||
- bin/console doctrine:database:create -n
|
||||
|
||||
78
README.md
78
README.md
@@ -1,72 +1,68 @@
|
||||
# Kimai 2 - Time-tracking made easy
|
||||
|
||||
Kimai - the open source time-tracker application with a mobile-first approach (read more at the [official website](https://www.kimai.org)).
|
||||
# Kimai 2 - online time-tracker
|
||||
|
||||
[](https://packagist.org/packages/kevinpapst/kimai2)
|
||||
[](https://packagist.org/packages/kevinpapst/kimai2)
|
||||
[](https://travis-ci.org/kevinpapst/kimai2)
|
||||
[](https://codecov.io/gh/kevinpapst/kimai2)
|
||||
[](https://gitter.im/kimai2/support)
|
||||
|
||||
Kimai is a free, open source and online time-tracking software designed for small businesses and freelancers.
|
||||
It is built with modern technologies such as Symfony, Bootstrap, RESTful API, responsive and mobile-ready etc.
|
||||
|
||||
## Introduction
|
||||
|
||||
This is the _reloaded_ version of the open source timetracker Kimai.
|
||||
It is built from scratch and doesn't share any source code with its [predecessor](http://www.kimai.org).
|
||||
But it adapts the same ideas and a clean & simple UI for your time-tracking experience.
|
||||
|
||||
By now it is in an almost-stable development phase, usable and with most advanced features from Kimai 1.
|
||||
You can even [import your data](https://www.kimai.org/documentation/migration-v1.html) and start testing and using it today.
|
||||
|
||||
Kimai is a [multi-language application](https://www.kimai.org/documentation/translations.html) and already translated to english, german, italian, french, spanish, russian, arabic, hungarian, portuguese (brazilian), swedish and japanese.
|
||||
- [Home](https://www.kimai.org) - The house of Kimai
|
||||
- [Blog](https://www.kimai.org/blog/) - Get the latest news
|
||||
- [Documentation](https://www.kimai.org/documentation/) - Learn how to use
|
||||
- [Translations](https://www.kimai.org/documentation/translations.html) - Kimai in your language
|
||||
- [Migration](https://www.kimai.org/documentation/migration-v1.html) - Import data from v1
|
||||
|
||||
### Requirements
|
||||
|
||||
- PHP 7.2 or higher (test your system compatibility with the [requirements-checker](http://symfony.com/doc/current/reference/requirements.html))
|
||||
- The PHP extensions [xml](http://php.net/manual/en/book.xml.php), [mbstring](http://php.net/manual/en/book.mbstring.php), [gd](http://php.net/manual/en/book.image.php), [intl](https://php.net/manual/en/book.intl.php), [zip](https://php.net/manual/en/book.zip.php) and [PDO](https://php.net/manual/en/book.pdo.php) with either [pdo_sqlite](https://php.net/manual/en/ref.pdo-sqlite.php) or [pdo_mysql](https://php.net/manual/en/ref.pdo-mysql.php) enabled
|
||||
- If you use MariaDB, make sure its at least v10.2.7 (see [FAQ](https://www.kimai.org/documentation/faq.html))
|
||||
- A modern browser, Kimai v2 might be broken on old browsers like IE 10
|
||||
- PHP 7.2 or higher
|
||||
- Database (MySQL, MariaDB, SQLite)
|
||||
- Webserver (nginx, Apache)
|
||||
- A modern browser
|
||||
- [Other libraries](https://www.kimai.org/download/)
|
||||
|
||||
## Documentation
|
||||
### About
|
||||
|
||||
Looking for more information about Kimai 2? Check out our detailed [documentation](https://www.kimai.org/documentation/).
|
||||
This new version of the open source timetracker Kimai. It is in a stable development phase, usable in production and
|
||||
with most advanced features from Kimai 1 and many new ones:
|
||||
|
||||
### Installation
|
||||
JSON API, invoicing, data exports, multi-timer and punch-in punch-out mode, tagging, multi-user and multi-timezones,
|
||||
LDAP and built-in authentication, customizable role permissions, responsive and usable on your mobile device,
|
||||
hourly and fixed rates, advanced filtering, support for plugins and many more.
|
||||
|
||||
There are multiple ways to install Kimai, all of them described in the [installation docu](https://www.kimai.org/documentation/installation.html):
|
||||
## Installation
|
||||
|
||||
- [Recommended installation with GIT and Composer](https://www.kimai.org/documentation/installation.html#recommended-setup)
|
||||
- [Development setup](https://www.kimai.org/documentation/installation.html#development-installation)
|
||||
- [Docker](https://www.kimai.org/documentation/docker.html)
|
||||
- [1-click installations](https://www.kimai.org/documentation/installation.html#hosting-and-1-click-installations)
|
||||
- [FTP](https://www.kimai.org/documentation/installation.html#ftp-installation)
|
||||
- [Recommended setup](https://www.kimai.org/documentation/installation.html#recommended-setup) - with Git and Composer
|
||||
- [Docker](https://www.kimai.org/documentation/docker.html) - containerized
|
||||
- [Development](https://www.kimai.org/documentation/installation.html#development-installation) - on your local machine
|
||||
- [1-click installer](https://www.kimai.org/documentation/installation.html#hosting-and-1-click-installations) - hosted environments
|
||||
- [FTP](https://www.kimai.org/documentation/installation.html#ftp-installation) - unfortunately still widely used
|
||||
|
||||
### Updating Kimai
|
||||
|
||||
Read the following documentations before you start your upgrade:
|
||||
- [Update Kimai](https://www.kimai.org/documentation/updates.html) - the documentation
|
||||
- [UPGRADING guide](UPGRADING.md) - version specific steps
|
||||
|
||||
- The [update documentation](https://www.kimai.org/documentation/updates.html)
|
||||
- The version specific [UPGRADING guide](UPGRADING.md)
|
||||
- The [release information](https://github.com/kevinpapst/kimai2/releases)
|
||||
### Plugins
|
||||
|
||||
- [Plugin marketplace](https://www.kimai.org/store/) - users find existing plugins here
|
||||
- [Developer documentation](https://www.kimai.org/documentation/developers.html) - how to create a plugin
|
||||
|
||||
## Roadmap and releases
|
||||
|
||||
You can see a rough development roadmap in the [Milestones](https://github.com/kevinpapst/kimai2/milestones) sections.
|
||||
It is open for changes and input from the community, your [ideas and questions](https://github.com/kevinpapst/kimai2/issues) are welcome!
|
||||
It is open for changes and input from the community, your [ideas and questions](https://github.com/kevinpapst/kimai2/issues) are welcome.
|
||||
|
||||
> Kimai 2 uses a rolling release concept for delivering updates.
|
||||
> You can upgrade Kimai at any time, you don't need to wait for the next official release. Read the [upgrade docs](UPGRADING.md) first!
|
||||
> You can upgrade Kimai at any time, you don't need to wait for the next official release.
|
||||
|
||||
Release versions will be created on a regular base (approx. 1 per month) and you can use these tags if you are familiar with Git,
|
||||
but we cannot provide support for any specific version (especially older ones).
|
||||
Release versions will be created on a regular base (approx. every second month) and you can use these tags if you are familiar with git.
|
||||
Every code change, whether it's a new feature or a bug fix, will be done on the master branch.
|
||||
I have to do it this way, as I develop Kimai in my free time and want to put most
|
||||
effort into the software instead of backporting changes for old versions.
|
||||
|
||||
## Plugins for Kimai 2
|
||||
|
||||
Kimai 2 was built on top of the Symfony framework and can be extended in various ways:
|
||||
|
||||
- Users looking for existing plugins go to our [plugin marketplace](https://www.kimai.org/store/)
|
||||
- Developer start with the [developer documentation](https://www.kimai.org/documentation/developers.html)
|
||||
I have to do it this way, as I develop Kimai in my free time and want to put my effort into the software instead of backporting changes for old versions.
|
||||
|
||||
## Credits
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
"php": "^7.2",
|
||||
"ext-gd": "*",
|
||||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-zip": "*",
|
||||
"ext-json": "*",
|
||||
"beberlei/doctrineextensions": "^1.2",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.0",
|
||||
"erusev/parsedown": "^1.6",
|
||||
@@ -34,6 +34,7 @@
|
||||
"ocramius/proxy-manager": "2.1.1",
|
||||
"phpoffice/phpspreadsheet": "^1.4",
|
||||
"phpoffice/phpword": "^0.15.0",
|
||||
"psr/log": "^1.1",
|
||||
"sensio/framework-extra-bundle": "^5.2",
|
||||
"symfony/asset": "^4.0",
|
||||
"symfony/console": "^4.0",
|
||||
@@ -48,7 +49,7 @@
|
||||
"symfony/monolog-bundle": "^3.1",
|
||||
"symfony/orm-pack": "^1.0",
|
||||
"symfony/profiler-pack": "^1.0",
|
||||
"symfony/security-bundle": "^4.0",
|
||||
"symfony/security-bundle": "~4.2.0",
|
||||
"symfony/security-csrf": "^4.0",
|
||||
"symfony/serializer": "^4.0",
|
||||
"symfony/swiftmailer-bundle": "^3.1",
|
||||
@@ -59,13 +60,15 @@
|
||||
"symfony/webpack-encore-bundle": "^1.5",
|
||||
"symfony/yaml": "^4.0",
|
||||
"twig/extensions": "^1.5",
|
||||
"white-october/pagerfanta-bundle": "^1.1"
|
||||
"white-october/pagerfanta-bundle": "^1.1",
|
||||
"zendframework/zend-ldap": "^2.10"
|
||||
},
|
||||
"require-dev": {
|
||||
"dama/doctrine-test-bundle": "^5.0",
|
||||
"friendsofphp/php-cs-fixer": "^2.10",
|
||||
"phpstan/phpstan": "^0.11.7",
|
||||
"phpstan/phpstan-doctrine": "^0.11.4",
|
||||
"phpstan/phpstan-phpunit": "^0.11.2",
|
||||
"phpstan/phpstan-symfony": "^0.11.6",
|
||||
"phpunit/phpunit": "^7.0",
|
||||
"symfony/browser-kit": "^4.0",
|
||||
@@ -110,17 +113,20 @@
|
||||
"post-update-cmd": [
|
||||
"@auto-scripts"
|
||||
],
|
||||
"code-check": [
|
||||
"@codestyle",
|
||||
"@phpstan",
|
||||
"@tests"
|
||||
"kimai:code-check": [
|
||||
"@kimai:codestyle",
|
||||
"@kimai:phpstan",
|
||||
"@kimai:tests"
|
||||
],
|
||||
"tests": "vendor/bin/phpunit tests/",
|
||||
"tests-unit": "vendor/bin/phpunit --exclude-group integration tests/",
|
||||
"tests-integration": "vendor/bin/phpunit --group integration tests/",
|
||||
"phpstan": "vendor/bin/phpstan analyse src --level=3",
|
||||
"codestyle": "vendor/bin/php-cs-fixer fix --dry-run --verbose --show-progress=none",
|
||||
"codestyle-fix": "vendor/bin/php-cs-fixer fix"
|
||||
"kimai:tests": "vendor/bin/phpunit tests/",
|
||||
"kimai:tests-unit": "vendor/bin/phpunit --exclude-group integration tests/",
|
||||
"kimai:tests-integration": "vendor/bin/phpunit --group integration tests/",
|
||||
"kimai:phpstan": [
|
||||
"vendor/bin/phpstan analyse src -c phpstan.neon --level=3",
|
||||
"vendor/bin/phpstan analyse tests -c tests/phpstan.neon --level=4"
|
||||
],
|
||||
"kimai:codestyle": "vendor/bin/php-cs-fixer fix --dry-run --verbose --show-progress=none",
|
||||
"kimai:codestyle-fix": "vendor/bin/php-cs-fixer fix"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
|
||||
116
composer.lock
generated
116
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "a25872c6e7ee3cc2df923ec8a26b8265",
|
||||
"content-hash": "ed199b91b3e5eb0b9d2a1eb00ea11cba",
|
||||
"packages": [
|
||||
{
|
||||
"name": "beberlei/DoctrineExtensions",
|
||||
@@ -8618,6 +8618,59 @@
|
||||
],
|
||||
"time": "2018-04-25T15:33:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "zendframework/zend-ldap",
|
||||
"version": "2.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/zendframework/zend-ldap.git",
|
||||
"reference": "b63c7884a08d3a6bda60ebcf7d6238cf8ad89f49"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/zendframework/zend-ldap/zipball/b63c7884a08d3a6bda60ebcf7d6238cf8ad89f49",
|
||||
"reference": "b63c7884a08d3a6bda60ebcf7d6238cf8ad89f49",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-ldap": "*",
|
||||
"php": "^5.6 || ^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-mock/php-mock-phpunit": "^1.1.2 || ^2.1.1",
|
||||
"phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2",
|
||||
"zendframework/zend-coding-standard": "~1.0.0",
|
||||
"zendframework/zend-config": "^2.5",
|
||||
"zendframework/zend-eventmanager": "^2.6.3 || ^3.0.1",
|
||||
"zendframework/zend-stdlib": "^2.7 || ^3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"zendframework/zend-eventmanager": "Zend\\EventManager component"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.10.x-dev",
|
||||
"dev-develop": "2.11.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Zend\\Ldap\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"description": "Provides support for LDAP operations including but not limited to binding, searching and modifying entries in an LDAP directory",
|
||||
"keywords": [
|
||||
"ZendFramework",
|
||||
"ldap",
|
||||
"zf"
|
||||
],
|
||||
"time": "2018-07-05T05:05:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "zircote/swagger-php",
|
||||
"version": "2.0.14",
|
||||
@@ -9910,6 +9963,63 @@
|
||||
"description": "Doctrine extensions for PHPStan",
|
||||
"time": "2019-05-29T06:01:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-phpunit",
|
||||
"version": "0.11.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-phpunit.git",
|
||||
"reference": "fbf2ad56c3b13189d29655e226c9b1da47c2fad9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/fbf2ad56c3b13189d29655e226c9b1da47c2fad9",
|
||||
"reference": "fbf2ad56c3b13189d29655e226c9b1da47c2fad9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"nikic/php-parser": "^4.0",
|
||||
"php": "~7.1",
|
||||
"phpstan/phpdoc-parser": "^0.3",
|
||||
"phpstan/phpstan": "^0.11.4"
|
||||
},
|
||||
"conflict": {
|
||||
"phpunit/phpunit": "<7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"consistence/coding-standard": "^3.0.1",
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "^0.4.4",
|
||||
"jakub-onderka/php-parallel-lint": "^1.0",
|
||||
"phing/phing": "^2.16.0",
|
||||
"phpstan/phpstan-strict-rules": "^0.11",
|
||||
"phpunit/phpunit": "^7.0",
|
||||
"satooshi/php-coveralls": "^1.0",
|
||||
"slevomat/coding-standard": "^4.5.2"
|
||||
},
|
||||
"type": "phpstan-extension",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "0.11-dev"
|
||||
},
|
||||
"phpstan": {
|
||||
"includes": [
|
||||
"extension.neon",
|
||||
"rules.neon"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PHPStan\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "PHPUnit extensions and rules for PHPStan",
|
||||
"time": "2019-05-17T17:50:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-symfony",
|
||||
"version": "0.11.6",
|
||||
@@ -11206,10 +11316,10 @@
|
||||
"php": "^7.2",
|
||||
"ext-gd": "*",
|
||||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-zip": "*",
|
||||
"ext-json": "*"
|
||||
"ext-zip": "*"
|
||||
},
|
||||
"platform-dev": []
|
||||
}
|
||||
|
||||
@@ -314,3 +314,12 @@ kimai:
|
||||
data_dir: '%kernel.project_dir%/var/data'
|
||||
plugin_dir: '%kernel.project_dir%/var/plugins'
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# LDAP
|
||||
# Connect to your companies directory server, see https://www.kimai.org/documentation/ldap.html
|
||||
# --------------------------------------------------------------------------------
|
||||
# ldap:
|
||||
# active: true
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
@@ -3,6 +3,11 @@ security:
|
||||
App\Entity\User: bcrypt
|
||||
|
||||
providers:
|
||||
chain_provider:
|
||||
chain:
|
||||
providers: [fos_userbundle, kimai_ldap]
|
||||
kimai_ldap:
|
||||
id: App\Ldap\LdapUserProvider
|
||||
fos_userbundle:
|
||||
id: fos_user.user_provider.username_email
|
||||
|
||||
@@ -26,8 +31,13 @@ security:
|
||||
lifetime: 604800
|
||||
path: /
|
||||
|
||||
# activate the LdapAuthenticationProvider
|
||||
kimai_ldap: ~
|
||||
|
||||
# activate all configured user provider
|
||||
provider: chain_provider
|
||||
|
||||
form_login:
|
||||
provider: fos_userbundle
|
||||
check_path: fos_user_security_check
|
||||
login_path: fos_user_security_login
|
||||
csrf_token_generator: security.csrf.token_manager
|
||||
|
||||
@@ -13,7 +13,7 @@ services:
|
||||
# fetching services directly from the container via $container->get() won't work.
|
||||
# The best practice is to be explicit about your dependencies anyway.
|
||||
bind:
|
||||
$projectDirectory: "%kernel.project_dir%"
|
||||
$projectDirectory: '%kernel.project_dir%'
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
@@ -36,34 +36,37 @@ services:
|
||||
|
||||
App\Repository\WidgetRepository:
|
||||
arguments:
|
||||
$widgets: "%kimai.widgets%"
|
||||
$widgets: '%kimai.widgets%'
|
||||
|
||||
App\Controller\DashboardController:
|
||||
arguments:
|
||||
$dashboard: "%kimai.dashboard%"
|
||||
$dashboard: '%kimai.dashboard%'
|
||||
|
||||
App\Configuration\LanguageFormattings:
|
||||
arguments: ["%kimai.languages%"]
|
||||
arguments: ['%kimai.languages%']
|
||||
|
||||
App\Configuration\TimesheetConfiguration:
|
||||
arguments:
|
||||
$settings: "%kimai.timesheet%"
|
||||
$settings: '%kimai.timesheet%'
|
||||
|
||||
App\Configuration\CalendarConfiguration:
|
||||
arguments:
|
||||
$settings: "%kimai.calendar%"
|
||||
$settings: '%kimai.calendar%'
|
||||
|
||||
App\Configuration\FormConfiguration:
|
||||
arguments:
|
||||
$settings: "%kimai.defaults%"
|
||||
$settings: '%kimai.defaults%'
|
||||
|
||||
App\Configuration\SystemConfiguration:
|
||||
arguments:
|
||||
$settings: "%kimai.config%"
|
||||
$settings: '%kimai.config%'
|
||||
|
||||
App\Configuration\ThemeConfiguration:
|
||||
arguments:
|
||||
$settings: "%kimai.theme%"
|
||||
$settings: '%kimai.theme%'
|
||||
|
||||
App\Configuration\LdapConfiguration:
|
||||
arguments: ['%kimai.ldap%']
|
||||
|
||||
App\Utils\MPdfConverter:
|
||||
arguments: ['%kernel.cache_dir%']
|
||||
@@ -95,11 +98,8 @@ services:
|
||||
# FORMS
|
||||
# ================================================================================
|
||||
|
||||
App\Form\Type\UserRoleType:
|
||||
arguments: ["%security.role_hierarchy.roles%"]
|
||||
|
||||
App\Form\Type\LanguageType:
|
||||
arguments: ["%app_locales%"]
|
||||
arguments: ['%app_locales%']
|
||||
|
||||
# ================================================================================
|
||||
# THEME
|
||||
@@ -113,17 +113,31 @@ services:
|
||||
# ================================================================================
|
||||
|
||||
App\Timesheet\Calculator\DurationCalculator:
|
||||
arguments: ["%kimai.timesheet.rounding%"]
|
||||
arguments: ['%kimai.timesheet.rounding%']
|
||||
|
||||
App\Timesheet\Calculator\RateCalculator:
|
||||
arguments: ["%kimai.timesheet.rates%"]
|
||||
arguments: ['%kimai.timesheet.rates%']
|
||||
|
||||
# ================================================================================
|
||||
# SECURITY & VOTER
|
||||
# ================================================================================
|
||||
|
||||
App\Security\RoleService:
|
||||
arguments: ['%security.role_hierarchy.roles%']
|
||||
|
||||
App\Security\RolePermissionManager:
|
||||
arguments: ["%kimai.permissions%"]
|
||||
arguments: ['%kimai.permissions%']
|
||||
|
||||
# ================================================================================
|
||||
# LDAP
|
||||
# ================================================================================
|
||||
|
||||
Zend\Ldap\Ldap:
|
||||
class: App\Ldap\ZendLdap
|
||||
|
||||
kimai_ldap.security.authentication.provider:
|
||||
class: App\Ldap\LdapAuthenticationProvider
|
||||
arguments: ['@App\Security\UserChecker', '', '', '', '@App\Configuration\LdapConfiguration', '%security.authentication.hide_user_not_found%']
|
||||
|
||||
# ================================================================================
|
||||
# REPOSITORIES
|
||||
|
||||
43
src/Configuration/LdapConfiguration.php
Normal file
43
src/Configuration/LdapConfiguration.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Configuration;
|
||||
|
||||
class LdapConfiguration
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $settings = [];
|
||||
|
||||
public function __construct(array $settings)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
public function isActivated(): bool
|
||||
{
|
||||
return (bool) $this->settings['active'];
|
||||
}
|
||||
|
||||
public function getRoleParameters(): array
|
||||
{
|
||||
return (array) $this->settings['role'];
|
||||
}
|
||||
|
||||
public function getUserParameters(): array
|
||||
{
|
||||
return (array) $this->settings['user'];
|
||||
}
|
||||
|
||||
public function getConnectionParameters(): array
|
||||
{
|
||||
return (array) $this->settings['connection'];
|
||||
}
|
||||
}
|
||||
@@ -29,12 +29,12 @@ class AppExtension extends Extension
|
||||
$config = $this->processConfiguration($configuration, $configs);
|
||||
} catch (InvalidConfigurationException $e) {
|
||||
trigger_error('Found invalid "kimai" configuration: ' . $e->getMessage());
|
||||
$config = [];
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// @deprecated since 0.9, duration_only will be removed with 1.0
|
||||
if (isset($config['timesheet']['duration_only'])) {
|
||||
trigger_error('Configuration "kimai.timesheet.duration_only" is deprecated, please remove it', E_USER_DEPRECATED);
|
||||
@trigger_error('Configuration "kimai.timesheet.duration_only" is deprecated, please remove it', E_USER_DEPRECATED);
|
||||
if (true === $config['timesheet']['duration_only'] && 'duration_only' !== $config['timesheet']['mode']) {
|
||||
trigger_error('Found ambiguous configuration. Please remove "kimai.timesheet.duration_only" and set "kimai.timesheet.mode" instead.');
|
||||
}
|
||||
@@ -60,6 +60,25 @@ class AppExtension extends Extension
|
||||
$container->setParameter('kimai.timesheet', $config['timesheet']);
|
||||
$container->setParameter('kimai.timesheet.rates', $config['timesheet']['rates']);
|
||||
$container->setParameter('kimai.timesheet.rounding', $config['timesheet']['rounding']);
|
||||
|
||||
$this->setLdapParameter($config['ldap'], $container);
|
||||
}
|
||||
|
||||
protected function setLdapParameter(array $config, ContainerBuilder $container)
|
||||
{
|
||||
if (!isset($config['connection']['baseDn'])) {
|
||||
$config['connection']['baseDn'] = $config['user']['baseDn'];
|
||||
}
|
||||
|
||||
if (empty($config['connection']['accountFilterFormat']) && $config['connection']['bindRequiresDn']) {
|
||||
$filter = '';
|
||||
if (!empty($config['user']['filter'])) {
|
||||
$filter = $config['user']['filter'];
|
||||
}
|
||||
$config['connection']['accountFilterFormat'] = '(&' . $filter . '(' . $config['user']['usernameAttribute'] . '=%s))';
|
||||
}
|
||||
|
||||
$container->setParameter('kimai.ldap', $config);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -62,6 +62,7 @@ class Configuration implements ConfigurationInterface
|
||||
->append($this->getWidgetsNode())
|
||||
->append($this->getDefaultsNode())
|
||||
->append($this->getPermissionsNode())
|
||||
->append($this->getLdapNode())
|
||||
->end()
|
||||
->end();
|
||||
|
||||
@@ -100,7 +101,7 @@ class Configuration implements ConfigurationInterface
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->scalarPrototype()->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->integerNode('begin')
|
||||
@@ -141,7 +142,7 @@ class Configuration implements ConfigurationInterface
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->scalarPrototype()->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->floatNode('factor')
|
||||
@@ -258,7 +259,7 @@ class Configuration implements ConfigurationInterface
|
||||
->children()
|
||||
->arrayNode('days')
|
||||
->requiresAtLeastOneElement()
|
||||
->prototype('integer')->end()
|
||||
->integerPrototype()->end()
|
||||
->defaultValue([1, 2, 3, 4, 5])
|
||||
->end()
|
||||
->scalarNode('begin')->defaultValue('08:00')->end()
|
||||
@@ -449,7 +450,7 @@ class Configuration implements ConfigurationInterface
|
||||
->arrayPrototype()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->scalarPrototype()->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->end()
|
||||
@@ -459,7 +460,7 @@ class Configuration implements ConfigurationInterface
|
||||
->arrayPrototype()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->scalarPrototype()->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->end()
|
||||
@@ -469,7 +470,7 @@ class Configuration implements ConfigurationInterface
|
||||
->arrayPrototype()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->scalarPrototype()->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->defaultValue([
|
||||
@@ -484,4 +485,130 @@ class Configuration implements ConfigurationInterface
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
protected function getLdapNode()
|
||||
{
|
||||
$treeBuilder = new TreeBuilder('ldap');
|
||||
$node = $treeBuilder->getRootNode();
|
||||
|
||||
$node
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->booleanNode('active')->defaultFalse()->end()
|
||||
->arrayNode('connection')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('host')->defaultNull()->end()
|
||||
->scalarNode('port')->defaultValue(389)->end()
|
||||
->scalarNode('useStartTls')->defaultFalse()->end()
|
||||
->scalarNode('useSsl')->defaultFalse()->end()
|
||||
->scalarNode('username')->end()
|
||||
->scalarNode('password')->end()
|
||||
->scalarNode('bindRequiresDn')->defaultTrue()->end()
|
||||
->scalarNode('baseDn')->end()
|
||||
->scalarNode('accountCanonicalForm')->end()
|
||||
->scalarNode('accountDomainName')->end()
|
||||
->scalarNode('accountDomainNameShort')->end()
|
||||
->scalarNode('accountFilterFormat')
|
||||
->defaultNull()
|
||||
->validate()
|
||||
->ifTrue(static function ($v) {
|
||||
if (empty($v)) {
|
||||
return false;
|
||||
}
|
||||
if ($v[0] !== '(' || (substr_count($v, '(') !== substr_count($v, ')'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (substr_count($v, '%s') !== 1);
|
||||
})
|
||||
->thenInvalid('The accountFilterFormat must be enclosed by a matching number of parentheses "()" and contain one "%%s" replacer for the username')
|
||||
->end()
|
||||
->end()
|
||||
->scalarNode('allowEmptyPassword')->end()
|
||||
->scalarNode('optReferrals')->end()
|
||||
->scalarNode('tryUsernameSplit')->end()
|
||||
->scalarNode('networkTimeout')->end()
|
||||
->end()
|
||||
->validate()
|
||||
->ifTrue(static function ($v) {
|
||||
return $v['useSsl'] && $v['useStartTls'];
|
||||
})
|
||||
->thenInvalid('The ldap.connection.useSsl and ldap.connection.useStartTls options are mutually exclusive.')
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('user')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('baseDn')->defaultNull()->end()
|
||||
->scalarNode('filter')
|
||||
->defaultValue('')
|
||||
->validate()
|
||||
->ifTrue(static function ($v) {
|
||||
if (empty($v)) {
|
||||
return false;
|
||||
}
|
||||
if ($v[0] !== '(' || (substr_count($v, '(') !== substr_count($v, ')'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (stripos($v, '%s') !== false);
|
||||
})
|
||||
->thenInvalid('The ldap.user.filter must be enclosed by a matching number of parentheses "()" and must NOT contain a "%%s" replacer')
|
||||
->end()
|
||||
->end()
|
||||
->scalarNode('usernameAttribute')->defaultValue('uid')->end()
|
||||
->arrayNode('attributes')
|
||||
->defaultValue([])
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
->scalarNode('ldap_attr')->isRequired()->cannotBeEmpty()->end()
|
||||
->scalarNode('user_method')->isRequired()->cannotBeEmpty()->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('role')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('baseDn')->defaultNull()->end()
|
||||
->scalarNode('filter')->end()
|
||||
->scalarNode('usernameAttribute')->defaultValue('dn')->end()
|
||||
->scalarNode('nameAttribute')->defaultValue('cn')->end()
|
||||
->scalarNode('userDnAttribute')->defaultValue('member')->end()
|
||||
->arrayNode('groups')
|
||||
->defaultValue([])
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
->scalarNode('ldap_value')->isRequired()->cannotBeEmpty()->end()
|
||||
->scalarNode('role')->isRequired()->cannotBeEmpty()->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->validate()
|
||||
->ifTrue(static function ($v) {
|
||||
return $v['active'] && !extension_loaded('ldap');
|
||||
})
|
||||
->thenInvalid('LDAP is activated, but the LDAP PHP extension is not loaded.')
|
||||
->end()
|
||||
->validate()
|
||||
->ifTrue(static function ($v) {
|
||||
return $v['active'] && empty($v['connection']['host']);
|
||||
})
|
||||
->thenInvalid('The "ldap.connection.host" config must be set if LDAP is activated.')
|
||||
->end()
|
||||
->validate()
|
||||
->ifTrue(static function ($v) {
|
||||
return $v['active'] && empty($v['user']['baseDn']);
|
||||
})
|
||||
->thenInvalid('The "ldap.user.baseDn" config must be set if LDAP is activated.')
|
||||
->end()
|
||||
;
|
||||
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
@@ -28,7 +30,7 @@ class Activity
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Project
|
||||
* @var Project|null
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project", inversedBy="activities")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
@@ -60,7 +62,7 @@ class Activity
|
||||
private $visible = true;
|
||||
|
||||
/**
|
||||
* @var Timesheet[]
|
||||
* @var Timesheet[]|ArrayCollection
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\Timesheet", mappedBy="activity")
|
||||
*/
|
||||
@@ -70,94 +72,68 @@ class Activity
|
||||
use RatesTrait;
|
||||
use ColorTrait;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
public function __construct()
|
||||
{
|
||||
$this->timesheets = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Timesheet[]
|
||||
* @return Collection<Timesheet>
|
||||
*/
|
||||
public function getTimesheets(): array
|
||||
public function getTimesheets(): Collection
|
||||
{
|
||||
return $this->timesheets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Project
|
||||
*/
|
||||
public function getProject()
|
||||
public function getProject(): ?Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @return Activity
|
||||
*/
|
||||
public function setProject($project)
|
||||
public function setProject(?Project $project): Activity
|
||||
{
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return Activity
|
||||
*/
|
||||
public function setName($name)
|
||||
public function setName(string $name): Activity
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $comment
|
||||
* @return Activity
|
||||
*/
|
||||
public function setComment($comment)
|
||||
public function setComment(?string $comment): Activity
|
||||
{
|
||||
$this->comment = $comment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getComment()
|
||||
public function getComment(): ?string
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $visible
|
||||
* @return Activity
|
||||
*/
|
||||
public function setVisible($visible)
|
||||
public function setVisible(bool $visible): Activity
|
||||
{
|
||||
$this->visible = $visible;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getVisible()
|
||||
public function getVisible(): bool
|
||||
{
|
||||
return $this->visible;
|
||||
}
|
||||
|
||||
@@ -48,10 +48,7 @@ class Configuration
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
@@ -76,14 +73,17 @@ class Configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Given $value will not be serialized before its stored, so it should be a scalar type.
|
||||
* Given $value will not be serialized before its stored, so it should be a scalar type
|
||||
* that can be casted to string.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param string|null|int|bool $value
|
||||
* @return Configuration
|
||||
*/
|
||||
public function setValue($value): Configuration
|
||||
{
|
||||
$this->value = $value;
|
||||
if (null !== $value) {
|
||||
$this->value = (string) $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
@@ -53,7 +55,7 @@ class Customer
|
||||
private $comment;
|
||||
|
||||
/**
|
||||
* @var Project[]
|
||||
* @var Project[]|ArrayCollection
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\Project", mappedBy="customer")
|
||||
*/
|
||||
@@ -151,362 +153,200 @@ class Customer
|
||||
use RatesTrait;
|
||||
use ColorTrait;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
public function __construct()
|
||||
{
|
||||
$this->projects = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set name
|
||||
*
|
||||
* @param string $name
|
||||
* @return Customer
|
||||
*/
|
||||
public function setName($name)
|
||||
public function setName(string $name): Customer
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $number
|
||||
* @return Customer
|
||||
*/
|
||||
public function setNumber(string $number)
|
||||
public function setNumber(?string $number): Customer
|
||||
{
|
||||
$this->number = $number;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNumber(): ?string
|
||||
{
|
||||
return $this->number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set comment
|
||||
*
|
||||
* @param string $comment
|
||||
* @return Customer
|
||||
*/
|
||||
public function setComment($comment)
|
||||
public function setComment(?string $comment): Customer
|
||||
{
|
||||
$this->comment = $comment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comment
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getComment()
|
||||
public function getComment(): ?string
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set visible
|
||||
*
|
||||
* @param bool $visible
|
||||
* @return Customer
|
||||
*/
|
||||
public function setVisible($visible)
|
||||
public function setVisible(bool $visible): Customer
|
||||
{
|
||||
$this->visible = $visible;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visible
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getVisible()
|
||||
public function getVisible(): bool
|
||||
{
|
||||
return $this->visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set company
|
||||
*
|
||||
* @param string $company
|
||||
* @return Customer
|
||||
*/
|
||||
public function setCompany($company)
|
||||
public function setCompany(?string $company): Customer
|
||||
{
|
||||
$this->company = $company;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get company
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCompany()
|
||||
public function getCompany(): ?string
|
||||
{
|
||||
return $this->company;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set contact
|
||||
*
|
||||
* @param string $contact
|
||||
* @return Customer
|
||||
*/
|
||||
public function setContact($contact)
|
||||
public function setContact(?string $contact): Customer
|
||||
{
|
||||
$this->contact = $contact;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contact
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContact()
|
||||
public function getContact(): ?string
|
||||
{
|
||||
return $this->contact;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $address
|
||||
* @return Customer
|
||||
*/
|
||||
public function setAddress($address)
|
||||
public function setAddress(?string $address): Customer
|
||||
{
|
||||
$this->address = $address;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAddress()
|
||||
public function getAddress(): ?string
|
||||
{
|
||||
return $this->address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set country
|
||||
*
|
||||
* @param string $country
|
||||
* @return Customer
|
||||
*/
|
||||
public function setCountry($country)
|
||||
public function setCountry(string $country): Customer
|
||||
{
|
||||
$this->country = $country;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get country
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCountry()
|
||||
public function getCountry(): ?string
|
||||
{
|
||||
return $this->country;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $currency
|
||||
* @return Customer
|
||||
*/
|
||||
public function setCurrency($currency)
|
||||
public function setCurrency(string $currency): Customer
|
||||
{
|
||||
$this->currency = $currency;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCurrency()
|
||||
public function getCurrency(): string
|
||||
{
|
||||
return $this->currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set phone
|
||||
*
|
||||
* @param string $phone
|
||||
* @return Customer
|
||||
*/
|
||||
public function setPhone($phone)
|
||||
public function setPhone(?string $phone): Customer
|
||||
{
|
||||
$this->phone = $phone;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get phone
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPhone()
|
||||
public function getPhone(): ?string
|
||||
{
|
||||
return $this->phone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fax
|
||||
*
|
||||
* @param string $fax
|
||||
* @return Customer
|
||||
*/
|
||||
public function setFax($fax)
|
||||
public function setFax(?string $fax): Customer
|
||||
{
|
||||
$this->fax = $fax;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fax
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFax()
|
||||
public function getFax(): ?string
|
||||
{
|
||||
return $this->fax;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set mobile
|
||||
*
|
||||
* @param string $mobile
|
||||
* @return Customer
|
||||
*/
|
||||
public function setMobile($mobile)
|
||||
public function setMobile(?string $mobile): Customer
|
||||
{
|
||||
$this->mobile = $mobile;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mobile
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMobile()
|
||||
public function getMobile(): ?string
|
||||
{
|
||||
return $this->mobile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set mail
|
||||
*
|
||||
* @param string $mail
|
||||
* @return Customer
|
||||
*/
|
||||
public function setEmail($mail)
|
||||
public function setEmail(?string $mail): Customer
|
||||
{
|
||||
$this->email = $mail;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mail
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEmail()
|
||||
public function getEmail(): ?string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set homepage
|
||||
*
|
||||
* @param string $homepage
|
||||
* @return Customer
|
||||
*/
|
||||
public function setHomepage($homepage)
|
||||
public function setHomepage(?string $homepage): Customer
|
||||
{
|
||||
$this->homepage = $homepage;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get homepage
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHomepage()
|
||||
public function getHomepage(): ?string
|
||||
{
|
||||
return $this->homepage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set timezone
|
||||
*
|
||||
* @param string $timezone
|
||||
* @return Customer
|
||||
*/
|
||||
public function setTimezone($timezone)
|
||||
public function setTimezone(string $timezone): Customer
|
||||
{
|
||||
$this->timezone = $timezone;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timezone
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTimezone()
|
||||
public function getTimezone(): ?string
|
||||
{
|
||||
return $this->timezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project[] $projects
|
||||
* @return Customer
|
||||
* @return Collection<Project>
|
||||
*/
|
||||
public function setProjects($projects)
|
||||
{
|
||||
$this->projects = $projects;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Project[]
|
||||
*/
|
||||
public function getProjects()
|
||||
public function getProjects(): Collection
|
||||
{
|
||||
return $this->projects;
|
||||
}
|
||||
|
||||
@@ -109,202 +109,125 @@ class InvoiceTemplate
|
||||
*/
|
||||
private $paymentTerms;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set name
|
||||
*
|
||||
* @param string $name
|
||||
* @return $this
|
||||
*/
|
||||
public function setName($name)
|
||||
public function setName(string $name): InvoiceTemplate
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $title
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setTitle(string $title)
|
||||
public function setTitle(string $title): InvoiceTemplate
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAddress(): ?string
|
||||
{
|
||||
return $this->address;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $address
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setAddress($address)
|
||||
public function setAddress(?string $address): InvoiceTemplate
|
||||
{
|
||||
$this->address = $address;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNumberGenerator(): ?string
|
||||
public function getNumberGenerator(): string
|
||||
{
|
||||
return $this->numberGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $numberGenerator
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setNumberGenerator(string $numberGenerator)
|
||||
public function setNumberGenerator(string $numberGenerator): InvoiceTemplate
|
||||
{
|
||||
$this->numberGenerator = $numberGenerator;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDueDays(): ?int
|
||||
public function getDueDays(): int
|
||||
{
|
||||
return $this->dueDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $dueDays
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setDueDays(int $dueDays)
|
||||
public function setDueDays(int $dueDays): InvoiceTemplate
|
||||
{
|
||||
$this->dueDays = $dueDays;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getVat(): ?float
|
||||
public function getVat(): float
|
||||
{
|
||||
return $this->vat;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $vat
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setVat(float $vat)
|
||||
public function setVat(float $vat): InvoiceTemplate
|
||||
{
|
||||
$this->vat = $vat;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCompany(): ?string
|
||||
{
|
||||
return $this->company;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $company
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setCompany(string $company)
|
||||
public function setCompany(string $company): InvoiceTemplate
|
||||
{
|
||||
$this->company = $company;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRenderer(): string
|
||||
{
|
||||
return $this->renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $renderer
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setRenderer(string $renderer)
|
||||
public function setRenderer(string $renderer): InvoiceTemplate
|
||||
{
|
||||
$this->renderer = $renderer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCalculator(): string
|
||||
{
|
||||
return $this->calculator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $calculator
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setCalculator(string $calculator)
|
||||
public function setCalculator(string $calculator): InvoiceTemplate
|
||||
{
|
||||
$this->calculator = $calculator;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPaymentTerms(): ?string
|
||||
{
|
||||
return $this->paymentTerms;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $paymentTerms
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setPaymentTerms(?string $paymentTerms)
|
||||
public function setPaymentTerms(?string $paymentTerms): InvoiceTemplate
|
||||
{
|
||||
$this->paymentTerms = $paymentTerms;
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
@@ -77,7 +79,7 @@ class Project
|
||||
private $budget = 0.00;
|
||||
|
||||
/**
|
||||
* @var Activity[]
|
||||
* @var Activity[]|ArrayCollection
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\Activity", mappedBy="project")
|
||||
*/
|
||||
@@ -88,54 +90,43 @@ class Project
|
||||
use ColorTrait;
|
||||
|
||||
/**
|
||||
* @var Timesheet[]
|
||||
* @var Timesheet[]|ArrayCollection
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\Timesheet", mappedBy="project")
|
||||
*/
|
||||
private $timesheets;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
public function __construct()
|
||||
{
|
||||
$this->activities = new ArrayCollection();
|
||||
$this->timesheets = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Customer
|
||||
*/
|
||||
public function getCustomer()
|
||||
public function getCustomer(): ?Customer
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @return Project
|
||||
*/
|
||||
public function setCustomer($customer)
|
||||
public function setCustomer(Customer $customer): Project
|
||||
{
|
||||
$this->customer = $customer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return Project
|
||||
*/
|
||||
public function setName($name)
|
||||
public function setName(string $name): Project
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
@@ -144,26 +135,19 @@ class Project
|
||||
* @param string $comment
|
||||
* @return Project
|
||||
*/
|
||||
public function setComment($comment)
|
||||
public function setComment($comment): Project
|
||||
{
|
||||
$this->comment = $comment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getComment()
|
||||
public function getComment(): ?string
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $visible
|
||||
* @return Project
|
||||
*/
|
||||
public function setVisible($visible)
|
||||
public function setVisible(bool $visible): Project
|
||||
{
|
||||
$this->visible = $visible;
|
||||
|
||||
@@ -173,7 +157,7 @@ class Project
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getVisible()
|
||||
public function getVisible(): bool
|
||||
{
|
||||
return $this->visible;
|
||||
}
|
||||
@@ -182,7 +166,7 @@ class Project
|
||||
* @param float $budget
|
||||
* @return Project
|
||||
*/
|
||||
public function setBudget($budget)
|
||||
public function setBudget($budget): Project
|
||||
{
|
||||
$this->budget = $budget;
|
||||
|
||||
@@ -198,39 +182,17 @@ class Project
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @return Project
|
||||
* @return Collection<Timesheet>
|
||||
*/
|
||||
public function setTimesheets($timesheets)
|
||||
{
|
||||
$this->timesheets = $timesheets;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
public function getTimesheets()
|
||||
public function getTimesheets(): Collection
|
||||
{
|
||||
return $this->timesheets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity[] $activities
|
||||
* @return Project
|
||||
* @return Collection<Activity>
|
||||
*/
|
||||
public function setActivities($activities)
|
||||
{
|
||||
$this->activities = $activities;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Activity[]
|
||||
*/
|
||||
public function getActivities()
|
||||
public function getActivities(): Collection
|
||||
{
|
||||
return $this->activities;
|
||||
}
|
||||
@@ -247,7 +209,7 @@ class Project
|
||||
* @param string $orderNumber
|
||||
* @return Project
|
||||
*/
|
||||
public function setOrderNumber($orderNumber)
|
||||
public function setOrderNumber($orderNumber): Project
|
||||
{
|
||||
$this->orderNumber = $orderNumber;
|
||||
|
||||
|
||||
@@ -55,36 +55,23 @@ class Tag
|
||||
$this->timesheets = new ArrayCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $tagName
|
||||
* @return Tag
|
||||
*/
|
||||
public function setName($tagName)
|
||||
public function setName(string $tagName): Tag
|
||||
{
|
||||
$this->name = $tagName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
*/
|
||||
public function addTimesheet(Timesheet $timesheet)
|
||||
{
|
||||
if ($this->timesheets->contains($timesheet)) {
|
||||
@@ -95,9 +82,6 @@ class Tag
|
||||
$timesheet->addTag($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
*/
|
||||
public function removeTimesheet(Timesheet $timesheet)
|
||||
{
|
||||
if (!$this->timesheets->contains($timesheet)) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
@@ -148,11 +149,11 @@ class Timesheet
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entry id
|
||||
* Get entry id, returns null for new entities which were not persisted.
|
||||
*
|
||||
* @return int
|
||||
* @return int|null
|
||||
*/
|
||||
public function getId()
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
@@ -178,10 +179,7 @@ class Timesheet
|
||||
$this->localized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getBegin()
|
||||
public function getBegin(): ?\DateTime
|
||||
{
|
||||
$this->localizeDates();
|
||||
|
||||
@@ -192,7 +190,7 @@ class Timesheet
|
||||
* @param \DateTime $begin
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setBegin(\DateTime $begin)
|
||||
public function setBegin(\DateTime $begin): Timesheet
|
||||
{
|
||||
$this->begin = $begin;
|
||||
$this->timezone = $begin->getTimezone()->getName();
|
||||
@@ -200,10 +198,7 @@ class Timesheet
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime|null
|
||||
*/
|
||||
public function getEnd()
|
||||
public function getEnd(): ?\DateTime
|
||||
{
|
||||
$this->localizeDates();
|
||||
|
||||
@@ -214,7 +209,7 @@ class Timesheet
|
||||
* @param \DateTime $end
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setEnd(?\DateTime $end)
|
||||
public function setEnd(?\DateTime $end): Timesheet
|
||||
{
|
||||
$this->end = $end;
|
||||
|
||||
@@ -232,7 +227,7 @@ class Timesheet
|
||||
* @param int $duration
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setDuration($duration)
|
||||
public function setDuration($duration): Timesheet
|
||||
{
|
||||
$this->duration = $duration;
|
||||
|
||||
@@ -253,17 +248,14 @@ class Timesheet
|
||||
* @param User $user
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setUser(User $user)
|
||||
public function setUser(User $user): Timesheet
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User
|
||||
*/
|
||||
public function getUser()
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
@@ -272,25 +264,19 @@ class Timesheet
|
||||
* @param Activity $activity
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setActivity($activity)
|
||||
public function setActivity($activity): Timesheet
|
||||
{
|
||||
$this->activity = $activity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Activity
|
||||
*/
|
||||
public function getActivity()
|
||||
public function getActivity(): ?Activity
|
||||
{
|
||||
return $this->activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Project
|
||||
*/
|
||||
public function getProject()
|
||||
public function getProject(): ?Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
@@ -299,7 +285,7 @@ class Timesheet
|
||||
* @param Project $project
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setProject(Project $project)
|
||||
public function setProject(Project $project): Timesheet
|
||||
{
|
||||
$this->project = $project;
|
||||
|
||||
@@ -310,17 +296,14 @@ class Timesheet
|
||||
* @param string $description
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setDescription($description)
|
||||
public function setDescription($description): Timesheet
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
@@ -329,7 +312,7 @@ class Timesheet
|
||||
* @param float $rate
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setRate($rate)
|
||||
public function setRate($rate): Timesheet
|
||||
{
|
||||
$this->rate = $rate;
|
||||
|
||||
@@ -348,7 +331,7 @@ class Timesheet
|
||||
* @param Tag $tag
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function addTag(Tag $tag)
|
||||
public function addTag(Tag $tag): Timesheet
|
||||
{
|
||||
if ($this->tags->contains($tag)) {
|
||||
return $this;
|
||||
@@ -372,9 +355,9 @@ class Timesheet
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Tag[]|ArrayCollection
|
||||
* @return Collection<Tag>
|
||||
*/
|
||||
public function getTags()
|
||||
public function getTags(): Collection
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
@@ -404,7 +387,7 @@ class Timesheet
|
||||
* @param bool $exported
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setExported(bool $exported)
|
||||
public function setExported(bool $exported): Timesheet
|
||||
{
|
||||
$this->exported = $exported;
|
||||
|
||||
@@ -426,7 +409,7 @@ class Timesheet
|
||||
* @param string $timezone
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setTimezone(string $timezone)
|
||||
public function setTimezone(string $timezone): Timesheet
|
||||
{
|
||||
$this->timezone = $timezone;
|
||||
|
||||
|
||||
@@ -105,122 +105,77 @@ class User extends BaseUser implements UserInterface
|
||||
$this->preferences = new ArrayCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getRegisteredAt()
|
||||
public function getRegisteredAt(): ?\DateTime
|
||||
{
|
||||
return $this->registeredAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $registeredAt
|
||||
* @return $this
|
||||
*/
|
||||
public function setRegisteredAt(\DateTime $registeredAt)
|
||||
public function setRegisteredAt(\DateTime $registeredAt): User
|
||||
{
|
||||
$this->registeredAt = $registeredAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $alias
|
||||
* @return $this
|
||||
*/
|
||||
public function setAlias($alias)
|
||||
public function setAlias(?string $alias): User
|
||||
{
|
||||
$this->alias = $alias;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAlias()
|
||||
public function getAlias(): ?string
|
||||
{
|
||||
return $this->alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $title
|
||||
* @return $this
|
||||
*/
|
||||
public function setTitle($title)
|
||||
public function setTitle(?string $title): User
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAvatar()
|
||||
public function getAvatar(): ?string
|
||||
{
|
||||
return $this->avatar;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $avatar
|
||||
* @return $this
|
||||
*/
|
||||
public function setAvatar($avatar)
|
||||
public function setAvatar(?string $avatar): User
|
||||
{
|
||||
$this->avatar = $avatar;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getApiToken()
|
||||
public function getApiToken(): ?string
|
||||
{
|
||||
return $this->apiToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $apiToken
|
||||
* @return User
|
||||
*/
|
||||
public function setApiToken($apiToken)
|
||||
public function setApiToken(?string $apiToken): User
|
||||
{
|
||||
$this->apiToken = $apiToken;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPlainApiToken(): ?string
|
||||
{
|
||||
return $this->plainApiToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $plainApiToken
|
||||
* @return User
|
||||
*/
|
||||
public function setPlainApiToken(string $plainApiToken)
|
||||
public function setPlainApiToken(?string $plainApiToken): User
|
||||
{
|
||||
$this->plainApiToken = $plainApiToken;
|
||||
|
||||
@@ -228,7 +183,7 @@ class User extends BaseUser implements UserInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UserPreference[]|Collection
|
||||
* @return Collection<UserPreference>
|
||||
*/
|
||||
public function getPreferences(): Collection
|
||||
{
|
||||
@@ -236,10 +191,10 @@ class User extends BaseUser implements UserInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserPreference[]|Collection<UserPreference> $preferences
|
||||
* @param iterable<UserPreference> $preferences
|
||||
* @return User
|
||||
*/
|
||||
public function setPreferences($preferences)
|
||||
public function setPreferences(iterable $preferences): User
|
||||
{
|
||||
$this->preferences = new ArrayCollection();
|
||||
|
||||
@@ -252,10 +207,29 @@ class User extends BaseUser implements UserInterface
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return UserPreference|null
|
||||
* @param bool|int|string|null $value
|
||||
*/
|
||||
public function getPreference(string $name)
|
||||
public function setPreferenceValue(string $name, $value = null)
|
||||
{
|
||||
$pref = $this->getPreference($name);
|
||||
|
||||
if (null === $pref) {
|
||||
$pref = new UserPreference();
|
||||
$pref->setName($name);
|
||||
$this->addPreference($pref);
|
||||
}
|
||||
|
||||
$pref->setValue($value);
|
||||
}
|
||||
|
||||
public function getPreference(string $name): ?UserPreference
|
||||
{
|
||||
// this code will be triggered, if a currently logged-in user will be deleted and the refreshed from the session
|
||||
// via one of the UserProvider - e.g. see LdapUserProvider::refreshUser() which calls $user->getPreferenceValue()
|
||||
if (empty($this->preferences)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($this->preferences as $preference) {
|
||||
if ($preference->getName() == $name) {
|
||||
return $preference;
|
||||
@@ -268,7 +242,7 @@ class User extends BaseUser implements UserInterface
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLocale()
|
||||
public function getLocale(): string
|
||||
{
|
||||
return $this->getPreferenceValue(UserPreference::LOCALE, User::DEFAULT_LANGUAGE);
|
||||
}
|
||||
@@ -292,7 +266,7 @@ class User extends BaseUser implements UserInterface
|
||||
* @param UserPreference $preference
|
||||
* @return User
|
||||
*/
|
||||
public function addPreference(UserPreference $preference)
|
||||
public function addPreference(UserPreference $preference): User
|
||||
{
|
||||
$this->preferences->add($preference);
|
||||
$preference->setUser($this);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use App\Security\RoleService;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
@@ -19,14 +20,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
class UserRoleType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
* @var RoleService
|
||||
*/
|
||||
protected $roles = [];
|
||||
protected $roles;
|
||||
|
||||
/**
|
||||
* @param string[] $roles
|
||||
*/
|
||||
public function __construct(array $roles = [])
|
||||
public function __construct(RoleService $roles)
|
||||
{
|
||||
$this->roles = $roles;
|
||||
}
|
||||
@@ -37,13 +35,8 @@ class UserRoleType extends AbstractType
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$roles = [];
|
||||
foreach ($this->roles as $key => $value) {
|
||||
$roles[$key] = $key;
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $value2) {
|
||||
$roles[$value2] = $value2;
|
||||
}
|
||||
}
|
||||
foreach ($this->roles->getAvailableNames() as $name) {
|
||||
$roles[$name] = $name;
|
||||
}
|
||||
|
||||
$resolver->setDefaults([
|
||||
|
||||
@@ -17,9 +17,11 @@ use App\Export\RendererInterface as ExportRendererInterface;
|
||||
use App\Invoice\CalculatorInterface as InvoiceCalculator;
|
||||
use App\Invoice\NumberGeneratorInterface;
|
||||
use App\Invoice\RendererInterface as InvoiceRendererInterface;
|
||||
use App\Ldap\FormLoginLdapFactory;
|
||||
use App\Plugin\PluginInterface;
|
||||
use App\Timesheet\CalculatorInterface as TimesheetCalculator;
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
|
||||
use Symfony\Component\Config\Loader\LoaderInterface;
|
||||
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
@@ -58,6 +60,10 @@ class Kernel extends BaseKernel
|
||||
$container->registerForAutoconfiguration(NumberGeneratorInterface::class)->addTag(self::TAG_INVOICE_NUMBER_GENERATOR);
|
||||
$container->registerForAutoconfiguration(InvoiceCalculator::class)->addTag(self::TAG_INVOICE_CALCULATOR);
|
||||
$container->registerForAutoconfiguration(PluginInterface::class)->addTag(self::TAG_PLUGIN);
|
||||
|
||||
/** @var SecurityExtension $extension */
|
||||
$extension = $container->getExtension('security');
|
||||
$extension->addSecurityListenerFactory(new FormLoginLdapFactory());
|
||||
}
|
||||
|
||||
public function registerBundles()
|
||||
|
||||
72
src/Ldap/FormLoginLdapFactory.php
Normal file
72
src/Ldap/FormLoginLdapFactory.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Ldap;
|
||||
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
|
||||
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* Inspired by https://github.com/Maks3w/FR3DLdapBundle @ MIT License
|
||||
*/
|
||||
class FormLoginLdapFactory implements SecurityFactoryInterface
|
||||
{
|
||||
public function create(ContainerBuilder $container, $id, $config, $userProviderId, $defaultEntryPointId)
|
||||
{
|
||||
$authProviderId = $this->createAuthProvider($container, $id, $userProviderId);
|
||||
$listenerId = $this->createListener($container, $id, $config);
|
||||
|
||||
return [$authProviderId, $listenerId, $defaultEntryPointId];
|
||||
}
|
||||
|
||||
public function getPosition()
|
||||
{
|
||||
return 'pre_auth';
|
||||
}
|
||||
|
||||
public function getKey()
|
||||
{
|
||||
return 'kimai_ldap';
|
||||
}
|
||||
|
||||
public function addConfiguration(NodeDefinition $node)
|
||||
{
|
||||
}
|
||||
|
||||
protected function createAuthProvider(ContainerBuilder $container, $id, $userProviderId)
|
||||
{
|
||||
$provider = 'kimai_ldap.security.authentication.provider';
|
||||
$providerId = $provider . '.' . $id;
|
||||
|
||||
$container
|
||||
->setDefinition($providerId, new ChildDefinition($provider))
|
||||
->replaceArgument(1, $id)
|
||||
->replaceArgument(2, new Reference($userProviderId))
|
||||
;
|
||||
|
||||
return $providerId;
|
||||
}
|
||||
|
||||
protected function createListener(ContainerBuilder $container, $id, $config)
|
||||
{
|
||||
$listenerId = 'security.authentication.listener.form';
|
||||
|
||||
$listener = new ChildDefinition($listenerId);
|
||||
$listener->replaceArgument(4, $id);
|
||||
$listener->replaceArgument(5, $config);
|
||||
|
||||
$listenerId .= '.' . $id;
|
||||
$container->setDefinition($listenerId, $listener);
|
||||
|
||||
return $listenerId;
|
||||
}
|
||||
}
|
||||
131
src/Ldap/LdapAuthenticationProvider.php
Normal file
131
src/Ldap/LdapAuthenticationProvider.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Ldap;
|
||||
|
||||
use App\Configuration\LdapConfiguration;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Provider\UserAuthenticationProvider;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* Inspired by https://github.com/Maks3w/FR3DLdapBundle @ MIT License
|
||||
*/
|
||||
class LdapAuthenticationProvider extends UserAuthenticationProvider
|
||||
{
|
||||
/**
|
||||
* @var UserProviderInterface
|
||||
*/
|
||||
private $userProvider;
|
||||
|
||||
/**
|
||||
* @var LdapManager
|
||||
*/
|
||||
private $ldapManager;
|
||||
/**
|
||||
* @var LdapConfiguration
|
||||
*/
|
||||
private $config;
|
||||
|
||||
public function __construct(UserCheckerInterface $userChecker, $providerKey, UserProviderInterface $userProvider, LdapManager $ldapManager, LdapConfiguration $config, $hideUserNotFoundExceptions = true)
|
||||
{
|
||||
parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions);
|
||||
|
||||
$this->ldapManager = $ldapManager;
|
||||
$this->config = $config;
|
||||
$this->userProvider = $userProvider;
|
||||
}
|
||||
|
||||
public function supports(TokenInterface $token)
|
||||
{
|
||||
if (!$this->config->isActivated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::supports($token);
|
||||
}
|
||||
|
||||
protected function retrieveUser($username, UsernamePasswordToken $token)
|
||||
{
|
||||
$user = $token->getUser();
|
||||
if ($user instanceof UserInterface) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
try {
|
||||
// this will always query the FOSUserBundle first...
|
||||
// only first-time logins from LDAP user (not yet existing in local user database)
|
||||
// will actually hit the LdapUserProvider
|
||||
$user = $this->userProvider->loadUserByUsername($username);
|
||||
|
||||
// do not update the user here from LDAP, as we don't know if the user can be authenticated
|
||||
} catch (UsernameNotFoundException $notFound) {
|
||||
throw $notFound;
|
||||
} catch (\Exception $repositoryProblem) {
|
||||
$e = new AuthenticationServiceException($repositoryProblem->getMessage(), (int) $repositoryProblem->getCode(), $repositoryProblem);
|
||||
$e->setToken($token);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* The updateUser() call should theoretically happen in retrieveUser() but that would require an additional
|
||||
* $this->ldapManager->bind($user, $token->getCredentials())
|
||||
* to check if the user is still valid.
|
||||
*
|
||||
* Symfony calls retrieveUser() before checkAuthentication()
|
||||
* and we should not used ldap->search() before ldap->bind()
|
||||
*
|
||||
* @param UserInterface $user
|
||||
* @param UsernamePasswordToken $token
|
||||
* @throws LdapDriverException
|
||||
*/
|
||||
protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token)
|
||||
{
|
||||
$currentUser = $token->getUser();
|
||||
$presentedPassword = $token->getCredentials();
|
||||
if ($currentUser instanceof UserInterface) {
|
||||
if ('' === $presentedPassword) {
|
||||
throw new BadCredentialsException(
|
||||
'The password in the token is empty. Check `erase_credentials` in your `security.yaml`'
|
||||
);
|
||||
}
|
||||
|
||||
if (!$this->ldapManager->bind($currentUser, $presentedPassword)) {
|
||||
throw new BadCredentialsException('The credentials were changed from another session.');
|
||||
}
|
||||
} else {
|
||||
if ('' === $presentedPassword) {
|
||||
throw new BadCredentialsException('The presented password cannot be empty.');
|
||||
}
|
||||
|
||||
if (!$this->ldapManager->bind($user, $presentedPassword)) {
|
||||
throw new BadCredentialsException('The presented password is invalid.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($user instanceof User && null !== $user->getPreferenceValue('ldap.dn')) {
|
||||
try {
|
||||
$this->ldapManager->updateUser($user);
|
||||
} catch (LdapDriverException $ex) {
|
||||
throw new BadCredentialsException('Fetching user data/roles failed, probably DN is expired.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
129
src/Ldap/LdapDriver.php
Normal file
129
src/Ldap/LdapDriver.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Ldap;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Zend\Ldap\Exception\LdapException;
|
||||
use Zend\Ldap\Ldap;
|
||||
|
||||
/**
|
||||
* Inspired by https://github.com/Maks3w/FR3DLdapBundle @ MIT License
|
||||
*/
|
||||
class LdapDriver
|
||||
{
|
||||
/**
|
||||
* @var Ldap
|
||||
*/
|
||||
private $driver;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* @param Ldap $driver Initialized Zend::Ldap Object
|
||||
* @param LoggerInterface $logger optional logger for write debug messages
|
||||
*/
|
||||
public function __construct(Ldap $driver, LoggerInterface $logger = null)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $baseDn
|
||||
* @param string $filter
|
||||
* @param array $attributes
|
||||
* @return array
|
||||
* @throws LdapDriverException
|
||||
*/
|
||||
public function search(string $baseDn, string $filter, array $attributes = []): array
|
||||
{
|
||||
$attributes = array_unique(array_merge($attributes, ['+', '*']));
|
||||
|
||||
$this->logDebug('{action}({base_dn}, {filter}, {attributes})', [
|
||||
'action' => 'ldap_search',
|
||||
'base_dn' => $baseDn,
|
||||
'filter' => $filter,
|
||||
'attributes' => $attributes,
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->driver->bind();
|
||||
$entries = $this->driver->searchEntries($filter, $baseDn, Ldap::SEARCH_SCOPE_SUB, $attributes);
|
||||
|
||||
// searchEntries don't return 'count' key as specified by php native function ldap_get_entries()
|
||||
$entries['count'] = count($entries);
|
||||
} catch (LdapException $exception) {
|
||||
$this->zendExceptionHandler($exception);
|
||||
|
||||
throw new LdapDriverException('An error occurred with the search operation.');
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
public function bind(UserInterface $user, string $password): bool
|
||||
{
|
||||
$bindDn = $user->getUsername();
|
||||
|
||||
try {
|
||||
$this->logDebug('{action}({bindDn}, ****)', [
|
||||
'action' => 'ldap_bind',
|
||||
'bindDn' => $bindDn,
|
||||
]);
|
||||
$bind = $this->driver->bind($bindDn, $password);
|
||||
|
||||
return $bind instanceof Ldap;
|
||||
} catch (LdapException $exception) {
|
||||
$this->zendExceptionHandler($exception, $password);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Treat a Zend Ldap Exception.
|
||||
*/
|
||||
protected function zendExceptionHandler(LdapException $exception, string $password = null): void
|
||||
{
|
||||
$sanitizedException = null !== $password ? new SanitizingException($exception, $password) : $exception;
|
||||
|
||||
switch ($exception->getCode()) {
|
||||
// Error level codes
|
||||
case LdapException::LDAP_SERVER_DOWN:
|
||||
if ($this->logger) {
|
||||
$this->logger->error('{exception}', ['exception' => $sanitizedException]);
|
||||
}
|
||||
break;
|
||||
|
||||
// Other level codes
|
||||
default:
|
||||
$this->logDebug('{exception}', ['exception' => $sanitizedException]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log debug messages if the logger is set.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*/
|
||||
private function logDebug(string $message, array $context = []): void
|
||||
{
|
||||
if (null === $this->logger) {
|
||||
return;
|
||||
}
|
||||
$this->logger->debug($message, $context);
|
||||
}
|
||||
}
|
||||
18
src/Ldap/LdapDriverException.php
Normal file
18
src/Ldap/LdapDriverException.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Ldap;
|
||||
|
||||
class LdapDriverException extends \Exception
|
||||
{
|
||||
public function __construct($message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
}
|
||||
165
src/Ldap/LdapManager.php
Normal file
165
src/Ldap/LdapManager.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Ldap;
|
||||
|
||||
use App\Configuration\LdapConfiguration;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* Inspired by https://github.com/Maks3w/FR3DLdapBundle @ MIT License
|
||||
*/
|
||||
class LdapManager
|
||||
{
|
||||
/**
|
||||
* @var LdapConfiguration
|
||||
*/
|
||||
protected $config;
|
||||
/**
|
||||
* @var LdapDriver
|
||||
*/
|
||||
protected $driver;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $params = [];
|
||||
/**
|
||||
* @var LdapUserHydrator
|
||||
*/
|
||||
protected $hydrator;
|
||||
|
||||
public function __construct(LdapDriver $driver, LdapUserHydrator $hydrator, LdapConfiguration $config)
|
||||
{
|
||||
$this->params = $config->getUserParameters();
|
||||
$this->config = $config;
|
||||
$this->driver = $driver;
|
||||
$this->hydrator = $hydrator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only executed for unknown local users.
|
||||
*
|
||||
* @param string $username
|
||||
* @return User|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function findUserByUsername(string $username): ?UserInterface
|
||||
{
|
||||
return $this->findUserBy([$this->params['usernameAttribute'] => $username]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $criteria
|
||||
* @return User|null
|
||||
* @throws LdapDriverException
|
||||
*/
|
||||
public function findUserBy(array $criteria): ?UserInterface
|
||||
{
|
||||
$filter = $this->buildFilter($criteria);
|
||||
$entries = $this->driver->search($this->params['baseDn'], $filter);
|
||||
|
||||
if ($entries['count'] > 1) {
|
||||
throw new LdapDriverException('This search must only return a single user');
|
||||
}
|
||||
|
||||
if (0 === $entries['count']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// do not updateUser() here, as this would happen before bind()
|
||||
return $this->hydrator->hydrate($entries[0]);
|
||||
}
|
||||
|
||||
protected function buildFilter(array $criteria, string $condition = '&'): string
|
||||
{
|
||||
$filters = [];
|
||||
$filters[] = $this->params['filter'];
|
||||
foreach ($criteria as $key => $value) {
|
||||
$value = ldap_escape($value, '', LDAP_ESCAPE_FILTER);
|
||||
$filters[] = sprintf('(%s=%s)', $key, $value);
|
||||
}
|
||||
|
||||
return sprintf('(%s%s)', $condition, implode($filters));
|
||||
}
|
||||
|
||||
public function bind(UserInterface $user, string $password): bool
|
||||
{
|
||||
return $this->driver->bind($user, $password);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method does all the heavy lifting:
|
||||
* - searching for latest 'dn'
|
||||
* - syncing user attributes
|
||||
* - syncing roles
|
||||
*
|
||||
* @param User $user
|
||||
* @throws LdapDriverException
|
||||
*/
|
||||
public function updateUser(User $user)
|
||||
{
|
||||
$baseDn = $user->getPreferenceValue('ldap.dn');
|
||||
$filter = '(objectClass=*)';
|
||||
|
||||
if (null === $baseDn) {
|
||||
throw new LdapDriverException('This account is not a registered LDAP user');
|
||||
}
|
||||
|
||||
// always look up the users current DN first, as the cached DN might have been renamed in LDAP
|
||||
$userFresh = $this->findUserByUsername($user->getUsername());
|
||||
if (null === $userFresh || null === ($baseDn = $userFresh->getPreferenceValue('ldap.dn'))) {
|
||||
throw new LdapDriverException(sprintf('Failed fetching user DN for %s', $user->getUsername()));
|
||||
}
|
||||
$user->setPreferenceValue('ldap.dn', $baseDn);
|
||||
|
||||
$entries = $this->driver->search($baseDn, $filter);
|
||||
|
||||
if ($entries['count'] > 1) {
|
||||
throw new LdapDriverException('This search must only return a single user');
|
||||
}
|
||||
|
||||
if (0 === $entries['count']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->hydrator->hydrateUser($user, $entries[0]);
|
||||
|
||||
$roleParameter = $this->config->getRoleParameters();
|
||||
if (null === $roleParameter['baseDn']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$param = $roleParameter['usernameAttribute'];
|
||||
if (!isset($entries[0][$param]) && $param !== 'dn') {
|
||||
$param = 'dn';
|
||||
}
|
||||
|
||||
$roleValue = $entries[0][$param];
|
||||
if (is_array($roleValue)) {
|
||||
$roleValue = $roleValue[0];
|
||||
}
|
||||
$roles = $this->getRoles($roleValue, $roleParameter);
|
||||
|
||||
if (!empty($roles)) {
|
||||
$this->hydrator->hydrateRoles($user, $roles);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getRoles(string $dn, array $roleParameter): array
|
||||
{
|
||||
$filter = $roleParameter['filter'] ?? '';
|
||||
|
||||
return $this->driver->search(
|
||||
$roleParameter['baseDn'],
|
||||
sprintf('(&%s(%s=%s))', $filter, $roleParameter['userDnAttribute'], $dn),
|
||||
[$roleParameter['nameAttribute']]
|
||||
);
|
||||
}
|
||||
}
|
||||
144
src/Ldap/LdapUserHydrator.php
Normal file
144
src/Ldap/LdapUserHydrator.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Ldap;
|
||||
|
||||
use App\Configuration\LdapConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Security\RoleService;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* Inspired by https://github.com/Maks3w/FR3DLdapBundle @ MIT License
|
||||
*/
|
||||
class LdapUserHydrator
|
||||
{
|
||||
/**
|
||||
* @var LdapConfiguration
|
||||
*/
|
||||
private $config;
|
||||
/**
|
||||
* @var RoleService
|
||||
*/
|
||||
private $roles;
|
||||
|
||||
public function __construct(LdapConfiguration $config, RoleService $roles)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->roles = $roles;
|
||||
}
|
||||
|
||||
protected function createUser(): User
|
||||
{
|
||||
$user = new User();
|
||||
$user->setEnabled(true);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function hydrate(array $ldapEntry): User
|
||||
{
|
||||
$user = $this->createUser();
|
||||
$this->hydrateUser($user, $ldapEntry);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function hydrateUser(User $user, array $ldapEntry)
|
||||
{
|
||||
$userParams = $this->config->getUserParameters();
|
||||
$attributeMap = $userParams['attributes'];
|
||||
$attributeMap = array_merge(
|
||||
[
|
||||
['ldap_attr' => $userParams['usernameAttribute'], 'user_method' => 'setUsername'],
|
||||
],
|
||||
$attributeMap
|
||||
);
|
||||
|
||||
$this->hydrateUserWithAttributesMap($user, $ldapEntry, $attributeMap);
|
||||
|
||||
if (null === $user->getEmail()) {
|
||||
$user->setEmail($user->getUsername());
|
||||
}
|
||||
|
||||
// prevent that users will define a password for the internal account
|
||||
$user->setPassword('');
|
||||
|
||||
$user->setPreferenceValue('ldap.dn', $ldapEntry['dn']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param array $entries
|
||||
*/
|
||||
public function hydrateRoles(User $user, array $entries)
|
||||
{
|
||||
$roleParams = $this->config->getRoleParameters();
|
||||
$allowedRoles = $this->roles->getAvailableNames();
|
||||
$groupNameMapping = $roleParams['groups'];
|
||||
$roleNameAttr = $roleParams['nameAttribute'];
|
||||
|
||||
$roles = [];
|
||||
for ($i = 0; $i < $entries['count']; $i++) {
|
||||
$roleName = $entries[$i][$roleNameAttr][0];
|
||||
$mapped = false;
|
||||
foreach ($groupNameMapping as $attr) {
|
||||
if ($roleName === $attr['ldap_value']) {
|
||||
$roleName = $attr['role'];
|
||||
$mapped = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$mapped) {
|
||||
$roleName = sprintf('ROLE_%s', self::slugify($roleName));
|
||||
}
|
||||
|
||||
if (!in_array($roleName, $allowedRoles)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$roles[] = $roleName;
|
||||
}
|
||||
|
||||
$user->setRoles($roles);
|
||||
}
|
||||
|
||||
private static function slugify(string $role): string
|
||||
{
|
||||
$role = preg_replace('/\W+/', '_', $role);
|
||||
$role = trim($role, '_');
|
||||
$role = strtoupper($role);
|
||||
|
||||
return $role;
|
||||
}
|
||||
|
||||
protected function hydrateUserWithAttributesMap(UserInterface $user, array $ldapUserAttributes, array $attributeMap)
|
||||
{
|
||||
/** @var array $attr */
|
||||
foreach ($attributeMap as $attr) {
|
||||
if (!array_key_exists($attr['ldap_attr'], $ldapUserAttributes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ldapValue = $ldapUserAttributes[$attr['ldap_attr']];
|
||||
|
||||
if (array_key_exists('count', $ldapValue)) {
|
||||
unset($ldapValue['count']);
|
||||
}
|
||||
|
||||
if (1 === count($ldapValue)) {
|
||||
$value = array_shift($ldapValue);
|
||||
} else {
|
||||
$value = $ldapValue;
|
||||
}
|
||||
|
||||
$user->{$attr['user_method']}($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
120
src/Ldap/LdapUserProvider.php
Normal file
120
src/Ldap/LdapUserProvider.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Ldap;
|
||||
|
||||
use App\Configuration\LdapConfiguration;
|
||||
use App\Entity\User;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* Overwritten to be able to deactivate LDAP via config switch.
|
||||
*
|
||||
* Inspired by https://github.com/Maks3w/FR3DLdapBundle @ MIT License
|
||||
*/
|
||||
class LdapUserProvider implements UserProviderInterface
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $activated = false;
|
||||
/**
|
||||
* @var LdapManager
|
||||
*/
|
||||
protected $ldapManager;
|
||||
/**
|
||||
* @var LoggerInterface|null
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
public function __construct(LdapManager $ldapManager, LdapConfiguration $config, LoggerInterface $logger = null)
|
||||
{
|
||||
$this->ldapManager = $ldapManager;
|
||||
$this->logger = $logger;
|
||||
$this->activated = $config->isActivated();
|
||||
}
|
||||
|
||||
public function loadUserByUsername($username)
|
||||
{
|
||||
// this method is called at least for unknown user, no matter what supportsClass() returns,
|
||||
// so we have to check if LDAP is activated here as well
|
||||
if (!$this->activated) {
|
||||
$ex = new UsernameNotFoundException(sprintf('LDAP is deactivated, user "%s" not searched', $username));
|
||||
$ex->setUsername($username);
|
||||
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
$user = $this->ldapManager->findUserByUsername($username);
|
||||
|
||||
if (empty($user)) {
|
||||
$this->logInfo('User {username} {result} on LDAP', [
|
||||
'action' => 'loadUserByUsername',
|
||||
'username' => $username,
|
||||
'result' => 'not found',
|
||||
]);
|
||||
$ex = new UsernameNotFoundException(sprintf('User "%s" not found', $username));
|
||||
$ex->setUsername($username);
|
||||
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
$this->logInfo('User {username} {result} on LDAP', [
|
||||
'action' => 'loadUserByUsername',
|
||||
'username' => $username,
|
||||
'result' => 'found',
|
||||
]);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function refreshUser(UserInterface $user)
|
||||
{
|
||||
if (!($user instanceof User) || !$this->supportsClass(get_class($user))) {
|
||||
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
|
||||
}
|
||||
|
||||
if (null === $user->getPreferenceValue('ldap.dn')) {
|
||||
throw new UnsupportedUserException(sprintf('Account "%s" is not a registered LDAP user.', $user->getUsername()));
|
||||
}
|
||||
|
||||
try {
|
||||
$this->ldapManager->updateUser($user);
|
||||
} catch (LdapDriverException $ex) {
|
||||
throw new UnsupportedUserException(sprintf('Failed to refresh user "%s", probably DN is expired.', $user->getUsername()));
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function supportsClass($class)
|
||||
{
|
||||
if (!$this->activated) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $class === User::class || $class === 'App\Entity\User';
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message into the logger if this exists.
|
||||
*/
|
||||
private function logInfo(string $message, array $context = []): void
|
||||
{
|
||||
if (!$this->logger) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->info($message, $context);
|
||||
}
|
||||
}
|
||||
40
src/Ldap/SanitizingException.php
Normal file
40
src/Ldap/SanitizingException.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Ldap;
|
||||
|
||||
/**
|
||||
* Inspired by https://github.com/Maks3w/FR3DLdapBundle @ MIT License
|
||||
*/
|
||||
class SanitizingException extends \Exception
|
||||
{
|
||||
protected $actualException;
|
||||
protected $secret;
|
||||
|
||||
public function __construct(\Exception $actualException, $secret)
|
||||
{
|
||||
parent::__construct(
|
||||
$this->stripSecret($actualException->getMessage(), $secret),
|
||||
$actualException->getCode()
|
||||
);
|
||||
|
||||
$this->actualException = $actualException;
|
||||
$this->secret = $secret;
|
||||
}
|
||||
|
||||
protected function stripSecret(string $message, string $secret)
|
||||
{
|
||||
return str_replace($secret, '****', $message);
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->stripSecret($this->actualException->__toString(), $this->secret);
|
||||
}
|
||||
}
|
||||
29
src/Ldap/ZendLdap.php
Normal file
29
src/Ldap/ZendLdap.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Ldap;
|
||||
|
||||
use App\Configuration\LdapConfiguration;
|
||||
use Zend\Ldap\Ldap;
|
||||
|
||||
/**
|
||||
* Overwritten to prevent errors in case:
|
||||
* LDAP is deactivated and LDAP extension is not loaded
|
||||
*/
|
||||
class ZendLdap extends Ldap
|
||||
{
|
||||
public function __construct(LdapConfiguration $config)
|
||||
{
|
||||
if (!$config->isActivated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
parent::__construct($config->getConnectionParameters());
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ use App\Repository\Query\InvoiceQuery;
|
||||
class InvoiceModel
|
||||
{
|
||||
/**
|
||||
* @var Customer
|
||||
* @var Customer|null
|
||||
*/
|
||||
protected $customer;
|
||||
|
||||
@@ -95,36 +95,26 @@ class InvoiceModel
|
||||
* @param Timesheet[] $entries
|
||||
* @return InvoiceModel
|
||||
*/
|
||||
public function setEntries(array $entries)
|
||||
public function setEntries(array $entries): InvoiceModel
|
||||
{
|
||||
$this->entries = $entries;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function getTemplate(): ?InvoiceTemplate
|
||||
{
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceTemplate $template
|
||||
* @return InvoiceModel
|
||||
*/
|
||||
public function setTemplate(InvoiceTemplate $template)
|
||||
public function setTemplate(InvoiceTemplate $template): InvoiceModel
|
||||
{
|
||||
$this->template = $template;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Customer
|
||||
*/
|
||||
public function getCustomer()
|
||||
public function getCustomer(): ?Customer
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
@@ -133,16 +123,13 @@ class InvoiceModel
|
||||
* @param Customer $customer
|
||||
* @return InvoiceModel
|
||||
*/
|
||||
public function setCustomer($customer)
|
||||
public function setCustomer($customer): InvoiceModel
|
||||
{
|
||||
$this->customer = $customer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getDueDate(): ?\DateTime
|
||||
{
|
||||
if (null === $this->getTemplate()) {
|
||||
@@ -160,11 +147,7 @@ class InvoiceModel
|
||||
return $this->invoiceDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param NumberGeneratorInterface $generator
|
||||
* @return InvoiceModel
|
||||
*/
|
||||
public function setNumberGenerator(NumberGeneratorInterface $generator)
|
||||
public function setNumberGenerator(NumberGeneratorInterface $generator): InvoiceModel
|
||||
{
|
||||
$this->generator = $generator;
|
||||
$this->generator->setModel($this);
|
||||
@@ -172,19 +155,12 @@ class InvoiceModel
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return NumberGeneratorInterface
|
||||
*/
|
||||
public function getNumberGenerator(): ?NumberGeneratorInterface
|
||||
{
|
||||
return $this->generator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CalculatorInterface $calculator
|
||||
* @return InvoiceModel
|
||||
*/
|
||||
public function setCalculator(CalculatorInterface $calculator)
|
||||
public function setCalculator(CalculatorInterface $calculator): InvoiceModel
|
||||
{
|
||||
$this->calculator = $calculator;
|
||||
$this->calculator->setModel($this);
|
||||
@@ -192,9 +168,6 @@ class InvoiceModel
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CalculatorInterface
|
||||
*/
|
||||
public function getCalculator(): ?CalculatorInterface
|
||||
{
|
||||
return $this->calculator;
|
||||
|
||||
@@ -33,7 +33,7 @@ class TimesheetStatistic
|
||||
*/
|
||||
protected $amountTotal = 0;
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @var \DateTime|null
|
||||
*/
|
||||
protected $firstEntry;
|
||||
/**
|
||||
@@ -41,10 +41,7 @@ class TimesheetStatistic
|
||||
*/
|
||||
protected $recordsTotal = 0;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDurationThisMonth()
|
||||
public function getDurationThisMonth(): int
|
||||
{
|
||||
return $this->durationThisMonth;
|
||||
}
|
||||
@@ -57,10 +54,7 @@ class TimesheetStatistic
|
||||
$this->durationThisMonth = (int) $durationThisMonth;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getAmountTotal()
|
||||
public function getAmountTotal(): int
|
||||
{
|
||||
return $this->amountTotal;
|
||||
}
|
||||
@@ -73,10 +67,7 @@ class TimesheetStatistic
|
||||
$this->amountTotal = (int) $amountTotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDurationTotal()
|
||||
public function getDurationTotal(): int
|
||||
{
|
||||
return $this->durationTotal;
|
||||
}
|
||||
@@ -89,10 +80,7 @@ class TimesheetStatistic
|
||||
$this->durationTotal = (int) $durationTotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getAmountThisMonth()
|
||||
public function getAmountThisMonth(): int
|
||||
{
|
||||
return $this->amountThisMonth;
|
||||
}
|
||||
@@ -105,10 +93,7 @@ class TimesheetStatistic
|
||||
$this->amountThisMonth = (int) $amountThisMonth;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DateTime
|
||||
*/
|
||||
public function getFirstEntry()
|
||||
public function getFirstEntry(): ?\DateTime
|
||||
{
|
||||
return $this->firstEntry;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use App\Entity\Project;
|
||||
class ActivityQuery extends ProjectQuery
|
||||
{
|
||||
/**
|
||||
* @var Project|int
|
||||
* @var Project|int|null
|
||||
*/
|
||||
protected $project;
|
||||
/**
|
||||
@@ -37,11 +37,7 @@ class ActivityQuery extends ProjectQuery
|
||||
return $this->orderGlobalsFirst;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $orderGlobalsFirst
|
||||
* @return ActivityQuery
|
||||
*/
|
||||
public function setOrderGlobalsFirst(bool $orderGlobalsFirst)
|
||||
public function setOrderGlobalsFirst(bool $orderGlobalsFirst): ActivityQuery
|
||||
{
|
||||
$this->orderGlobalsFirst = $orderGlobalsFirst;
|
||||
|
||||
@@ -60,15 +56,15 @@ class ActivityQuery extends ProjectQuery
|
||||
* @param bool $globalsOnly
|
||||
* @return ActivityQuery
|
||||
*/
|
||||
public function setGlobalsOnly($globalsOnly)
|
||||
public function setGlobalsOnly($globalsOnly): ActivityQuery
|
||||
{
|
||||
$this->globalsOnly = $globalsOnly;
|
||||
$this->globalsOnly = (bool) $globalsOnly;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Project|int
|
||||
* @return Project|int|null
|
||||
*/
|
||||
public function getProject()
|
||||
{
|
||||
@@ -76,10 +72,10 @@ class ActivityQuery extends ProjectQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project|int $project
|
||||
* @return $this
|
||||
* @param Project|int|null $project
|
||||
* @return ActivityQuery
|
||||
*/
|
||||
public function setProject($project = null)
|
||||
public function setProject($project = null): ActivityQuery
|
||||
{
|
||||
$this->project = $project;
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class BaseQuery
|
||||
public const RESULT_TYPE_QUERYBUILDER = 'QueryBuilder';
|
||||
|
||||
/**
|
||||
* @var object
|
||||
* @var object|null
|
||||
*/
|
||||
protected $hiddenEntity;
|
||||
/**
|
||||
@@ -158,7 +158,7 @@ class BaseQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* @return object
|
||||
* @return object|null
|
||||
*/
|
||||
public function getHiddenEntity()
|
||||
{
|
||||
@@ -166,7 +166,7 @@ class BaseQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object|string $hiddenEntity
|
||||
* @param object|string|null $hiddenEntity
|
||||
* @return BaseQuery
|
||||
*/
|
||||
public function setHiddenEntity($hiddenEntity)
|
||||
|
||||
@@ -17,7 +17,7 @@ use App\Entity\Customer;
|
||||
class ProjectQuery extends VisibilityQuery
|
||||
{
|
||||
/**
|
||||
* @var Customer|int
|
||||
* @var Customer|int|null
|
||||
*/
|
||||
protected $customer;
|
||||
|
||||
@@ -46,7 +46,7 @@ class ProjectQuery extends VisibilityQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Customer|int
|
||||
* @return Customer|int|null
|
||||
*/
|
||||
public function getCustomer()
|
||||
{
|
||||
@@ -54,7 +54,7 @@ class ProjectQuery extends VisibilityQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer|int $customer
|
||||
* @param Customer|int|null $customer
|
||||
* @return $this
|
||||
*/
|
||||
public function setCustomer($customer = null)
|
||||
|
||||
@@ -36,11 +36,11 @@ class TimesheetQuery extends ActivityQuery
|
||||
*/
|
||||
protected $orderBy = 'begin';
|
||||
/**
|
||||
* @var User
|
||||
* @var User|null
|
||||
*/
|
||||
protected $user;
|
||||
/**
|
||||
* @var Activity
|
||||
* @var Activity|null
|
||||
*/
|
||||
protected $activity;
|
||||
/**
|
||||
@@ -66,7 +66,7 @@ class TimesheetQuery extends ActivityQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User
|
||||
* @return User|null
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
@@ -74,7 +74,7 @@ class TimesheetQuery extends ActivityQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User|int $user
|
||||
* @param User|int|null $user
|
||||
* @return TimesheetQuery
|
||||
*/
|
||||
public function setUser($user = null)
|
||||
@@ -87,7 +87,7 @@ class TimesheetQuery extends ActivityQuery
|
||||
/**
|
||||
* Activity overwrites: setProject() and setCustomer()
|
||||
*
|
||||
* @return Activity
|
||||
* @return Activity|null
|
||||
*/
|
||||
public function getActivity()
|
||||
{
|
||||
@@ -95,7 +95,7 @@ class TimesheetQuery extends ActivityQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity|int $activity
|
||||
* @param Activity|int|null $activity
|
||||
* @return TimesheetQuery
|
||||
*/
|
||||
public function setActivity($activity = null)
|
||||
|
||||
@@ -15,12 +15,12 @@ namespace App\Repository\Query;
|
||||
class UserQuery extends VisibilityQuery
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*/
|
||||
protected $role;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return string|null
|
||||
*/
|
||||
public function getRole()
|
||||
{
|
||||
|
||||
@@ -274,6 +274,7 @@ class TimesheetRepository extends AbstractRepository
|
||||
->join('t.activity', 'a')
|
||||
->join('t.project', 'p')
|
||||
->join('p.customer', 'c')
|
||||
->leftJoin('t.tags', 'tags')
|
||||
->where($qb->expr()->isNotNull('t.begin'))
|
||||
->andWhere($qb->expr()->isNull('t.end'))
|
||||
->orderBy('t.begin', 'DESC');
|
||||
@@ -438,11 +439,12 @@ class TimesheetRepository extends AbstractRepository
|
||||
$ids = array_column($results, 'maxid');
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('t', 'a', 'p', 'c')
|
||||
$qb->select('t', 'a', 'p', 'c', 'tags')
|
||||
->from(Timesheet::class, 't')
|
||||
->join('t.activity', 'a')
|
||||
->join('t.project', 'p')
|
||||
->join('p.customer', 'c')
|
||||
->leftJoin('t.tags', 'tags')
|
||||
->andWhere($qb->expr()->in('t.id', $ids))
|
||||
->orderBy('t.end', 'DESC')
|
||||
;
|
||||
|
||||
@@ -20,7 +20,6 @@ class AclDecisionManager
|
||||
protected $decisionManager;
|
||||
|
||||
/**
|
||||
* AbstractVoter constructor.
|
||||
* @param AccessDecisionManagerInterface $decisionManager
|
||||
*/
|
||||
public function __construct(AccessDecisionManagerInterface $decisionManager)
|
||||
@@ -40,22 +39,4 @@ class AclDecisionManager
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TokenInterface $token
|
||||
* @param string|array $roles
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRole(TokenInterface $token, $roles)
|
||||
{
|
||||
if (!is_array($roles)) {
|
||||
$roles = [$roles];
|
||||
}
|
||||
|
||||
if ($this->decisionManager->decide($token, $roles)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,11 +49,6 @@ class RolePermissionManager
|
||||
return array_keys($this->permissions);
|
||||
}
|
||||
|
||||
public function roleHasPermission(string $role): bool
|
||||
{
|
||||
return isset($this->permissions[$role]);
|
||||
}
|
||||
|
||||
public function getPermissions(): array
|
||||
{
|
||||
return $this->knownPermissions;
|
||||
|
||||
38
src/Security/RoleService.php
Normal file
38
src/Security/RoleService.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
class RoleService
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $roles;
|
||||
|
||||
public function __construct(array $roles)
|
||||
{
|
||||
$this->roles = $roles;
|
||||
}
|
||||
|
||||
public function getAvailableNames(): array
|
||||
{
|
||||
$roles = [];
|
||||
foreach ($this->roles as $key => $value) {
|
||||
$roles[] = $key;
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $name) {
|
||||
$roles[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($roles));
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ namespace App\Security;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Exception\AccountStatusException;
|
||||
use Symfony\Component\Security\Core\Exception\LockedException;
|
||||
use Symfony\Component\Security\Core\Exception\DisabledException;
|
||||
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
@@ -26,6 +26,15 @@ class UserChecker implements UserCheckerInterface
|
||||
*/
|
||||
public function checkPreAuth(UserInterface $user)
|
||||
{
|
||||
if (!($user instanceof User)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$user->isEnabled()) {
|
||||
$ex = new DisabledException('User account is disabled.');
|
||||
$ex->setUser($user);
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,13 +43,14 @@ class UserChecker implements UserCheckerInterface
|
||||
*/
|
||||
public function checkPostAuth(UserInterface $user)
|
||||
{
|
||||
if (!$user instanceof User) {
|
||||
if (!($user instanceof User)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// user account is not enabled, the user may be notified
|
||||
if (!$user->isEnabled()) {
|
||||
throw new LockedException();
|
||||
$ex = new DisabledException('User account is disabled.');
|
||||
$ex->setUser($user);
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,16 +48,6 @@ abstract class AbstractVoter extends Voter
|
||||
return $this->decisionManager->isFullyAuthenticated($token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $role
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasRole($role, TokenInterface $token)
|
||||
{
|
||||
return $this->decisionManager->hasRole($token, [$role]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $role
|
||||
* @param string $permission
|
||||
|
||||
@@ -83,7 +83,6 @@ class UserVoter extends AbstractVoter
|
||||
// used in templates and ProfileController
|
||||
case self::VIEW:
|
||||
case self::EDIT:
|
||||
case self::PASSWORD:
|
||||
case self::PREFERENCES:
|
||||
// always allow the user to edit these own settings
|
||||
if ($subject->getId() === $user->getId()) {
|
||||
@@ -91,6 +90,7 @@ class UserVoter extends AbstractVoter
|
||||
}
|
||||
// no break on purpose
|
||||
|
||||
case self::PASSWORD:
|
||||
case self::API_TOKEN:
|
||||
case self::ROLES:
|
||||
case self::HOURLY_RATE:
|
||||
|
||||
18
symfony.lock
18
symfony.lock
@@ -327,6 +327,9 @@
|
||||
"phpstan/phpstan-doctrine": {
|
||||
"version": "0.11.4"
|
||||
},
|
||||
"phpstan/phpstan-phpunit": {
|
||||
"version": "0.11.2"
|
||||
},
|
||||
"phpstan/phpstan-symfony": {
|
||||
"version": "0.11.6"
|
||||
},
|
||||
@@ -513,6 +516,9 @@
|
||||
"symfony/intl": {
|
||||
"version": "v4.0.3"
|
||||
},
|
||||
"symfony/ldap": {
|
||||
"version": "v4.2.8"
|
||||
},
|
||||
"symfony/maker-bundle": {
|
||||
"version": "1.0",
|
||||
"recipe": {
|
||||
@@ -594,18 +600,9 @@
|
||||
"ref": "85834af1496735f28d831489d12ab1921a875e0d"
|
||||
}
|
||||
},
|
||||
"symfony/security-core": {
|
||||
"version": "v4.2.6"
|
||||
},
|
||||
"symfony/security-csrf": {
|
||||
"version": "v4.0.4"
|
||||
},
|
||||
"symfony/security-guard": {
|
||||
"version": "v4.2.6"
|
||||
},
|
||||
"symfony/security-http": {
|
||||
"version": "v4.2.6"
|
||||
},
|
||||
"symfony/serializer": {
|
||||
"version": "v4.1.2"
|
||||
},
|
||||
@@ -732,6 +729,9 @@
|
||||
"zendframework/zend-eventmanager": {
|
||||
"version": "3.2.0"
|
||||
},
|
||||
"zendframework/zend-ldap": {
|
||||
"version": "2.10.0"
|
||||
},
|
||||
"zircote/swagger-php": {
|
||||
"version": "2.0.13"
|
||||
}
|
||||
|
||||
@@ -21,11 +21,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
*/
|
||||
abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
{
|
||||
/**
|
||||
* @param string $role
|
||||
* @return Client
|
||||
*/
|
||||
protected function getClientForAuthenticatedUser(string $role = User::ROLE_USER)
|
||||
protected function getClientForAuthenticatedUser(string $role = User::ROLE_USER): Client
|
||||
{
|
||||
switch ($role) {
|
||||
case User::ROLE_SUPER_ADMIN:
|
||||
@@ -57,7 +53,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
break;
|
||||
|
||||
default:
|
||||
$client = null;
|
||||
throw new \Exception(sprintf('Unknown role "%s"', $role));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -74,11 +70,6 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
return '/' . ltrim($url, '/') . ($json ? '.json' : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Client $client
|
||||
* @param string $url
|
||||
* @param string $method
|
||||
*/
|
||||
protected function assertRequestIsSecured(Client $client, string $url, $method = 'GET')
|
||||
{
|
||||
$this->request($client, $url, $method);
|
||||
@@ -134,26 +125,13 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Client $client
|
||||
* @param string $url
|
||||
* @param string $method
|
||||
* @param array $parameters
|
||||
* @param string $content
|
||||
* @return Crawler
|
||||
*/
|
||||
protected function request(Client $client, string $url, $method = 'GET', array $parameters = [], string $content = null)
|
||||
protected function request(Client $client, string $url, $method = 'GET', array $parameters = [], string $content = null): Crawler
|
||||
{
|
||||
$server = ['HTTP_CONTENT_TYPE' => 'application/json', 'CONTENT_TYPE' => 'application/json'];
|
||||
|
||||
return $client->request($method, $this->createUrl($url), $parameters, [], $server, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $role
|
||||
* @param string $url
|
||||
* @param string $method
|
||||
*/
|
||||
protected function assertEntityNotFound(string $role, string $url, string $method = 'GET')
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser($role);
|
||||
@@ -172,11 +150,6 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $role
|
||||
* @param string $url
|
||||
* @param array $data
|
||||
*/
|
||||
protected function assertEntityNotFoundForPatch(string $role, string $url, array $data)
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser($role);
|
||||
@@ -198,11 +171,6 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $role
|
||||
* @param string $url
|
||||
* @param array $data
|
||||
*/
|
||||
protected function assertEntityNotFoundForDelete(string $role, string $url, array $data)
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser($role);
|
||||
@@ -224,10 +192,6 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Response $response
|
||||
* @param string $message
|
||||
*/
|
||||
protected function assertApiException(Response $response, string $message)
|
||||
{
|
||||
$this->assertFalse($response->isSuccessful());
|
||||
@@ -235,22 +199,12 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
$this->assertEquals(['code' => 500, 'message' => $message], json_decode($response->getContent(), true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Client $client
|
||||
* @param string $url
|
||||
* @param string $message
|
||||
*/
|
||||
protected function assertApiAccessDenied(Client $client, string $url, string $message)
|
||||
{
|
||||
$this->request($client, $url);
|
||||
$this->assertApiResponseAccessDenied($client->getResponse(), $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Client $client
|
||||
* @param string $url
|
||||
* @param string $message
|
||||
*/
|
||||
protected function assertApiResponseAccessDenied(Response $response, string $message)
|
||||
{
|
||||
$this->assertFalse($response->isSuccessful());
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\API;
|
||||
namespace App\Tests\API\Model;
|
||||
|
||||
use App\API\Model\I18n;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Command\InstallCommand;
|
||||
use App\Utils\File;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
/**
|
||||
@@ -26,7 +27,7 @@ class InstallCommandTest extends KernelTestCase
|
||||
*/
|
||||
protected $application;
|
||||
|
||||
protected function getCommand($permission = 0777): InstallCommand
|
||||
protected function getCommand($permission = 0777): Command
|
||||
{
|
||||
$fileMock = $this->getMockBuilder(File::class)->setMethods(['getPermissions'])->getMock();
|
||||
$fileMock->expects($this->exactly(5))->method('getPermissions')->willReturn($permission);
|
||||
|
||||
52
tests/Configuration/LdapConfigurationTest.php
Normal file
52
tests/Configuration/LdapConfigurationTest.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Configuration;
|
||||
|
||||
use App\Configuration\LdapConfiguration;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Configuration\LdapConfiguration
|
||||
*/
|
||||
class LdapConfigurationTest extends TestCase
|
||||
{
|
||||
protected function getSut(array $settings)
|
||||
{
|
||||
return new LdapConfiguration($settings);
|
||||
}
|
||||
|
||||
protected function getDefaultSettings()
|
||||
{
|
||||
return [
|
||||
'active' => true,
|
||||
'connection' => [
|
||||
'host' => '1.2.3.4',
|
||||
],
|
||||
'user' => [
|
||||
'foo' => 'bar',
|
||||
],
|
||||
'role' => [
|
||||
'bar' => 'foo',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testMapping()
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings());
|
||||
$this->assertTrue($sut->isActivated());
|
||||
$this->assertEquals(['foo' => 'bar'], $sut->getUserParameters());
|
||||
$this->assertEquals(['bar' => 'foo'], $sut->getRoleParameters());
|
||||
$this->assertEquals(['host' => '1.2.3.4'], $sut->getConnectionParameters());
|
||||
|
||||
$sut = $this->getSut(['active' => false]);
|
||||
$this->assertFalse($sut->isActivated());
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ class ActivityControllerTest extends ControllerBaseTest
|
||||
$this->assertAccessIsGranted($client, '/admin/activity/create');
|
||||
$form = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
|
||||
$this->assertTrue($form->has('activity_edit_form[create_more]'));
|
||||
$this->assertNull($form->get('activity_edit_form[create_more]')->getValue());
|
||||
$this->assertFalse($form->get('activity_edit_form[create_more]')->hasValue());
|
||||
$client->submit($form, [
|
||||
'activity_edit_form' => [
|
||||
'name' => 'An AcTiVitY Name',
|
||||
@@ -87,6 +87,7 @@ class ActivityControllerTest extends ControllerBaseTest
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
$form = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
|
||||
$this->assertTrue($form->has('activity_edit_form[create_more]'));
|
||||
$this->assertTrue($form->get('activity_edit_form[create_more]')->hasValue());
|
||||
$this->assertEquals(1, $form->get('activity_edit_form[create_more]')->getValue());
|
||||
$this->assertEquals($selectedProject, $form->get('activity_edit_form[project]')->getValue());
|
||||
}
|
||||
|
||||
@@ -95,12 +95,13 @@ abstract class ControllerBaseTest extends WebTestCase
|
||||
* @param string $url
|
||||
* @param string $method
|
||||
*/
|
||||
protected function assertRequestIsSecured(Client $client, string $url, $method = 'GET')
|
||||
protected function assertRequestIsSecured(Client $client, string $url, ?string $method = 'GET')
|
||||
{
|
||||
$this->request($client, $url, $method);
|
||||
|
||||
/* @var RedirectResponse $response */
|
||||
/** @var RedirectResponse $response */
|
||||
$response = $client->getResponse();
|
||||
self::assertInstanceOf(RedirectResponse::class, $response);
|
||||
|
||||
$this->assertTrue(
|
||||
$response->isRedirect(),
|
||||
@@ -153,32 +154,19 @@ abstract class ControllerBaseTest extends WebTestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Client $client
|
||||
* @param $url
|
||||
* @param string $method
|
||||
* @param array $parameters
|
||||
*/
|
||||
protected function assertAccessIsGranted(Client $client, $url, $method = 'GET', array $parameters = [])
|
||||
protected function assertAccessIsGranted(Client $client, string $url, string $method = 'GET', array $parameters = [])
|
||||
{
|
||||
$this->request($client, $url, $method, $parameters);
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Client $client
|
||||
*/
|
||||
protected function assertRouteNotFound(Client $client)
|
||||
{
|
||||
$this->assertFalse($client->getResponse()->isSuccessful());
|
||||
$this->assertEquals(404, $client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Client $client
|
||||
* @param string $classname
|
||||
*/
|
||||
protected function assertMainContentClass(Client $client, $classname)
|
||||
protected function assertMainContentClass(Client $client, string $classname)
|
||||
{
|
||||
$this->assertContains('<section class="content ' . $classname . '">', $client->getResponse()->getContent());
|
||||
}
|
||||
@@ -279,19 +267,11 @@ abstract class ControllerBaseTest extends WebTestCase
|
||||
$this->assertContains($message, $node->text());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Client $client
|
||||
* @param string|null $message
|
||||
*/
|
||||
protected function assertHasFlashDeleteSuccess(Client $client)
|
||||
{
|
||||
$this->assertHasFlashSuccess($client, 'Entry was deleted');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Client $client
|
||||
* @param string|null $message
|
||||
*/
|
||||
protected function assertHasFlashSaveSuccess(Client $client)
|
||||
{
|
||||
$this->assertHasFlashSuccess($client, 'Saved changes');
|
||||
|
||||
@@ -39,7 +39,7 @@ class ProjectControllerTest extends ControllerBaseTest
|
||||
$this->assertAccessIsGranted($client, '/admin/project/create');
|
||||
$form = $client->getCrawler()->filter('form[name=project_edit_form]')->form();
|
||||
$this->assertTrue($form->has('project_edit_form[create_more]'));
|
||||
$this->assertNull($form->get('project_edit_form[create_more]')->getValue());
|
||||
$this->assertFalse($form->get('project_edit_form[create_more]')->hasValue());
|
||||
$client->submit($form, [
|
||||
'project_edit_form' => [
|
||||
'name' => 'Test 2',
|
||||
@@ -80,6 +80,7 @@ class ProjectControllerTest extends ControllerBaseTest
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
$form = $client->getCrawler()->filter('form[name=project_edit_form]')->form();
|
||||
$this->assertTrue($form->has('project_edit_form[create_more]'));
|
||||
$this->assertTrue($form->get('project_edit_form[create_more]')->hasValue());
|
||||
$this->assertEquals(1, $form->get('project_edit_form[create_more]')->getValue());
|
||||
$this->assertEquals($selectedCustomer, $form->get('project_edit_form[customer]')->getValue());
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
|
||||
foreach ($result as $item) {
|
||||
$this->assertContains('btn btn-default', $item->getAttribute('class'));
|
||||
$this->assertEquals('i', $item->firstChild->tagName);
|
||||
/** @var \DOMElement $domElement */
|
||||
$domElement = $item->firstChild;
|
||||
$this->assertEquals('i', $domElement->tagName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
|
||||
|
||||
foreach ($result as $item) {
|
||||
$this->assertContains('btn btn-default', $item->getAttribute('class'));
|
||||
$this->assertEquals('i', $item->firstChild->tagName);
|
||||
/** @var \DOMElement $domElement */
|
||||
$domElement = $item->firstChild;
|
||||
$this->assertEquals('i', $domElement->tagName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ class UserControllerTest extends ControllerBaseTest
|
||||
$form = $client->getCrawler()->filter('form[name=user_create]')->form();
|
||||
$this->assertTrue($form->has('user_create[create_more]'));
|
||||
$this->assertFalse($form->get('user_create[create_more]')->hasValue());
|
||||
$this->assertNull($form->get('user_create[create_more]')->getValue());
|
||||
$client->submit($form, [
|
||||
'user_create' => [
|
||||
'username' => $username,
|
||||
@@ -86,6 +85,7 @@ class UserControllerTest extends ControllerBaseTest
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
$form = $client->getCrawler()->filter('form[name=user_create]')->form();
|
||||
$this->assertTrue($form->has('user_create[create_more]'));
|
||||
$this->assertTrue($form->get('user_create[create_more]')->hasValue());
|
||||
$this->assertEquals(1, $form->get('user_create[create_more]')->getValue());
|
||||
}
|
||||
|
||||
|
||||
@@ -54,11 +54,7 @@ class TagFixtures extends Fixture
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $tagName
|
||||
* @return Tag
|
||||
*/
|
||||
protected function createTagEntry($tagName)
|
||||
protected function createTagEntry(string $tagName): Tag
|
||||
{
|
||||
$tagObject = new Tag();
|
||||
$tagObject->setName($tagName);
|
||||
|
||||
@@ -58,7 +58,7 @@ class TimesheetFixtures extends Fixture
|
||||
*/
|
||||
protected $allowEmptyDescriptions = true;
|
||||
/**
|
||||
* @var int
|
||||
* @var bool
|
||||
*/
|
||||
protected $exported = false;
|
||||
/**
|
||||
@@ -265,11 +265,7 @@ class TimesheetFixtures extends Fixture
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $cnt
|
||||
* @return array
|
||||
*/
|
||||
protected function getTagObjectList($cnt)
|
||||
protected function getTagObjectList(int $cnt): array
|
||||
{
|
||||
if (true === $this->useTags) {
|
||||
$tagObject = new Tag();
|
||||
@@ -281,11 +277,7 @@ class TimesheetFixtures extends Fixture
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $i
|
||||
* @return bool|\DateTime
|
||||
*/
|
||||
protected function getDateTime($i)
|
||||
protected function getDateTime(int $i): \DateTime
|
||||
{
|
||||
$start = \DateTime::createFromFormat('Y-m-d', $this->startDate);
|
||||
$start->modify("+ $i days");
|
||||
|
||||
316
tests/DependencyInjection/AppExtensionTest.php
Normal file
316
tests/DependencyInjection/AppExtensionTest.php
Normal file
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\DependencyInjection;
|
||||
|
||||
use App\DependencyInjection\AppExtension;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
/**
|
||||
* @covers \App\DependencyInjection\AppExtension
|
||||
*/
|
||||
class AppExtensionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var AppExtension
|
||||
*/
|
||||
private $extension;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->extension = new AppExtension();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ContainerBuilder
|
||||
*/
|
||||
private function getContainer()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
protected function getMinConfig()
|
||||
{
|
||||
return [
|
||||
'kimai' => [
|
||||
'data_dir' => '/tmp/',
|
||||
'plugin_dir' => '/tmp/',
|
||||
'timesheet' => [],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$minConfig = $this->getMinConfig();
|
||||
|
||||
$this->extension->load($minConfig, $container = $this->getContainer());
|
||||
|
||||
$expected = [
|
||||
'kimai.data_dir' => '/tmp/',
|
||||
'kimai.plugin_dir' => '/tmp/',
|
||||
'kimai.languages' => [],
|
||||
'kimai.calendar' => [
|
||||
'week_numbers' => true,
|
||||
'day_limit' => 4,
|
||||
'businessHours' => [
|
||||
'days' => [1, 2, 3, 4, 5],
|
||||
'begin' => '08:00',
|
||||
'end' => '20:00',
|
||||
],
|
||||
'visibleHours' => [
|
||||
'begin' => '00:00',
|
||||
'end' => '24:00',
|
||||
],
|
||||
'google' => [
|
||||
'api_key' => null,
|
||||
'sources' => [],
|
||||
],
|
||||
'weekends' => true
|
||||
],
|
||||
'kimai.dashboard' => [],
|
||||
'kimai.widgets' => [],
|
||||
'kimai.invoice.documents' => [
|
||||
'var/invoices/',
|
||||
'templates/invoice/renderer/',
|
||||
],
|
||||
'kimai.defaults' => [
|
||||
'customer' => [
|
||||
'timezone' => 'Europe/Berlin',
|
||||
'country' => 'DE',
|
||||
'currency' => 'EUR',
|
||||
]
|
||||
],
|
||||
|
||||
'kimai.theme' => [
|
||||
'active_warning' => 3,
|
||||
'box_color' => 'green',
|
||||
'select_type' => null,
|
||||
'show_about' => true,
|
||||
],
|
||||
'kimai.theme.select_type' => null,
|
||||
'kimai.theme.show_about' => true,
|
||||
|
||||
'kimai.fosuser' => [
|
||||
'registration' => true,
|
||||
'password_reset' => true,
|
||||
],
|
||||
|
||||
'kimai.timesheet' => [
|
||||
'mode' => 'default',
|
||||
'markdown_content' => false,
|
||||
'rounding' => [],
|
||||
'rates' => [],
|
||||
'active_entries' => [
|
||||
'soft_limit' => 1,
|
||||
'hard_limit' => 1,
|
||||
],
|
||||
'rules' => [
|
||||
'allow_future_times' => true,
|
||||
],
|
||||
],
|
||||
'kimai.timesheet.rates' => [],
|
||||
'kimai.timesheet.rounding' => [],
|
||||
'kimai.ldap' => [
|
||||
'active' => false,
|
||||
'user' => [
|
||||
'baseDn' => null,
|
||||
'filter' => '',
|
||||
'usernameAttribute' => 'uid',
|
||||
'attributes' => [],
|
||||
],
|
||||
'role' => [
|
||||
'baseDn' => null,
|
||||
'nameAttribute' => 'cn',
|
||||
'userDnAttribute' => 'member',
|
||||
'groups' => [],
|
||||
'usernameAttribute' => 'dn',
|
||||
],
|
||||
'connection' => [
|
||||
'baseDn' => null,
|
||||
'host' => null,
|
||||
'port' => 389,
|
||||
'useStartTls' => false,
|
||||
'useSsl' => false,
|
||||
'bindRequiresDn' => true,
|
||||
'accountFilterFormat' => '(&(uid=%s))',
|
||||
],
|
||||
],
|
||||
'kimai.permissions' => [
|
||||
'ROLE_USER' => [],
|
||||
'ROLE_TEAMLEAD' => [],
|
||||
'ROLE_ADMIN' => [],
|
||||
'ROLE_SUPER_ADMIN' => [],
|
||||
],
|
||||
];
|
||||
|
||||
// nasty parameter, should be removed!!!
|
||||
$this->assertTrue($container->hasParameter('kimai.config'));
|
||||
|
||||
foreach ($expected as $key => $value) {
|
||||
$this->assertTrue($container->hasParameter($key), 'Could not find config: ' . $key);
|
||||
$this->assertEquals($value, $container->getParameter($key), 'Invalid config: ' . $key);
|
||||
}
|
||||
}
|
||||
|
||||
public function testAdditionalAuthenticationRoutes()
|
||||
{
|
||||
$minConfig = $this->getMinConfig();
|
||||
$adminLte = [
|
||||
'adminlte_registration' => 'foo',
|
||||
'adminlte_password_reset' => 'bar',
|
||||
];
|
||||
|
||||
$container = $this->getContainer();
|
||||
$container->setParameter('admin_lte_theme.routes', $adminLte);
|
||||
|
||||
$this->extension->load($minConfig, $container);
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
'adminlte_registration' => 'foo',
|
||||
'adminlte_password_reset' => 'bar',
|
||||
],
|
||||
$container->getParameter('admin_lte_theme.routes')
|
||||
);
|
||||
}
|
||||
|
||||
public function testDeactivateAdditionalAuthenticationRoutes()
|
||||
{
|
||||
$minConfig = $this->getMinConfig();
|
||||
$minConfig['kimai']['user'] = [
|
||||
'registration' => false,
|
||||
'password_reset' => false,
|
||||
];
|
||||
$adminLte = [
|
||||
'adminlte_registration' => 'foo',
|
||||
'adminlte_password_reset' => 'bar',
|
||||
];
|
||||
|
||||
$container = $this->getContainer();
|
||||
$container->setParameter('admin_lte_theme.routes', $adminLte);
|
||||
|
||||
$this->extension->load($minConfig, $container);
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
'adminlte_registration' => null,
|
||||
'adminlte_password_reset' => null,
|
||||
],
|
||||
$container->getParameter('admin_lte_theme.routes')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \PHPUnit\Framework\Error\Notice
|
||||
* @expectedExceptionMessage Found ambiguous configuration. Please remove "kimai.timesheet.duration_only" and set "kimai.timesheet.mode" instead.
|
||||
* @expectedDeprecation Configuration "kimai.timesheet.duration_only" is deprecated, please remove it
|
||||
* @group legacy
|
||||
*/
|
||||
public function testDurationOnlyDeprecationIsTriggered()
|
||||
{
|
||||
$minConfig = $this->getMinConfig();
|
||||
$minConfig['kimai']['timesheet']['duration_only'] = true;
|
||||
$minConfig['kimai']['timesheet']['mode'] = 'punch';
|
||||
|
||||
$this->extension->load($minConfig, $container = $this->getContainer());
|
||||
}
|
||||
|
||||
public function testLdapDefaultValues()
|
||||
{
|
||||
$minConfig = $this->getMinConfig();
|
||||
$minConfig['kimai']['ldap'] = [
|
||||
'active' => true,
|
||||
'connection' => [
|
||||
'host' => '9.9.9.9',
|
||||
'baseDn' => 'lkhiuzhkj',
|
||||
'accountFilterFormat' => '(uid=%s)'
|
||||
],
|
||||
'user' => [
|
||||
'baseDn' => '123123123',
|
||||
'usernameAttribute' => 'xxx',
|
||||
'filter' => '(..........)'
|
||||
],
|
||||
];
|
||||
|
||||
$this->extension->load($minConfig, $container = $this->getContainer());
|
||||
|
||||
$ldapConfig = $container->getParameter('kimai.ldap');
|
||||
$this->assertEquals('123123123', $ldapConfig['user']['baseDn']);
|
||||
$this->assertEquals('(..........)', $ldapConfig['user']['filter']);
|
||||
$this->assertEquals('xxx', $ldapConfig['user']['usernameAttribute']);
|
||||
$this->assertEquals('lkhiuzhkj', $ldapConfig['connection']['baseDn']);
|
||||
$this->assertEquals('(uid=%s)', $ldapConfig['connection']['accountFilterFormat']);
|
||||
}
|
||||
|
||||
public function testLdapFallbackValue()
|
||||
{
|
||||
$minConfig = $this->getMinConfig();
|
||||
$minConfig['kimai']['ldap'] = [
|
||||
'active' => true,
|
||||
'connection' => [
|
||||
'host' => '9.9.9.9',
|
||||
],
|
||||
'user' => [
|
||||
'baseDn' => '123123123',
|
||||
'usernameAttribute' => 'xxx',
|
||||
],
|
||||
];
|
||||
|
||||
$this->extension->load($minConfig, $container = $this->getContainer());
|
||||
|
||||
$ldapConfig = $container->getParameter('kimai.ldap');
|
||||
$this->assertEquals('123123123', $ldapConfig['user']['baseDn']);
|
||||
$this->assertEquals('xxx', $ldapConfig['user']['usernameAttribute']);
|
||||
$this->assertEquals('123123123', $ldapConfig['connection']['baseDn']);
|
||||
$this->assertEquals('(&(xxx=%s))', $ldapConfig['connection']['accountFilterFormat']);
|
||||
$this->assertEquals('', $ldapConfig['user']['filter']);
|
||||
}
|
||||
|
||||
public function testLdapMoreFallbackValue()
|
||||
{
|
||||
$minConfig = $this->getMinConfig();
|
||||
$minConfig['kimai']['ldap'] = [
|
||||
'active' => true,
|
||||
'connection' => [
|
||||
'host' => '9.9.9.9',
|
||||
'baseDn' => '7658765',
|
||||
],
|
||||
'user' => [
|
||||
'baseDn' => '123123123',
|
||||
'usernameAttribute' => 'zzzz',
|
||||
'filter' => '(&(objectClass=inetOrgPerson))',
|
||||
],
|
||||
];
|
||||
|
||||
$this->extension->load($minConfig, $container = $this->getContainer());
|
||||
|
||||
$ldapConfig = $container->getParameter('kimai.ldap');
|
||||
$this->assertEquals('123123123', $ldapConfig['user']['baseDn']);
|
||||
$this->assertEquals('zzzz', $ldapConfig['user']['usernameAttribute']);
|
||||
$this->assertEquals('7658765', $ldapConfig['connection']['baseDn']);
|
||||
$this->assertEquals('(&(&(objectClass=inetOrgPerson))(zzzz=%s))', $ldapConfig['connection']['accountFilterFormat']);
|
||||
$this->assertEquals('(&(objectClass=inetOrgPerson))', $ldapConfig['user']['filter']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \PHPUnit\Framework\Error\Notice
|
||||
* @expectedExceptionMessage Found invalid "kimai" configuration: The child node "data_dir" at path "kimai" must be configured.
|
||||
*/
|
||||
public function testInvalidConfiguration()
|
||||
{
|
||||
$this->extension->load([], $container = $this->getContainer());
|
||||
}
|
||||
|
||||
// TODO test permissions
|
||||
}
|
||||
246
tests/DependencyInjection/ConfigurationTest.php
Normal file
246
tests/DependencyInjection/ConfigurationTest.php
Normal file
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\DependencyInjection;
|
||||
|
||||
use App\DependencyInjection\Configuration;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\DependencyInjection\Configuration
|
||||
*/
|
||||
class ConfigurationTest extends TestCase
|
||||
{
|
||||
protected function getMinConfig($dataDir = '/tmp/', $pluginDir = '/tmp/')
|
||||
{
|
||||
return [
|
||||
'data_dir' => $dataDir,
|
||||
'plugin_dir' => $pluginDir,
|
||||
'timesheet' => [],
|
||||
];
|
||||
}
|
||||
|
||||
protected function assertConfig($inputConfig, $expectedConfig)
|
||||
{
|
||||
$finalizedConfig = $this->getCompiledConfig($inputConfig);
|
||||
|
||||
self::assertEquals($expectedConfig, $finalizedConfig);
|
||||
}
|
||||
|
||||
protected function getCompiledConfig($inputConfig)
|
||||
{
|
||||
$configuration = new Configuration();
|
||||
|
||||
$node = $configuration->getConfigTreeBuilder()->buildTree();
|
||||
$normalizedConfig = $node->normalize($inputConfig);
|
||||
|
||||
return $node->finalize($normalizedConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
|
||||
* @expectedExceptionMessage Invalid configuration for path "kimai.data_dir": Data directory does not exist
|
||||
*/
|
||||
public function testValidateDataDir()
|
||||
{
|
||||
$this->assertConfig($this->getMinConfig('sdfsdfsdfds'), []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
|
||||
* @expectedExceptionMessage Invalid configuration for path "kimai.plugin_dir": Plugin directory does not exist
|
||||
*/
|
||||
public function testValidatePluginDir()
|
||||
{
|
||||
$this->assertConfig($this->getMinConfig('/tmp/', 'sdfsdfs'), []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
|
||||
* @expectedExceptionMessage Invalid configuration for path "kimai.ldap": The "ldap.user.baseDn" config must be set if LDAP is activated.
|
||||
*/
|
||||
public function testValidateLdapConfigUserBaseDn()
|
||||
{
|
||||
$config = $this->getMinConfig();
|
||||
$config['ldap'] = [
|
||||
'active' => true,
|
||||
'connection' => [
|
||||
'host' => 'foo'
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertConfig($config, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
|
||||
* @expectedExceptionMessage Invalid configuration for path "kimai.ldap": The "ldap.connection.host" config must be set if LDAP is activated.
|
||||
*/
|
||||
public function testValidateLdapConfigConnectionHost()
|
||||
{
|
||||
$config = $this->getMinConfig();
|
||||
$config['ldap'] = [
|
||||
'active' => true,
|
||||
'connection' => [
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertConfig($config, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
|
||||
* @expectedExceptionMessage Invalid configuration for path "kimai.ldap.connection": The ldap.connection.useSsl and ldap.connection.useStartTls options are mutually exclusive.
|
||||
*/
|
||||
public function testValidateLdapConfig()
|
||||
{
|
||||
$config = $this->getMinConfig();
|
||||
$config['ldap'] = [
|
||||
'active' => false,
|
||||
'connection' => [
|
||||
'useSsl' => true,
|
||||
'useStartTls' => true,
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertConfig($config, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
|
||||
* @expectedExceptionMessage Invalid configuration for path "kimai.ldap.user.filter": The ldap.user.filter must be enclosed by a matching number of parentheses "()" and must NOT contain a "%s" replacer
|
||||
*/
|
||||
public function testValidateLdapFilterIncludingReplacer()
|
||||
{
|
||||
$config = $this->getMinConfig();
|
||||
$config['ldap'] = [
|
||||
'active' => true,
|
||||
'user' => [
|
||||
'filter' => '(sdfsdfsdf)(uid=%s)',
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertConfig($config, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
|
||||
* @expectedExceptionMessage Invalid configuration for path "kimai.ldap.user.filter": The ldap.user.filter must be enclosed by a matching number of parentheses "()" and must NOT contain a "%s" replacer
|
||||
*/
|
||||
public function testValidateLdapFilterMissingStartingParenthesis()
|
||||
{
|
||||
$config = $this->getMinConfig();
|
||||
$config['ldap'] = [
|
||||
'active' => true,
|
||||
'user' => [
|
||||
'filter' => 's(dfsdfsdf)',
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertConfig($config, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
|
||||
* @expectedExceptionMessage Invalid configuration for path "kimai.ldap.user.filter": The ldap.user.filter must be enclosed by a matching number of parentheses "()" and must NOT contain a "%s" replacer
|
||||
*/
|
||||
public function testValidateLdapFilterInvalidParenthesisCounter()
|
||||
{
|
||||
$config = $this->getMinConfig();
|
||||
$config['ldap'] = [
|
||||
'active' => true,
|
||||
'user' => [
|
||||
'filter' => '(dfsdfsdf))',
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertConfig($config, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
|
||||
* @expectedExceptionMessage Invalid configuration for path "kimai.ldap.connection.accountFilterFormat": The accountFilterFormat must be enclosed by a matching number of parentheses "()" and contain one "%s" replacer for the username
|
||||
*/
|
||||
public function testValidateLdapAccountFilterFormatMissingUserAttributeReplacer()
|
||||
{
|
||||
$config = $this->getMinConfig();
|
||||
$config['ldap'] = [
|
||||
'active' => false,
|
||||
'connection' => [
|
||||
'accountFilterFormat' => '(sdfsdfsdf)(uid=xx)',
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertConfig($config, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
|
||||
* @expectedExceptionMessage Invalid configuration for path "kimai.ldap.connection.accountFilterFormat": The accountFilterFormat must be enclosed by a matching number of parentheses "()" and contain one "%s" replacer for the username
|
||||
*/
|
||||
public function testValidateLdapAccountFilterFormatMissingStartingParenthesis()
|
||||
{
|
||||
$config = $this->getMinConfig();
|
||||
$config['ldap'] = [
|
||||
'active' => true,
|
||||
'connection' => [
|
||||
'accountFilterFormat' => 's(dfsdfsdf)',
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertConfig($config, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
|
||||
* @expectedExceptionMessage Invalid configuration for path "kimai.ldap.connection.accountFilterFormat": The accountFilterFormat must be enclosed by a matching number of parentheses "()" and contain one "%s" replacer for the username
|
||||
*/
|
||||
public function testValidateLdapAccountFilterFormatInvalidParenthesisCounter()
|
||||
{
|
||||
$config = $this->getMinConfig();
|
||||
$config['ldap'] = [
|
||||
'active' => true,
|
||||
'connection' => [
|
||||
'accountFilterFormat' => '(dfsdfsdf))',
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertConfig($config, []);
|
||||
}
|
||||
|
||||
public function testDefaultLdapSettings()
|
||||
{
|
||||
$finalizedConfig = $this->getCompiledConfig($this->getMinConfig());
|
||||
$expected = [
|
||||
'active' => false,
|
||||
'user' => [
|
||||
'baseDn' => '',
|
||||
'filter' => '',
|
||||
'usernameAttribute' => 'uid',
|
||||
'attributes' => [],
|
||||
],
|
||||
'role' => [
|
||||
'baseDn' => null,
|
||||
'usernameAttribute' => 'dn',
|
||||
'nameAttribute' => 'cn',
|
||||
'userDnAttribute' => 'member',
|
||||
'groups' => [],
|
||||
],
|
||||
'connection' => [
|
||||
'host' => null,
|
||||
'port' => 389,
|
||||
'useStartTls' => false,
|
||||
'useSsl' => false,
|
||||
'bindRequiresDn' => true,
|
||||
'accountFilterFormat' => '',
|
||||
]
|
||||
];
|
||||
self::assertEquals($expected, $finalizedConfig['ldap']);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\EventSubscriber;
|
||||
namespace App\Tests\Doctrine;
|
||||
|
||||
use App\Doctrine\SqliteSessionInitSubscriber;
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
@@ -18,10 +18,10 @@ use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
abstract class AbstractEntityTest extends KernelTestCase
|
||||
{
|
||||
/**
|
||||
* @param $entity
|
||||
* @param object $entity
|
||||
* @param array|string $fieldNames
|
||||
*/
|
||||
protected function assertHasViolationForField($entity, $fieldNames)
|
||||
protected function assertHasViolationForField(object $entity, $fieldNames)
|
||||
{
|
||||
self::bootKernel();
|
||||
$validator = static::$kernel->getContainer()->get('validator');
|
||||
|
||||
@@ -24,7 +24,7 @@ class ActivityTest extends AbstractEntityTest
|
||||
$this->assertNull($sut->getName());
|
||||
$this->assertNull($sut->getComment());
|
||||
$this->assertTrue($sut->getVisible());
|
||||
// timesheets
|
||||
self::assertIsIterable($sut->getTimesheets());
|
||||
$this->assertNull($sut->getFixedRate());
|
||||
$this->assertNull($sut->getHourlyRate());
|
||||
$this->assertNull($sut->getColor());
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
|
||||
/**
|
||||
* @covers \App\Entity\Customer
|
||||
@@ -24,7 +23,8 @@ class CustomerTest extends AbstractEntityTest
|
||||
$this->assertNull($sut->getName());
|
||||
$this->assertNull($sut->getNumber());
|
||||
$this->assertNull($sut->getComment());
|
||||
// projects
|
||||
self::assertIsIterable($sut->getProjects());
|
||||
self::assertEmpty($sut->getProjects());
|
||||
$this->assertTrue($sut->getVisible());
|
||||
|
||||
$this->assertNull($sut->getCompany());
|
||||
@@ -61,10 +61,6 @@ class CustomerTest extends AbstractEntityTest
|
||||
$this->assertInstanceOf(Customer::class, $sut->setColor('#fffccc'));
|
||||
$this->assertEquals('#fffccc', $sut->getColor());
|
||||
|
||||
$projects = [(new Project())->setName('Test')];
|
||||
$this->assertInstanceOf(Customer::class, $sut->setProjects($projects));
|
||||
$this->assertSame($projects, $sut->getProjects());
|
||||
|
||||
$this->assertInstanceOf(Customer::class, $sut->setCompany('test company'));
|
||||
$this->assertEquals('test company', $sut->getCompany());
|
||||
|
||||
|
||||
@@ -9,10 +9,8 @@
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
|
||||
/**
|
||||
* @covers \App\Entity\Project
|
||||
@@ -29,10 +27,12 @@ class ProjectTest extends AbstractEntityTest
|
||||
$this->assertNull($sut->getComment());
|
||||
$this->assertTrue($sut->getVisible());
|
||||
$this->assertEquals(0.0, $sut->getBudget());
|
||||
// activities
|
||||
$this->assertNull($sut->getFixedRate());
|
||||
$this->assertNull($sut->getHourlyRate());
|
||||
$this->assertNull($sut->getTimesheets());
|
||||
self::assertIsIterable($sut->getTimesheets());
|
||||
self::assertEmpty($sut->getTimesheets());
|
||||
self::assertIsIterable($sut->getActivities());
|
||||
self::assertEmpty($sut->getActivities());
|
||||
$this->assertNull($sut->getColor());
|
||||
}
|
||||
|
||||
@@ -62,17 +62,9 @@ class ProjectTest extends AbstractEntityTest
|
||||
$this->assertInstanceOf(Project::class, $sut->setBudget(12345.67));
|
||||
$this->assertEquals(12345.67, $sut->getBudget());
|
||||
|
||||
$activities = [(new Activity())->setName('foo')];
|
||||
$this->assertInstanceOf(Project::class, $sut->setActivities($activities));
|
||||
$this->assertSame($activities, $sut->getActivities());
|
||||
|
||||
$this->assertInstanceOf(Project::class, $sut->setFixedRate(13.47));
|
||||
$this->assertEquals(13.47, $sut->getFixedRate());
|
||||
$this->assertInstanceOf(Project::class, $sut->setHourlyRate(99));
|
||||
$this->assertEquals(99, $sut->getHourlyRate());
|
||||
|
||||
$timesheets = [(new Timesheet())->setDescription('foo'), (new Timesheet())->setDescription('bar')];
|
||||
$this->assertInstanceOf(Project::class, $sut->setTimesheets($timesheets));
|
||||
$this->assertSame($timesheets, $sut->getTimesheets());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,13 @@ class UserTest extends AbstractEntityTest
|
||||
$user->addPreference($preference);
|
||||
$this->assertEquals('foobar', $user->getPreferenceValue('test', 'foo'));
|
||||
$this->assertEquals($preference, $user->getPreference('test'));
|
||||
|
||||
$user->setPreferenceValue('test', 'Hello World');
|
||||
$this->assertEquals('Hello World', $user->getPreferenceValue('test', 'foo'));
|
||||
|
||||
$this->assertNull($user->getPreferenceValue('test2'));
|
||||
$user->setPreferenceValue('test2', 'I like rain');
|
||||
$this->assertEquals('I like rain', $user->getPreferenceValue('test2'));
|
||||
}
|
||||
|
||||
public function testToString()
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\EventSubscriber;
|
||||
namespace App\Tests\Event;
|
||||
|
||||
use App\Event\ConfigureAdminMenuEvent;
|
||||
use KevinPapst\AdminLTEBundle\Model\MenuItemModel;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\EventSubscriber;
|
||||
namespace App\Tests\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\DashboardEvent;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\EventSubscriber;
|
||||
namespace App\Tests\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\ThemeEvent;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\EventSubscriber;
|
||||
namespace App\Tests\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
|
||||
@@ -7,16 +7,17 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Renderer;
|
||||
namespace App\Tests\Form\DataTransformer;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use App\Form\DataTransformer\TagArrayToStringTransformer;
|
||||
use App\Repository\TagRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Form\DataTransformer\TagArrayToStringTransformer
|
||||
*/
|
||||
class TagArrayToStringTransformerTest extends AbstractRendererTest
|
||||
class TagArrayToStringTransformerTest extends TestCase
|
||||
{
|
||||
public function testTransform()
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Invoice\Calculator;
|
||||
namespace App\Tests\Invoice\NumberGenerator;
|
||||
|
||||
use App\Invoice\NumberGenerator\DateNumberGenerator;
|
||||
use App\Model\InvoiceModel;
|
||||
|
||||
@@ -190,11 +190,7 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @return InvoiceModel
|
||||
*/
|
||||
protected function getInvoiceModelOneEntry()
|
||||
protected function getInvoiceModelOneEntry(): InvoiceModel
|
||||
{
|
||||
$customer = new Customer();
|
||||
$customer->setCurrency('USD');
|
||||
|
||||
@@ -54,7 +54,7 @@ class DebugRenderer implements RendererInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $amount
|
||||
* @param mixed $amount
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getFormattedMoney($amount)
|
||||
@@ -72,7 +72,7 @@ class DebugRenderer implements RendererInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $seconds
|
||||
* @param mixed $seconds
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getFormattedDuration($seconds)
|
||||
|
||||
50
tests/Ldap/FormLoginLdapFactoryTest.php
Normal file
50
tests/Ldap/FormLoginLdapFactoryTest.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Ldap;
|
||||
|
||||
use App\Ldap\FormLoginLdapFactory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
/**
|
||||
* @covers \App\Ldap\FormLoginLdapFactory
|
||||
*/
|
||||
class FormLoginLdapFactoryTest extends TestCase
|
||||
{
|
||||
public function testStaticValues()
|
||||
{
|
||||
$sut = new FormLoginLdapFactory();
|
||||
self::assertEquals('kimai_ldap', $sut->getKey());
|
||||
self::assertEquals('pre_auth', $sut->getPosition());
|
||||
}
|
||||
|
||||
public function testCreate()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$sut = new FormLoginLdapFactory();
|
||||
$result = $sut->create($container, 'test', ['foo' => 'bar'], 'fosuserbundle', 'secured_area');
|
||||
|
||||
self::assertEquals([
|
||||
'kimai_ldap.security.authentication.provider.test',
|
||||
'security.authentication.listener.form.test',
|
||||
'secured_area'
|
||||
], $result);
|
||||
|
||||
$definition = $container->getDefinition('kimai_ldap.security.authentication.provider.test');
|
||||
self::assertInstanceOf(ChildDefinition::class, $definition);
|
||||
self::assertEquals('test', $definition->getArguments()['index_1']);
|
||||
|
||||
$definition = $container->getDefinition('security.authentication.listener.form.test');
|
||||
self::assertInstanceOf(ChildDefinition::class, $definition);
|
||||
self::assertEquals('test', $definition->getArguments()['index_4']);
|
||||
self::assertEquals(['foo' => 'bar'], $definition->getArguments()['index_5']);
|
||||
}
|
||||
}
|
||||
238
tests/Ldap/LdapAuthenticationProviderTest.php
Normal file
238
tests/Ldap/LdapAuthenticationProviderTest.php
Normal file
@@ -0,0 +1,238 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Ldap;
|
||||
|
||||
use App\Configuration\LdapConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Ldap\LdapAuthenticationProvider;
|
||||
use App\Ldap\LdapManager;
|
||||
use App\Ldap\LdapUserProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\UserChecker;
|
||||
|
||||
/**
|
||||
* @covers \App\Ldap\LdapAuthenticationProvider
|
||||
*/
|
||||
class LdapAuthenticationProviderTest extends TestCase
|
||||
{
|
||||
public function testDeactivatedSupports()
|
||||
{
|
||||
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->getMock();
|
||||
$config = new LdapConfiguration(['active' => false]);
|
||||
$userProvider = new LdapUserProvider($manager, $config);
|
||||
$providerKey = 'secured_area';
|
||||
$userChecker = new UserChecker();
|
||||
|
||||
$token = new UsernamePasswordToken('foo', 'bar', $providerKey);
|
||||
|
||||
$sut = new LdapAuthenticationProvider($userChecker, $providerKey, $userProvider, $manager, $config, false);
|
||||
$result = $sut->supports($token);
|
||||
self::assertFalse($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Security\Core\Exception\AuthenticationException
|
||||
* @expectedExceptionMessage The token is not supported by this authentication provider.
|
||||
*/
|
||||
public function testDeactivatedAuthenticate()
|
||||
{
|
||||
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->getMock();
|
||||
$config = new LdapConfiguration(['active' => false]);
|
||||
$userProvider = new LdapUserProvider($manager, $config);
|
||||
$providerKey = 'secured_area';
|
||||
$userChecker = new UserChecker();
|
||||
|
||||
$token = new UsernamePasswordToken('foo', 'bar', $providerKey);
|
||||
|
||||
$sut = new LdapAuthenticationProvider($userChecker, $providerKey, $userProvider, $manager, $config, false);
|
||||
$sut->authenticate($token);
|
||||
}
|
||||
|
||||
public function testSupports()
|
||||
{
|
||||
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->getMock();
|
||||
$config = new LdapConfiguration(['active' => true]);
|
||||
$userProvider = new LdapUserProvider($manager, $config);
|
||||
$providerKey = 'secured_area';
|
||||
$userChecker = new UserChecker();
|
||||
|
||||
$token = new UsernamePasswordToken('foo', 'bar', $providerKey);
|
||||
|
||||
$sut = new LdapAuthenticationProvider($userChecker, $providerKey, $userProvider, $manager, $config, false);
|
||||
$result = $sut->supports($token);
|
||||
self::assertTrue($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Security\Core\Exception\BadCredentialsException
|
||||
* @expectedExceptionMessage The password in the token is empty. Check `erase_credentials` in your `security.yaml`
|
||||
*/
|
||||
public function testAuthenticateWithTokenUserButEnptyPasswordThrowsException()
|
||||
{
|
||||
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->getMock();
|
||||
$config = new LdapConfiguration(['active' => true]);
|
||||
$userProvider = new LdapUserProvider($manager, $config);
|
||||
$providerKey = 'secured_area';
|
||||
$userChecker = new UserChecker();
|
||||
|
||||
$user = (new User())->setUsername('foo')->setEnabled(true);
|
||||
$token = new UsernamePasswordToken($user, '', $providerKey);
|
||||
|
||||
$sut = new LdapAuthenticationProvider($userChecker, $providerKey, $userProvider, $manager, $config, false);
|
||||
$actual = $sut->authenticate($token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Security\Core\Exception\BadCredentialsException
|
||||
* @expectedExceptionMessage The presented password cannot be empty.
|
||||
*/
|
||||
public function testAuthenticateWithUsernameReturnsUser()
|
||||
{
|
||||
$user = (new User())->setUsername('foo')->setEnabled(true);
|
||||
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->getMock();
|
||||
$config = new LdapConfiguration(['active' => true]);
|
||||
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->setMethods(['loadUserByUsername'])->getMock();
|
||||
$userProvider->expects($this->once())->method('loadUserByUsername')->willReturn($user);
|
||||
$providerKey = 'secured_area';
|
||||
$userChecker = new UserChecker();
|
||||
|
||||
$token = new UsernamePasswordToken('foo', '', $providerKey);
|
||||
|
||||
$sut = new LdapAuthenticationProvider($userChecker, $providerKey, $userProvider, $manager, $config, false);
|
||||
$actual = $sut->authenticate($token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Security\Core\Exception\BadCredentialsException
|
||||
* @expectedExceptionMessage The presented password is invalid.
|
||||
*/
|
||||
public function testAuthenticateWithUsernameThrowsExceptionOnFailedBind()
|
||||
{
|
||||
$user = (new User())->setUsername('foo')->setEnabled(true);
|
||||
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->setMethods(['bind'])->getMock();
|
||||
$manager->expects($this->once())->method('bind')->willReturn(false);
|
||||
$config = new LdapConfiguration(['active' => true]);
|
||||
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->setMethods(['loadUserByUsername'])->getMock();
|
||||
$userProvider->expects($this->once())->method('loadUserByUsername')->willReturn($user);
|
||||
$providerKey = 'secured_area';
|
||||
$userChecker = new UserChecker();
|
||||
|
||||
$token = new UsernamePasswordToken('foo', 'sdfsdf', $providerKey);
|
||||
|
||||
$sut = new LdapAuthenticationProvider($userChecker, $providerKey, $userProvider, $manager, $config, false);
|
||||
$actual = $sut->authenticate($token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Security\Core\Exception\BadCredentialsException
|
||||
* @expectedExceptionMessage The credentials were changed from another session.
|
||||
*/
|
||||
public function testAuthenticateWithUserThrowsExceptionOnFailedBind()
|
||||
{
|
||||
$user = (new User())->setUsername('foo')->setEnabled(true);
|
||||
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->setMethods(['bind'])->getMock();
|
||||
$manager->expects($this->once())->method('bind')->willReturn(false);
|
||||
$config = new LdapConfiguration(['active' => true]);
|
||||
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->setMethods(['loadUserByUsername'])->getMock();
|
||||
$userProvider->expects($this->never())->method('loadUserByUsername');
|
||||
$providerKey = 'secured_area';
|
||||
$userChecker = new UserChecker();
|
||||
|
||||
$token = new UsernamePasswordToken($user, 'sdfsdf', $providerKey);
|
||||
|
||||
$sut = new LdapAuthenticationProvider($userChecker, $providerKey, $userProvider, $manager, $config, false);
|
||||
$actual = $sut->authenticate($token);
|
||||
}
|
||||
|
||||
public function testAuthenticateWithUsernameReturnsUserAndBinds()
|
||||
{
|
||||
$user = (new User())->setUsername('foo')->setEnabled(true);
|
||||
$user->setPreferenceValue('ldap.dn', 'blub');
|
||||
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->setMethods(['bind', 'updateUser'])->getMock();
|
||||
$manager->expects($this->once())->method('bind')->willReturn(true);
|
||||
$manager->expects($this->once())->method('updateUser')->willReturnCallback(function ($updateUser) use ($user) {
|
||||
self::assertSame($updateUser, $user);
|
||||
});
|
||||
$config = new LdapConfiguration(['active' => true]);
|
||||
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->setMethods(['loadUserByUsername'])->getMock();
|
||||
$userProvider->expects($this->once())->method('loadUserByUsername')->willReturn($user);
|
||||
$providerKey = 'secured_area';
|
||||
$userChecker = new UserChecker();
|
||||
|
||||
$token = new UsernamePasswordToken('foo', 'test', $providerKey);
|
||||
|
||||
$sut = new LdapAuthenticationProvider($userChecker, $providerKey, $userProvider, $manager, $config, false);
|
||||
$token = $sut->authenticate($token);
|
||||
self::assertSame($token->getUser(), $user);
|
||||
}
|
||||
|
||||
public function testAuthenticateWithUserReturnsUserAndBinds()
|
||||
{
|
||||
$user = (new User())->setUsername('foo')->setEnabled(true);
|
||||
$user->setPreferenceValue('ldap.dn', 'blub');
|
||||
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->setMethods(['bind', 'updateUser'])->getMock();
|
||||
$manager->expects($this->once())->method('bind')->willReturn(true);
|
||||
$manager->expects($this->once())->method('updateUser')->willReturnCallback(function ($updateUser) use ($user) {
|
||||
self::assertSame($updateUser, $user);
|
||||
});
|
||||
$config = new LdapConfiguration(['active' => true]);
|
||||
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->setMethods(['loadUserByUsername'])->getMock();
|
||||
$userProvider->expects($this->never())->method('loadUserByUsername');
|
||||
$providerKey = 'secured_area';
|
||||
$userChecker = new UserChecker();
|
||||
|
||||
$token = new UsernamePasswordToken($user, 'test', $providerKey);
|
||||
|
||||
$sut = new LdapAuthenticationProvider($userChecker, $providerKey, $userProvider, $manager, $config, false);
|
||||
$token = $sut->authenticate($token);
|
||||
self::assertSame($token->getUser(), $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Security\Core\Exception\UsernameNotFoundException
|
||||
* @expectedExceptionMessage blub foo bar
|
||||
*/
|
||||
public function testAuthenticateThrowsExceptionOnLdapNotFound()
|
||||
{
|
||||
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->getMock();
|
||||
$config = new LdapConfiguration(['active' => true]);
|
||||
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->setMethods(['loadUserByUsername'])->getMock();
|
||||
$userProvider->expects($this->once())->method('loadUserByUsername')->willThrowException(new UsernameNotFoundException('blub foo bar'));
|
||||
$providerKey = 'secured_area';
|
||||
$userChecker = new UserChecker();
|
||||
|
||||
$token = new UsernamePasswordToken('foo', 'test', $providerKey);
|
||||
|
||||
$sut = new LdapAuthenticationProvider($userChecker, $providerKey, $userProvider, $manager, $config, false);
|
||||
$sut->authenticate($token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Security\Core\Exception\AuthenticationServiceException
|
||||
* @expectedExceptionMessage server away
|
||||
* @expectedExceptionCode 1234
|
||||
*/
|
||||
public function testAuthenticateThrowsExceptionOnLdapDown()
|
||||
{
|
||||
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->getMock();
|
||||
$config = new LdapConfiguration(['active' => true]);
|
||||
$userProvider = $this->getMockBuilder(LdapUserProvider::class)->disableOriginalConstructor()->setMethods(['loadUserByUsername'])->getMock();
|
||||
$userProvider->expects($this->once())->method('loadUserByUsername')->willThrowException(new \Exception('server away', 1234));
|
||||
$providerKey = 'secured_area';
|
||||
$userChecker = new UserChecker();
|
||||
|
||||
$token = new UsernamePasswordToken('foo', 'test', $providerKey);
|
||||
|
||||
$sut = new LdapAuthenticationProvider($userChecker, $providerKey, $userProvider, $manager, $config, false);
|
||||
$sut->authenticate($token);
|
||||
}
|
||||
}
|
||||
28
tests/Ldap/LdapDriverExceptionTest.php
Normal file
28
tests/Ldap/LdapDriverExceptionTest.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Ldap;
|
||||
|
||||
use App\Ldap\LdapDriverException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Ldap\LdapDriverException
|
||||
*/
|
||||
class LdapDriverExceptionTest extends TestCase
|
||||
{
|
||||
public function testConstruct()
|
||||
{
|
||||
$sut = new LdapDriverException('Whooops');
|
||||
|
||||
self::assertInstanceOf(\Exception::class, $sut);
|
||||
|
||||
self::assertEquals('Whooops', $sut->getMessage());
|
||||
}
|
||||
}
|
||||
71
tests/Ldap/LdapDriverTest.php
Normal file
71
tests/Ldap/LdapDriverTest.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Ldap;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Ldap\LdapDriver;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Zend\Ldap\Exception\LdapException;
|
||||
use Zend\Ldap\Ldap;
|
||||
|
||||
/**
|
||||
* @covers \App\Ldap\LdapDriver
|
||||
*/
|
||||
class LdapDriverTest extends TestCase
|
||||
{
|
||||
public function testBindSuccess()
|
||||
{
|
||||
$zendLdap = $this->getMockBuilder(Ldap::class)->disableOriginalConstructor()->setMethods(['bind'])->getMock();
|
||||
$zendLdap->expects($this->once())->method('bind')->willReturnSelf();
|
||||
|
||||
$user = new User();
|
||||
$sut = new LdapDriver($zendLdap);
|
||||
$result = $sut->bind($user, 'test123');
|
||||
self::assertTrue($result);
|
||||
}
|
||||
|
||||
public function testBindException()
|
||||
{
|
||||
$zendLdap = $this->getMockBuilder(Ldap::class)->disableOriginalConstructor()->setMethods(['bind'])->getMock();
|
||||
$zendLdap->expects($this->once())->method('bind')->willThrowException(new LdapException());
|
||||
|
||||
$user = new User();
|
||||
$sut = new LdapDriver($zendLdap);
|
||||
$result = $sut->bind($user, 'test123');
|
||||
self::assertFalse($result);
|
||||
}
|
||||
|
||||
public function testSearchSuccess()
|
||||
{
|
||||
$zendLdap = $this->getMockBuilder(Ldap::class)->disableOriginalConstructor()->setMethods(['bind', 'searchEntries'])->getMock();
|
||||
$zendLdap->expects($this->once())->method('bind');
|
||||
$zendLdap->expects($this->once())->method('searchEntries')->willReturn([1, 2, 3]);
|
||||
|
||||
$sut = new LdapDriver($zendLdap);
|
||||
$result = $sut->search('', '', []);
|
||||
self::assertEquals(['count' => 3, 1, 2, 3], $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \App\Ldap\LdapDriverException
|
||||
* @expectedExceptionMessage An error occurred with the search operation.
|
||||
*/
|
||||
public function testSearchException()
|
||||
{
|
||||
$zendLdap = $this->getMockBuilder(Ldap::class)->disableOriginalConstructor()->setMethods(['bind', 'searchEntries'])->getMock();
|
||||
$zendLdap->expects($this->once())->method('bind');
|
||||
$zendLdap->expects($this->once())->method('searchEntries')->willThrowException(
|
||||
new LdapException($zendLdap, '', LdapException::LDAP_SERVER_DOWN)
|
||||
);
|
||||
|
||||
$sut = new LdapDriver($zendLdap);
|
||||
$sut->search('', '', []);
|
||||
}
|
||||
}
|
||||
454
tests/Ldap/LdapManagerTest.php
Normal file
454
tests/Ldap/LdapManagerTest.php
Normal file
@@ -0,0 +1,454 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Ldap;
|
||||
|
||||
use App\Configuration\LdapConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Ldap\LdapDriver;
|
||||
use App\Ldap\LdapManager;
|
||||
use App\Ldap\LdapUserHydrator;
|
||||
use App\Security\RoleService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Ldap\LdapManager
|
||||
*/
|
||||
class LdapManagerTest extends TestCase
|
||||
{
|
||||
protected function getLdapManager(LdapDriver $driver, $roleConfig = null)
|
||||
{
|
||||
if (null === $roleConfig) {
|
||||
$roleConfig = [
|
||||
'baseDn' => 'ou=groups, dc=kimai, dc=org',
|
||||
'nameAttribute' => 'cn',
|
||||
'userDnAttribute' => 'member',
|
||||
'groups' => [
|
||||
['ldap_value' => 'group1', 'role' => 'ROLE_TEAMLEAD'],
|
||||
['ldap_value' => 'group2', 'role' => 'ROLE_ADMIN'],
|
||||
['ldap_value' => 'group3', 'role' => 'ROLE_CUSTOMER'], // not existing!
|
||||
['ldap_value' => 'group4', 'role' => 'ROLE_SUPER_ADMIN'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$config = new LdapConfiguration([
|
||||
'user' => [
|
||||
'attributes' => [],
|
||||
'filter' => '(&(objectClass=inetOrgPerson))',
|
||||
'usernameAttribute' => 'uid',
|
||||
'baseDn' => 'ou=users, dc=kimai, dc=org',
|
||||
],
|
||||
'role' => $roleConfig,
|
||||
]);
|
||||
|
||||
$hydrator = new LdapUserHydrator($config, new RoleService([
|
||||
'ROLE_TEAMLEAD' => ['ROLE_USER'],
|
||||
'ROLE_ADMIN' => ['ROLE_TEAMLEAD'],
|
||||
'ROLE_SUPER_ADMIN' => ['ROLE_ADMIN']
|
||||
]));
|
||||
|
||||
return new LdapManager($driver, $hydrator, $config);
|
||||
}
|
||||
|
||||
public function testFindUserByUsernameOnZeroResults()
|
||||
{
|
||||
$expected = [
|
||||
'count' => 0
|
||||
];
|
||||
|
||||
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
|
||||
$driver->expects($this->once())->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
|
||||
self::assertEquals('ou=users, dc=kimai, dc=org', $baseDn);
|
||||
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=foo))', $filter);
|
||||
|
||||
return $expected;
|
||||
});
|
||||
|
||||
$sut = $this->getLdapManager($driver);
|
||||
$actual = $sut->findUserByUsername('foo');
|
||||
self::assertNull($actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \App\Ldap\LdapDriverException
|
||||
* @expectedExceptionMessage This search must only return a single user
|
||||
*/
|
||||
public function testFindUserByUsernameOnMultiResults()
|
||||
{
|
||||
$expected = [
|
||||
'count' => 3
|
||||
];
|
||||
|
||||
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
|
||||
$driver->expects($this->once())->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
|
||||
self::assertEquals('ou=users, dc=kimai, dc=org', $baseDn);
|
||||
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=foo))', $filter);
|
||||
|
||||
return $expected;
|
||||
});
|
||||
|
||||
$sut = $this->getLdapManager($driver);
|
||||
$sut->findUserByUsername('foo');
|
||||
}
|
||||
|
||||
public function testFindUserByUsernameOnValidResult()
|
||||
{
|
||||
$expected = [
|
||||
0 => ['dn' => 'foo'],
|
||||
'count' => 1,
|
||||
];
|
||||
|
||||
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
|
||||
$driver->expects($this->once())->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
|
||||
self::assertEquals('ou=users, dc=kimai, dc=org', $baseDn);
|
||||
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=foo))', $filter);
|
||||
|
||||
return $expected;
|
||||
});
|
||||
|
||||
$sut = $this->getLdapManager($driver);
|
||||
$actual = $sut->findUserByUsername('foo');
|
||||
self::assertInstanceOf(User::class, $actual);
|
||||
}
|
||||
|
||||
public function testFindUserByOnZeroResults()
|
||||
{
|
||||
$expected = [
|
||||
'count' => 0
|
||||
];
|
||||
|
||||
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
|
||||
$driver->expects($this->once())->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
|
||||
self::assertEquals('ou=users, dc=kimai, dc=org', $baseDn);
|
||||
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=foo))', $filter);
|
||||
|
||||
return $expected;
|
||||
});
|
||||
|
||||
$sut = $this->getLdapManager($driver);
|
||||
$actual = $sut->findUserBy(['uid' => 'foo']);
|
||||
self::assertNull($actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \App\Ldap\LdapDriverException
|
||||
* @expectedExceptionMessage This search must only return a single user
|
||||
*/
|
||||
public function testFindUserByOnMultiResults()
|
||||
{
|
||||
$expected = [
|
||||
'count' => 3
|
||||
];
|
||||
|
||||
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
|
||||
$driver->expects($this->once())->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
|
||||
self::assertEquals('ou=users, dc=kimai, dc=org', $baseDn);
|
||||
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=foo))', $filter);
|
||||
|
||||
return $expected;
|
||||
});
|
||||
|
||||
$sut = $this->getLdapManager($driver);
|
||||
$sut->findUserBy(['uid' => 'foo']);
|
||||
}
|
||||
|
||||
public function testFindUserByOnValidResult()
|
||||
{
|
||||
$expected = [
|
||||
0 => ['dn' => 'foo'],
|
||||
'count' => 1,
|
||||
];
|
||||
|
||||
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
|
||||
$driver->expects($this->once())->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
|
||||
self::assertEquals('ou=users, dc=kimai, dc=org', $baseDn);
|
||||
self::assertEquals('(&(&(objectClass=inetOrgPerson))(träl=alß#\\\aa=XY\5cZ0)(test=fu=n))', $filter);
|
||||
|
||||
return $expected;
|
||||
});
|
||||
|
||||
$sut = $this->getLdapManager($driver);
|
||||
$actual = $sut->findUserBy(['träl=alß#\\\aa' => 'XY\Z0', 'test' => 'fu=n']);
|
||||
self::assertInstanceOf(User::class, $actual);
|
||||
}
|
||||
|
||||
public function testBind()
|
||||
{
|
||||
$user = (new User())->setUsername('foobar');
|
||||
|
||||
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['bind'])->getMock();
|
||||
$driver->expects($this->once())->method('bind')->willReturnCallback(function ($bindUser, $password) use ($user) {
|
||||
self::assertSame($user, $bindUser);
|
||||
self::assertEquals('a-very-secret-secret', $password);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
$sut = $this->getLdapManager($driver);
|
||||
$actual = $sut->bind($user, 'a-very-secret-secret');
|
||||
self::assertTrue($actual);
|
||||
}
|
||||
|
||||
public function testUpdateUserOnZeroResults()
|
||||
{
|
||||
$user = (new User())->setUsername('foobar');
|
||||
$user->setPreferenceValue('ldap.dn', 'fooooooooooo');
|
||||
$expected = [
|
||||
[
|
||||
0 => ['dn' => 'blub'],
|
||||
'count' => 1,
|
||||
],
|
||||
[
|
||||
'count' => 0,
|
||||
],
|
||||
];
|
||||
|
||||
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
|
||||
$driver->expects($this->exactly(2))->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
|
||||
if ($baseDn === 'ou=users, dc=kimai, dc=org') {
|
||||
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=foobar))', $filter);
|
||||
|
||||
return $expected[0];
|
||||
} elseif ($baseDn === 'blub') {
|
||||
self::assertEquals('(objectClass=*)', $filter);
|
||||
|
||||
return $expected[1];
|
||||
}
|
||||
$this->fail(sprintf('Unexpected search with baseDn %s', $baseDn));
|
||||
});
|
||||
|
||||
$sut = $this->getLdapManager($driver);
|
||||
|
||||
$userOrig = clone $user;
|
||||
$sut->updateUser($user);
|
||||
self::assertEquals($userOrig, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \App\Ldap\LdapDriverException
|
||||
* @expectedExceptionMessage This search must only return a single user
|
||||
*/
|
||||
public function testUpdateUserOnMultiResults()
|
||||
{
|
||||
$user = (new User())->setUsername('foobar');
|
||||
$user->setPreferenceValue('ldap.dn', 'xxxxxxx');
|
||||
|
||||
$expected = [
|
||||
[
|
||||
0 => ['dn' => 'blub'],
|
||||
'count' => 1,
|
||||
],
|
||||
[
|
||||
'count' => 3,
|
||||
],
|
||||
];
|
||||
|
||||
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
|
||||
$driver->expects($this->exactly(2))->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
|
||||
if ($baseDn === 'ou=users, dc=kimai, dc=org') {
|
||||
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=foobar))', $filter);
|
||||
|
||||
return $expected[0];
|
||||
} elseif ($baseDn === 'blub') {
|
||||
self::assertEquals('(objectClass=*)', $filter);
|
||||
|
||||
return $expected[1];
|
||||
}
|
||||
|
||||
$this->fail(sprintf('Unexpected search with baseDn %s', $baseDn));
|
||||
});
|
||||
|
||||
$sut = $this->getLdapManager($driver);
|
||||
$sut->updateUser($user);
|
||||
}
|
||||
|
||||
public function testUpdateUserOnValidResultWithEmptyRoleBaseDn()
|
||||
{
|
||||
$user = (new User())->setUsername('foobar');
|
||||
$user->setPreferenceValue('ldap.dn', 'sssssss');
|
||||
|
||||
$expected = [
|
||||
[
|
||||
0 => ['dn' => 'blub'],
|
||||
'count' => 1,
|
||||
],
|
||||
[
|
||||
0 => ['dn' => 'blub-updated'],
|
||||
'count' => 1,
|
||||
],
|
||||
];
|
||||
|
||||
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
|
||||
$driver->expects($this->exactly(2))->method('search')->willReturnCallback(function ($baseDn, $filter) use ($expected) {
|
||||
if ($baseDn === 'ou=users, dc=kimai, dc=org') {
|
||||
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=foobar))', $filter);
|
||||
|
||||
return $expected[0];
|
||||
} elseif ($baseDn === 'blub') {
|
||||
self::assertEquals('(objectClass=*)', $filter);
|
||||
|
||||
return $expected[1];
|
||||
}
|
||||
|
||||
$this->fail(sprintf('Unexpected search with baseDn %s', $baseDn));
|
||||
});
|
||||
|
||||
$sut = $this->getLdapManager($driver, [
|
||||
'baseDn' => null,
|
||||
'nameAttribute' => 'cn',
|
||||
'userDnAttribute' => 'member',
|
||||
'groups' => [
|
||||
['ldap_value' => 'group1', 'role' => 'ROLE_TEAMLEAD'],
|
||||
['ldap_value' => 'group2', 'role' => 'ROLE_ADMIN'],
|
||||
['ldap_value' => 'group3', 'role' => 'ROLE_CUSTOMER'], // not existing!
|
||||
['ldap_value' => 'group4', 'role' => 'ROLE_SUPER_ADMIN'],
|
||||
],
|
||||
]);
|
||||
|
||||
$userOrig = clone $user;
|
||||
$sut->updateUser($user);
|
||||
self::assertEquals($userOrig->setEmail('foobar'), $user);
|
||||
self::assertEquals($user->getPreferenceValue('ldap.dn'), 'blub-updated');
|
||||
}
|
||||
|
||||
public function getValidConfigsTestData()
|
||||
{
|
||||
return [
|
||||
[
|
||||
[
|
||||
0 => [
|
||||
'dn' => 'blub',
|
||||
'uid' => ['Karl-Heinz'],
|
||||
// just some rubbish data
|
||||
'blub' => ['dfsdfsdf'],
|
||||
'foo' => ['count' => 1, 'bar'],
|
||||
'bar' => ['count' => 1, 'foo', 'xxx'],
|
||||
'xxxxxxxx' => ['https://www.example.com'],
|
||||
'blub1' => ['dfsdfsdf'],
|
||||
],
|
||||
'count' => 1,
|
||||
],
|
||||
[
|
||||
'baseDn' => 'ou=groups, dc=kimai, dc=org',
|
||||
'nameAttribute' => 'cn',
|
||||
'usernameAttribute' => 'cn', // test that "cn" is not set and fallback to "dn" happens
|
||||
'userDnAttribute' => 'member',
|
||||
'groups' => [
|
||||
['ldap_value' => 'group1', 'role' => 'ROLE_TEAMLEAD'],
|
||||
['ldap_value' => 'group2', 'role' => 'ROLE_ADMIN'],
|
||||
['ldap_value' => 'group3', 'role' => 'ROLE_CUSTOMER'], // not existing!
|
||||
['ldap_value' => 'group4', 'role' => 'ROLE_SUPER_ADMIN'],
|
||||
],
|
||||
],
|
||||
'(&(member=blub))'
|
||||
],
|
||||
[
|
||||
[
|
||||
0 => [
|
||||
'dn' => 'blub',
|
||||
'uid' => ['Karl-Heinz'],
|
||||
// just some rubbish data
|
||||
'blub' => ['dfsdfsdf'],
|
||||
'foo' => ['count' => 1, 'bar'],
|
||||
'bar' => ['count' => 1, 'foo', 'xxx'],
|
||||
'xxxxxxxx' => ['https://www.example.com'],
|
||||
'blub1' => ['dfsdfsdf'],
|
||||
],
|
||||
'count' => 1,
|
||||
],
|
||||
[
|
||||
'baseDn' => 'ou=groups, dc=kimai, dc=org',
|
||||
'nameAttribute' => 'cn',
|
||||
'usernameAttribute' => 'blub1',
|
||||
'userDnAttribute' => 'memberuid',
|
||||
'groups' => [
|
||||
['ldap_value' => 'group1', 'role' => 'ROLE_TEAMLEAD'],
|
||||
['ldap_value' => 'group2', 'role' => 'ROLE_ADMIN'],
|
||||
['ldap_value' => 'group3', 'role' => 'ROLE_CUSTOMER'], // not existing!
|
||||
['ldap_value' => 'group4', 'role' => 'ROLE_SUPER_ADMIN'],
|
||||
],
|
||||
],
|
||||
'(&(memberuid=dfsdfsdf))'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getValidConfigsTestData
|
||||
*/
|
||||
public function testUpdateUserOnValidResultWithRolesResult(array $expectedUsers, array $groupConfig, string $expectedGroupQuery)
|
||||
{
|
||||
$expected = [
|
||||
0 => ['dn' => 'blub'],
|
||||
'count' => 1,
|
||||
];
|
||||
|
||||
$expectedGroups = [
|
||||
// ROLE_TEAMLEAD
|
||||
0 => [
|
||||
'cn' => [0 => 'group1'],
|
||||
'member' => [0 => 'uid=user1,ou=users,dc=kimai,dc=org', 1 => 'uid=user2,ou=users,dc=kimai,dc=org'],
|
||||
],
|
||||
// ROLE_ADMIN
|
||||
1 => [
|
||||
'cn' => [0 => 'admin'],
|
||||
'member' => [0 => 'uid=user2,ou=users,dc=kimai,dc=org', 1 => 'uid=user3,ou=users,dc=kimai,dc=org'],
|
||||
],
|
||||
// will be ignored: unknown group
|
||||
2 => [
|
||||
'cn' => [0 => 'kimai_admin'],
|
||||
'member' => [0 => 'uid=user2,ou=users,dc=kimai,dc=org', 1 => 'uid=user3,ou=users,dc=kimai,dc=org'],
|
||||
],
|
||||
// will be ignored: unknown group
|
||||
3 => [
|
||||
'cn' => [0 => 'group3'],
|
||||
'member' => [0 => 'uid=user2,ou=users,dc=kimai,dc=org', 1 => 'uid=user3,ou=users,dc=kimai,dc=org'],
|
||||
],
|
||||
// will be ignored: the counter below does not announce this group!
|
||||
4 => [
|
||||
'cn' => [0 => 'group4'],
|
||||
'member' => [0 => 'uid=user2,ou=users,dc=kimai,dc=org', 1 => 'uid=user3,ou=users,dc=kimai,dc=org'],
|
||||
],
|
||||
'count' => 4
|
||||
];
|
||||
|
||||
$driver = $this->getMockBuilder(LdapDriver::class)->disableOriginalConstructor()->setMethods(['search'])->getMock();
|
||||
$driver->expects($this->exactly(3))->method('search')->willReturnCallback(function ($baseDn, $filter, $attributes) use ($expectedUsers, $expectedGroups, $expectedGroupQuery, $expected) {
|
||||
if ($baseDn === 'ou=users, dc=kimai, dc=org') {
|
||||
self::assertEquals('(&(&(objectClass=inetOrgPerson))(uid=Karl-Heinz))', $filter);
|
||||
|
||||
return $expected;
|
||||
} elseif ($baseDn === 'blub') {
|
||||
// user attributes search
|
||||
self::assertEquals('(objectClass=*)', $filter);
|
||||
|
||||
return $expectedUsers;
|
||||
} elseif ($baseDn === 'ou=groups, dc=kimai, dc=org') {
|
||||
// roles search
|
||||
self::assertEquals($expectedGroupQuery, $filter);
|
||||
self::assertEquals([0 => 'cn'], $attributes);
|
||||
|
||||
return $expectedGroups;
|
||||
}
|
||||
$this->fail(sprintf('Unexpected search with baseDn %s', $baseDn));
|
||||
});
|
||||
|
||||
$sut = $this->getLdapManager($driver, $groupConfig);
|
||||
|
||||
$user = (new User())->setUsername('Karl-Heinz');
|
||||
$user->setPreferenceValue('ldap.dn', 'blub');
|
||||
$userOrig = clone $user;
|
||||
$userOrig->setEmail('Karl-Heinz')->setRoles(['ROLE_TEAMLEAD', 'ROLE_ADMIN']);
|
||||
|
||||
$sut->updateUser($user);
|
||||
self::assertEquals($userOrig, $user);
|
||||
self::assertEquals(['ROLE_TEAMLEAD', 'ROLE_ADMIN', 'ROLE_USER'], $user->getRoles());
|
||||
}
|
||||
}
|
||||
189
tests/Ldap/LdapUserHydratorTest.php
Normal file
189
tests/Ldap/LdapUserHydratorTest.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Ldap;
|
||||
|
||||
use App\Configuration\LdapConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Ldap\LdapUserHydrator;
|
||||
use App\Security\RoleService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Ldap\LdapUserHydrator
|
||||
*/
|
||||
class LdapUserHydratorTest extends TestCase
|
||||
{
|
||||
public function testEmptyHydrate()
|
||||
{
|
||||
$config = new LdapConfiguration([
|
||||
'active' => false,
|
||||
'connection' => [
|
||||
'host' => '1.1.1.1'
|
||||
],
|
||||
'user' => [
|
||||
'usernameAttribute' => 'foo',
|
||||
'attributes' => []
|
||||
],
|
||||
'role' => [],
|
||||
]);
|
||||
|
||||
$sut = new LdapUserHydrator($config, new RoleService([]));
|
||||
$user = $sut->hydrate(['dn' => 'blub']);
|
||||
self::assertInstanceOf(User::class, $user);
|
||||
self::assertEmpty($user->getUsername());
|
||||
self::assertEmpty($user->getEmail());
|
||||
}
|
||||
|
||||
public function testHydrate()
|
||||
{
|
||||
$config = new LdapConfiguration([
|
||||
'active' => false,
|
||||
'connection' => [
|
||||
'host' => '1.1.1.1'
|
||||
],
|
||||
'user' => [
|
||||
'usernameAttribute' => 'foo',
|
||||
'attributes' => [
|
||||
['ldap_attr' => 'uid', 'user_method' => 'setUsername'],
|
||||
['ldap_attr' => 'foo', 'user_method' => 'setAlias'],
|
||||
['ldap_attr' => 'bar', 'user_method' => 'setTitle'],
|
||||
['ldap_attr' => 'roles', 'user_method' => 'setRoles'],
|
||||
['ldap_attr' => 'xxxxxxxx', 'user_method' => 'setAvatar'],
|
||||
['ldap_attr' => 'blubXX', 'user_method' => 'setAvatar'],
|
||||
]
|
||||
],
|
||||
'role' => [],
|
||||
]);
|
||||
|
||||
$ldapEntry = [
|
||||
'uid' => ['Karl-Heinz'],
|
||||
'blub' => ['dfsdfsdf'],
|
||||
'foo' => ['count' => 1, 0 => 'bar'],
|
||||
'bar' => ['foo'],
|
||||
'roles' => ['count' => 2, 0 => 'ROLE_TEAMLEAD', 1 => 'ROLE_ADMIN'],
|
||||
'xxxxxxxx' => ['https://www.example.com'],
|
||||
'blub1' => ['dfsdfsdf'],
|
||||
'dn' => 'blub',
|
||||
];
|
||||
|
||||
$sut = new LdapUserHydrator($config, new RoleService([]));
|
||||
$user = $sut->hydrate($ldapEntry);
|
||||
|
||||
self::assertInstanceOf(User::class, $user);
|
||||
self::assertEquals('Karl-Heinz', $user->getUsername());
|
||||
self::assertEquals('bar', $user->getAlias());
|
||||
self::assertEquals('foo', $user->getTitle());
|
||||
self::assertEquals(['ROLE_TEAMLEAD', 'ROLE_ADMIN', 'ROLE_USER'], $user->getRoles());
|
||||
self::assertEquals('https://www.example.com', $user->getAvatar());
|
||||
self::assertEquals('Karl-Heinz', $user->getEmail());
|
||||
}
|
||||
|
||||
public function testHydrateUser()
|
||||
{
|
||||
$config = new LdapConfiguration([
|
||||
'active' => false,
|
||||
'connection' => [
|
||||
'host' => '1.1.1.1'
|
||||
],
|
||||
'user' => [
|
||||
'usernameAttribute' => 'foo',
|
||||
'attributes' => [
|
||||
['ldap_attr' => 'uid', 'user_method' => 'setUsername'],
|
||||
['ldap_attr' => 'foo', 'user_method' => 'setAlias'],
|
||||
['ldap_attr' => 'bar', 'user_method' => 'setTitle'],
|
||||
['ldap_attr' => 'xxxxxxxx', 'user_method' => 'setAvatar'],
|
||||
]
|
||||
],
|
||||
'role' => [],
|
||||
]);
|
||||
|
||||
$ldapEntry = [
|
||||
'uid' => ['Karl-Heinz'],
|
||||
'blub' => ['dfsdfsdf'],
|
||||
'foo' => ['bar'],
|
||||
'bar' => ['foo'],
|
||||
'xxxxxxxx' => ['https://www.example.com'],
|
||||
'blub1' => ['dfsdfsdf'],
|
||||
'dn' => 'blub',
|
||||
];
|
||||
|
||||
$sut = new LdapUserHydrator($config, new RoleService([]));
|
||||
$user = new User();
|
||||
$user->setPassword('foobar');
|
||||
$sut->hydrateUser($user, $ldapEntry);
|
||||
self::assertEquals('Karl-Heinz', $user->getUsername());
|
||||
self::assertEquals('bar', $user->getAlias());
|
||||
self::assertEquals('foo', $user->getTitle());
|
||||
self::assertEquals('https://www.example.com', $user->getAvatar());
|
||||
self::assertEquals('Karl-Heinz', $user->getEmail());
|
||||
|
||||
// make sure that the password was resetted in hydrate
|
||||
$pwdCheck = clone $user;
|
||||
$pwdCheck->setPassword('');
|
||||
self::assertEquals($pwdCheck, $user);
|
||||
}
|
||||
|
||||
public function testHydrateRoles()
|
||||
{
|
||||
$config = new LdapConfiguration([
|
||||
'user' => [
|
||||
'attributes' => []
|
||||
],
|
||||
'role' => [
|
||||
'nameAttribute' => 'cn',
|
||||
'userDnAttribute' => 'member',
|
||||
'groups' => [
|
||||
['ldap_value' => 'group1', 'role' => 'ROLE_TEAMLEAD'],
|
||||
['ldap_value' => 'group2', 'role' => 'ROLE_ADMIN'],
|
||||
['ldap_value' => 'group3', 'role' => 'ROLE_CUSTOMER'], // not existing!
|
||||
['ldap_value' => 'group4', 'role' => 'ROLE_SUPER_ADMIN'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$ldapGroups = [
|
||||
// ROLE_TEAMLEAD
|
||||
0 => [
|
||||
'cn' => [0 => 'group1'],
|
||||
'member' => [0 => 'uid=user1,ou=users,dc=kimai,dc=org', 1 => 'uid=user2,ou=users,dc=kimai,dc=org'],
|
||||
],
|
||||
// ROLE_ADMIN
|
||||
1 => [
|
||||
'cn' => [0 => 'admin'],
|
||||
'member' => [0 => 'uid=user2,ou=users,dc=kimai,dc=org', 1 => 'uid=user3,ou=users,dc=kimai,dc=org'],
|
||||
],
|
||||
// will be ignored: unknown group
|
||||
2 => [
|
||||
'cn' => [0 => 'kimai_admin'],
|
||||
'member' => [0 => 'uid=user2,ou=users,dc=kimai,dc=org', 1 => 'uid=user3,ou=users,dc=kimai,dc=org'],
|
||||
],
|
||||
// will be ignored: unknown group
|
||||
3 => [
|
||||
'cn' => [0 => 'group3'],
|
||||
'member' => [0 => 'uid=user2,ou=users,dc=kimai,dc=org', 1 => 'uid=user3,ou=users,dc=kimai,dc=org'],
|
||||
],
|
||||
// will be ignored: the counter below does not announce this group!
|
||||
4 => [
|
||||
'cn' => [0 => 'group4'],
|
||||
'member' => [0 => 'uid=user2,ou=users,dc=kimai,dc=org', 1 => 'uid=user3,ou=users,dc=kimai,dc=org'],
|
||||
],
|
||||
'count' => 4
|
||||
];
|
||||
|
||||
$sut = new LdapUserHydrator($config, new RoleService([
|
||||
'ROLE_TEAMLEAD' => ['ROLE_USER'],
|
||||
'ROLE_ADMIN' => ['ROLE_TEAMLEAD'],
|
||||
'ROLE_SUPER_ADMIN' => ['ROLE_ADMIN']
|
||||
]));
|
||||
$user = new User();
|
||||
$sut->hydrateRoles($user, $ldapGroups);
|
||||
self::assertEquals(['ROLE_TEAMLEAD', 'ROLE_ADMIN', 'ROLE_USER'], $user->getRoles());
|
||||
}
|
||||
}
|
||||
102
tests/Ldap/LdapUserProviderTest.php
Normal file
102
tests/Ldap/LdapUserProviderTest.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Ldap;
|
||||
|
||||
use App\Configuration\LdapConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Ldap\LdapManager;
|
||||
use App\Ldap\LdapUserProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Ldap\LdapUserProvider
|
||||
*/
|
||||
class LdapUserProviderTest extends TestCase
|
||||
{
|
||||
public function testDeactivatedSupportsClass()
|
||||
{
|
||||
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->getMock();
|
||||
$config = new LdapConfiguration(['active' => false]);
|
||||
|
||||
$sut = new LdapUserProvider($manager, $config);
|
||||
self::assertFalse($sut->supportsClass(User::class));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Security\Core\Exception\UsernameNotFoundException
|
||||
* @expectedExceptionMessage LDAP is deactivated, user "test" not searched
|
||||
*/
|
||||
public function testDeactivatedLoadUserByUsername()
|
||||
{
|
||||
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->getMock();
|
||||
$config = new LdapConfiguration(['active' => false]);
|
||||
|
||||
$sut = new LdapUserProvider($manager, $config);
|
||||
$sut->loadUserByUsername('test');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Security\Core\Exception\UnsupportedUserException
|
||||
* @expectedExceptionMessage Instances of "App\Entity\User" are not supported.
|
||||
*/
|
||||
public function testDeactivatedRefreshUser()
|
||||
{
|
||||
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->getMock();
|
||||
$config = new LdapConfiguration(['active' => false]);
|
||||
|
||||
$sut = new LdapUserProvider($manager, $config);
|
||||
$sut->refreshUser(new User());
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Security\Core\Exception\UsernameNotFoundException
|
||||
* @expectedExceptionMessage User "test" not found
|
||||
*/
|
||||
public function testLoadUserByUsernameReturnsNull()
|
||||
{
|
||||
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->setMethods(['findUserByUsername'])->getMock();
|
||||
$manager->expects($this->once())->method('findUserByUsername')->willReturn(null);
|
||||
$config = new LdapConfiguration(['active' => true]);
|
||||
|
||||
$sut = new LdapUserProvider($manager, $config);
|
||||
$sut->loadUserByUsername('test');
|
||||
}
|
||||
|
||||
public function testLoadUserByUsernameReturnsUser()
|
||||
{
|
||||
$user = new User();
|
||||
$user->setUsername('foobar');
|
||||
|
||||
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->setMethods(['findUserByUsername'])->getMock();
|
||||
$manager->expects($this->once())->method('findUserByUsername')->willReturn($user);
|
||||
$config = new LdapConfiguration(['active' => true]);
|
||||
|
||||
$sut = new LdapUserProvider($manager, $config);
|
||||
$actual = $sut->loadUserByUsername('test');
|
||||
self::assertInstanceOf(User::class, $actual);
|
||||
self::assertSame($user, $actual);
|
||||
}
|
||||
|
||||
public function testRefreshUserReturnsUser()
|
||||
{
|
||||
$user = new User();
|
||||
$user->setUsername('foobar');
|
||||
$user->setPreferenceValue('ldap.dn', 'sdfdsf');
|
||||
|
||||
$manager = $this->getMockBuilder(LdapManager::class)->disableOriginalConstructor()->setMethods(['updateUser'])->getMock();
|
||||
$config = new LdapConfiguration(['active' => true]);
|
||||
|
||||
$sut = new LdapUserProvider($manager, $config);
|
||||
$actual = $sut->refreshUser($user);
|
||||
|
||||
self::assertInstanceOf(User::class, $actual);
|
||||
self::assertSame($user, $actual);
|
||||
}
|
||||
}
|
||||
31
tests/Ldap/SanitizingExceptionTest.php
Normal file
31
tests/Ldap/SanitizingExceptionTest.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Ldap;
|
||||
|
||||
use App\Ldap\SanitizingException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Ldap\SanitizingException
|
||||
*/
|
||||
class SanitizingExceptionTest extends TestCase
|
||||
{
|
||||
public function testMessagesAreSanitized()
|
||||
{
|
||||
$ex = new \Exception('Could not find user foo with password bar in your LDAP');
|
||||
$sut = new SanitizingException($ex, 'bar');
|
||||
|
||||
self::assertInstanceOf(\Exception::class, $sut);
|
||||
|
||||
self::assertNotContains('bar', $sut->getMessage());
|
||||
self::assertNotContains('bar', (string) $sut);
|
||||
self::assertEquals('Could not find user foo with password **** in your LDAP', $sut->getMessage());
|
||||
}
|
||||
}
|
||||
48
tests/Ldap/ZendLdapTest.php
Normal file
48
tests/Ldap/ZendLdapTest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Ldap;
|
||||
|
||||
use App\Configuration\LdapConfiguration;
|
||||
use App\Ldap\ZendLdap;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Ldap\ZendLdap
|
||||
*/
|
||||
class ZendLdapTest extends TestCase
|
||||
{
|
||||
public function testConstructDeactivated()
|
||||
{
|
||||
$config = new LdapConfiguration([
|
||||
'active' => false,
|
||||
'connection' => [
|
||||
'host' => '1.1.1.1'
|
||||
]
|
||||
]);
|
||||
|
||||
$sut = new ZendLdap($config);
|
||||
$options = $sut->getOptions();
|
||||
self::assertNull($options['host']);
|
||||
}
|
||||
|
||||
public function testConstructActivatedPassesOptions()
|
||||
{
|
||||
$config = new LdapConfiguration([
|
||||
'active' => true,
|
||||
'connection' => [
|
||||
'host' => '1.1.1.1'
|
||||
]
|
||||
]);
|
||||
|
||||
$sut = new ZendLdap($config);
|
||||
$options = $sut->getOptions();
|
||||
self::assertEquals('1.1.1.1', $options['host']);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ abstract class AbstractRepositoryTest extends KernelTestCase
|
||||
use KernelTestTrait;
|
||||
|
||||
/**
|
||||
* @var EntityManager
|
||||
* @var EntityManager|null
|
||||
*/
|
||||
private $entityManager;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Tests\Repository;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use App\Repository\TagRepository;
|
||||
use App\Tests\DataFixtures\TagFixtures;
|
||||
|
||||
/**
|
||||
@@ -29,6 +30,7 @@ class TagRepositoryTest extends AbstractRepositoryTest
|
||||
public function testFindIds()
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
/** @var TagRepository $repository */
|
||||
$repository = $em->getRepository(Tag::class);
|
||||
|
||||
$result = $repository->findIdsByTagNameList('2018,Test');
|
||||
@@ -47,6 +49,7 @@ class TagRepositoryTest extends AbstractRepositoryTest
|
||||
public function testFindNoIds()
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
/** @var TagRepository $repository */
|
||||
$repository = $em->getRepository(Tag::class);
|
||||
|
||||
$result = $repository->findIdsByTagNameList('Simply');
|
||||
@@ -58,6 +61,7 @@ class TagRepositoryTest extends AbstractRepositoryTest
|
||||
public function testFindAllTagNames()
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
/** @var TagRepository $repository */
|
||||
$repository = $em->getRepository(Tag::class);
|
||||
|
||||
$result = $repository->findAllTagNames('2018');
|
||||
@@ -75,6 +79,7 @@ class TagRepositoryTest extends AbstractRepositoryTest
|
||||
public function testFindNoTagNames()
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
/** @var TagRepository $repository */
|
||||
$repository = $em->getRepository(Tag::class);
|
||||
|
||||
$result = $repository->findAllTagNames('Nothing');
|
||||
|
||||
@@ -14,9 +14,12 @@ use App\Entity\Project;
|
||||
use App\Entity\Tag;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\RepositoryException;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
@@ -29,6 +32,7 @@ class TimesheetRepositoryTest extends AbstractRepositoryTest
|
||||
public function testResultTypeForQueryState()
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
/** @var TimesheetRepository $repository */
|
||||
$repository = $em->getRepository(Timesheet::class);
|
||||
|
||||
$query = new TimesheetQuery();
|
||||
@@ -53,6 +57,7 @@ class TimesheetRepositoryTest extends AbstractRepositoryTest
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||
/** @var TimesheetRepository $repository */
|
||||
$repository = $em->getRepository(Timesheet::class);
|
||||
|
||||
$fixtures = new TimesheetFixtures();
|
||||
@@ -66,6 +71,7 @@ class TimesheetRepositoryTest extends AbstractRepositoryTest
|
||||
$query->setUser($user);
|
||||
$query->setState(TimesheetQuery::STATE_STOPPED);
|
||||
|
||||
/** @var array $entities */
|
||||
$entities = $repository->findByQuery($query);
|
||||
|
||||
$this->assertCount(1, $entities);
|
||||
@@ -81,6 +87,7 @@ class TimesheetRepositoryTest extends AbstractRepositoryTest
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||
/** @var TimesheetRepository $repository */
|
||||
$repository = $em->getRepository(Timesheet::class);
|
||||
|
||||
$fixtures = new TimesheetFixtures();
|
||||
@@ -100,12 +107,15 @@ class TimesheetRepositoryTest extends AbstractRepositoryTest
|
||||
public function testSave()
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
/** @var ActivityRepository $activityRepository */
|
||||
$activityRepository = $em->getRepository(Activity::class);
|
||||
$activity = $activityRepository->find(1);
|
||||
/** @var ProjectRepository $projectRepository */
|
||||
$projectRepository = $em->getRepository(Project::class);
|
||||
$project = $projectRepository->find(1);
|
||||
|
||||
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||
/** @var TimesheetRepository $repository */
|
||||
$repository = $em->getRepository(Timesheet::class);
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet
|
||||
@@ -124,12 +134,15 @@ class TimesheetRepositoryTest extends AbstractRepositoryTest
|
||||
public function testSaveWithTags()
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
/** @var ActivityRepository $activityRepository */
|
||||
$activityRepository = $em->getRepository(Activity::class);
|
||||
$activity = $activityRepository->find(1);
|
||||
/** @var ProjectRepository $projectRepository */
|
||||
$projectRepository = $em->getRepository(Project::class);
|
||||
$project = $projectRepository->find(1);
|
||||
|
||||
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||
/** @var TimesheetRepository $repository */
|
||||
$repository = $em->getRepository(Timesheet::class);
|
||||
$tagOne = new Tag();
|
||||
$tagOne->setName('Travel');
|
||||
|
||||
34
tests/Security/RoleServiceTest.php
Normal file
34
tests/Security/RoleServiceTest.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Security;
|
||||
|
||||
use App\Security\RoleService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Security\RoleService
|
||||
*/
|
||||
class RoleServiceTest extends TestCase
|
||||
{
|
||||
public function testGetAvailableNames()
|
||||
{
|
||||
$real = [
|
||||
'ROLE_TEAMLEAD' => [0 => 'ROLE_USER'],
|
||||
'ROLE_ADMIN' => [0 => 'ROLE_TEAMLEAD'],
|
||||
'ROLE_SUPER_ADMIN' => [0 => 'ROLE_ADMIN']
|
||||
];
|
||||
|
||||
$sut = new RoleService($real);
|
||||
|
||||
$expected = ['ROLE_TEAMLEAD', 'ROLE_USER', 'ROLE_ADMIN', 'ROLE_SUPER_ADMIN'];
|
||||
|
||||
self::assertEquals($expected, $sut->getAvailableNames());
|
||||
}
|
||||
}
|
||||
@@ -7,51 +7,57 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Model;
|
||||
namespace App\Tests\Security;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Security\UserChecker;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\User as SymfonyUser;
|
||||
|
||||
/**
|
||||
* @covers \App\Security\UserChecker
|
||||
*/
|
||||
class UserCheckerTest extends TestCase
|
||||
{
|
||||
public function testCheckPreAuth()
|
||||
public function testCheckPreAuthReturnsOnUnknownUserClass()
|
||||
{
|
||||
$sut = new UserChecker();
|
||||
$user = new User();
|
||||
|
||||
try {
|
||||
$sut->checkPreAuth($user);
|
||||
$sut->checkPreAuth(new SymfonyUser('sdf', null));
|
||||
} catch (\Exception $ex) {
|
||||
$this->fail('UserChecker should not throw exception in checkPreAuth()');
|
||||
$this->fail('UserChecker should not throw exception in checkPreAuth(), ' . $ex->getMessage());
|
||||
}
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function testCheckPostAuthReturnsOnUnknownUserClass()
|
||||
{
|
||||
$sut = new UserChecker();
|
||||
|
||||
try {
|
||||
$sut->checkPostAuth(new SymfonyUser('sdf', null));
|
||||
} catch (\Exception $ex) {
|
||||
$this->fail('UserChecker should not throw exception in checkPostAuth(), ' . $ex->getMessage());
|
||||
}
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Security\Core\Exception\LockedException
|
||||
* @expectedException \Symfony\Component\Security\Core\Exception\DisabledException
|
||||
* @expectedExceptionMessage User account is disabled.
|
||||
*/
|
||||
public function testDisabledCannotLogin()
|
||||
public function testDisabledCannotLoginInCheckPreAuth()
|
||||
{
|
||||
$sut = new UserChecker();
|
||||
$user = new User();
|
||||
$user->setEnabled(false);
|
||||
|
||||
$sut->checkPostAuth($user);
|
||||
(new UserChecker())->checkPreAuth((new User())->setEnabled(false));
|
||||
}
|
||||
|
||||
public function testCheckPostAuth()
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Security\Core\Exception\DisabledException
|
||||
* @expectedExceptionMessage User account is disabled.
|
||||
*/
|
||||
public function testDisabledCannotLoginInCheckPostAuth()
|
||||
{
|
||||
$sut = new UserChecker();
|
||||
|
||||
$mock = $this->getMockBuilder(UserInterface::class)->setMethods(['isEnabled'])->getMockForAbstractClass();
|
||||
$mock->expects($this->never())->method('isEnabled')->willReturn(false);
|
||||
|
||||
$sut->checkPostAuth($mock);
|
||||
$this->assertTrue(true);
|
||||
(new UserChecker())->checkPostAuth((new User())->setEnabled(false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Timesheet\Calculator;
|
||||
namespace App\Tests\Timesheet\Rounding;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\Rounding\CeilRounding;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Timesheet\Calculator;
|
||||
namespace App\Tests\Timesheet\Rounding;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\Rounding\ClosestRounding;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Timesheet\Calculator;
|
||||
namespace App\Tests\Timesheet\Rounding;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\Rounding\DefaultRounding;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Timesheet\Calculator;
|
||||
namespace App\Tests\Timesheet\Rounding;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\Rounding\FloorRounding;
|
||||
|
||||
@@ -242,7 +242,6 @@ class ExtensionsTest extends TestCase
|
||||
'timesheet.html#duration-format' => 'https://www.kimai.org/documentation/timesheet.html#duration-format',
|
||||
'invoice.html' => 'https://www.kimai.org/documentation/invoice.html',
|
||||
'' => 'https://www.kimai.org/documentation/',
|
||||
null => 'https://www.kimai.org/documentation/',
|
||||
];
|
||||
|
||||
$sut = $this->getSut($this->localeEn);
|
||||
|
||||
@@ -28,18 +28,18 @@ abstract class AbstractVoterTest extends TestCase
|
||||
$isAuthenticated = empty($user->getRoles());
|
||||
$accessManager = $this->getMockBuilder(AclDecisionManager::class)->disableOriginalConstructor()->getMock();
|
||||
$accessManager->method('isFullyAuthenticated')->willReturn($isAuthenticated);
|
||||
$accessManager->method('hasRole')->willReturnCallback(function ($role) use ($user) {
|
||||
return in_array($role, $user->getRoles());
|
||||
});
|
||||
|
||||
$class = new \ReflectionClass($voterClass);
|
||||
/** @var AbstractVoter $voter */
|
||||
$voter = $class->newInstance($accessManager, $this->getRolePermissionManager());
|
||||
self::assertInstanceOf(AbstractVoter::class, $voter);
|
||||
|
||||
return $class->newInstance($accessManager, $this->getRolePermissionManager());
|
||||
return $voter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @param $role
|
||||
* @param int $id
|
||||
* @param string $role
|
||||
* @return User
|
||||
*/
|
||||
protected function getUser($id, $role)
|
||||
|
||||
@@ -148,8 +148,8 @@ class TimesheetVoterTest extends AbstractVoterTest
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @param $role
|
||||
* @param int $id
|
||||
* @param string $role
|
||||
* @return User
|
||||
*/
|
||||
protected function getUser($id, $role)
|
||||
|
||||
10
tests/phpstan.neon
Normal file
10
tests/phpstan.neon
Normal file
@@ -0,0 +1,10 @@
|
||||
includes:
|
||||
- %rootDir%/../phpstan-symfony/extension.neon
|
||||
- %rootDir%/../phpstan-doctrine/extension.neon
|
||||
- %rootDir%/../phpstan-phpunit/extension.neon
|
||||
|
||||
parameters:
|
||||
ignoreErrors:
|
||||
- '#Access to an undefined property Faker\\Generator::\$stateAbbr.#'
|
||||
- '#Access to an undefined property Faker\\Generator::\$catchPhrase.#'
|
||||
- '#Access to an undefined property Faker\\Generator::\$bs.#'
|
||||
Reference in New Issue
Block a user