From c0b74d83a5fa58964f34304c1f969a263e06e144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Mon, 20 Feb 2023 22:10:24 +0100 Subject: [PATCH 01/25] Started to work on interfacing with keycloak --- composer.json | 1 + composer.lock | 160 +++++++++++++++++++- config/bundles.php | 1 + config/packages/hslavich_onelogin_saml.yaml | 57 +++++++ config/packages/security.yaml | 22 ++- config/routes/hslavich_saml.yaml | 2 + symfony.lock | 3 + templates/security/login.html.twig | 2 + 8 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 config/packages/hslavich_onelogin_saml.yaml create mode 100644 config/routes/hslavich_saml.yaml diff --git a/composer.json b/composer.json index 33ffde89..b515cc4a 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "florianv/swap": "^4.0", "florianv/swap-bundle": "dev-master", "gregwar/captcha-bundle": "^2.1.0", + "hslavich/oneloginsaml-bundle": "^2.10", "jbtronics/2fa-webauthn": "^1.0.0", "league/html-to-markdown": "^5.0.1", "liip/imagine-bundle": "^2.2", diff --git a/composer.lock b/composer.lock index e1c56d37..07998b71 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "3a1c7d677a101ae6256c68bf69919414", + "content-hash": "73b35aff40231c2fe1ebf72c1d098689", "packages": [ { "name": "beberlei/assert", @@ -2365,6 +2365,67 @@ }, "time": "2022-01-11T08:28:06+00:00" }, + { + "name": "hslavich/oneloginsaml-bundle", + "version": "v2.10.0", + "source": { + "type": "git", + "url": "https://github.com/hslavich/OneloginSamlBundle.git", + "reference": "aee3450bd36b750a2e61b4a0ca19a09ecab7a086" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hslavich/OneloginSamlBundle/zipball/aee3450bd36b750a2e61b4a0ca19a09ecab7a086", + "reference": "aee3450bd36b750a2e61b4a0ca19a09ecab7a086", + "shasum": "" + }, + "require": { + "onelogin/php-saml": "^3.0", + "symfony/dependency-injection": "^5.4", + "symfony/deprecation-contracts": "^2.1 | ^3", + "symfony/event-dispatcher-contracts": "^2.4", + "symfony/framework-bundle": "^5.4", + "symfony/security-bundle": "^5.4" + }, + "require-dev": { + "dms/phpunit-arraysubset-asserts": "^0.2.0", + "doctrine/orm": "~2.3", + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^9.0", + "symfony/event-dispatcher": "^5.4", + "symfony/phpunit-bridge": "^5.4" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Hslavich\\OneloginSamlBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "hslavich", + "email": "hernan.slavich@gmail.com" + } + ], + "description": "OneLogin SAML Bundle for Symfony", + "keywords": [ + "SSO", + "onelogin", + "saml" + ], + "support": { + "issues": "https://github.com/hslavich/OneloginSamlBundle/issues", + "source": "https://github.com/hslavich/OneloginSamlBundle/tree/v2.10.0" + }, + "time": "2022-11-23T17:12:47+00:00" + }, { "name": "imagine/imagine", "version": "1.3.3", @@ -3767,6 +3828,61 @@ }, "time": "2021-06-29T08:12:37+00:00" }, + { + "name": "onelogin/php-saml", + "version": "3.6.1", + "source": { + "type": "git", + "url": "https://github.com/onelogin/php-saml.git", + "reference": "a7328b11887660ad248ea10952dd67a5aa73ba3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/onelogin/php-saml/zipball/a7328b11887660ad248ea10952dd67a5aa73ba3b", + "reference": "a7328b11887660ad248ea10952dd67a5aa73ba3b", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "robrichards/xmlseclibs": ">=3.1.1" + }, + "require-dev": { + "pdepend/pdepend": "^2.5.0", + "php-coveralls/php-coveralls": "^1.0.2 || ^2.0", + "phploc/phploc": "^2.1 || ^3.0 || ^4.0", + "phpunit/phpunit": "<7.5.18", + "sebastian/phpcpd": "^2.0 || ^3.0 || ^4.0", + "squizlabs/php_codesniffer": "^3.1.1" + }, + "suggest": { + "ext-curl": "Install curl lib to be able to use the IdPMetadataParser for parsing remote XMLs", + "ext-gettext": "Install gettext and php5-gettext libs to handle translations", + "ext-openssl": "Install openssl lib in order to handle with x509 certs (require to support sign and encryption)" + }, + "type": "library", + "autoload": { + "psr-4": { + "OneLogin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "OneLogin PHP SAML Toolkit", + "homepage": "https://developers.onelogin.com/saml/php", + "keywords": [ + "SAML2", + "onelogin", + "saml" + ], + "support": { + "email": "sixto.garcia@onelogin.com", + "issues": "https://github.com/onelogin/php-saml/issues", + "source": "https://github.com/onelogin/php-saml/" + }, + "time": "2021-03-02T10:13:07+00:00" + }, { "name": "paragonie/constant_time_encoding", "version": "v2.6.3", @@ -5213,6 +5329,48 @@ ], "time": "2021-09-25T23:10:38+00:00" }, + { + "name": "robrichards/xmlseclibs", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/robrichards/xmlseclibs.git", + "reference": "f8f19e58f26cdb42c54b214ff8a820760292f8df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/f8f19e58f26cdb42c54b214ff8a820760292f8df", + "reference": "f8f19e58f26cdb42c54b214ff8a820760292f8df", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">= 5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "RobRichards\\XMLSecLibs\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "A PHP library for XML Security", + "homepage": "https://github.com/robrichards/xmlseclibs", + "keywords": [ + "security", + "signature", + "xml", + "xmldsig" + ], + "support": { + "issues": "https://github.com/robrichards/xmlseclibs/issues", + "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.1" + }, + "time": "2020-09-05T13:00:25+00:00" + }, { "name": "s9e/regexp-builder", "version": "1.4.6", diff --git a/config/bundles.php b/config/bundles.php index 8ca67ae7..91ddea04 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -27,4 +27,5 @@ return [ Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true], SpomkyLabs\CborBundle\SpomkyLabsCborBundle::class => ['all' => true], Webauthn\Bundle\WebauthnBundle::class => ['all' => true], + Hslavich\OneloginSamlBundle\HslavichOneloginSamlBundle::class => ['all' => true], ]; diff --git a/config/packages/hslavich_onelogin_saml.yaml b/config/packages/hslavich_onelogin_saml.yaml new file mode 100644 index 00000000..de3882ab --- /dev/null +++ b/config/packages/hslavich_onelogin_saml.yaml @@ -0,0 +1,57 @@ +hslavich_onelogin_saml: + # Basic settings + idp: + entityId: 'http://localhost:8080/realms/master' + singleSignOnService: + url: 'http://localhost:8080/realms/master/protocol/saml' + binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + singleLogoutService: + url: 'http://localhost:8080/realms/master/protocol/saml' + binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + x509cert: 'MIICmzCCAYMCBgGGcG8PJTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjMwMjIwMjAwNDMyWhcNMzMwMjIwMjAwNjEyWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQAle3ob0ary+Hq+mr2IvGueJicIxNqGeG/eV+NpoHUVggHSdb+9kudy+Os0xhAtz8nffTc8T5PK09GClXy7O5mAg8X9E5p0YeRZOxqgBXXVEtgPXaliD2N2mVrY/Ju2uLNAtrwWdBfnLBZuPZLD26TzOX/Q4u39SbhoA395S/iPwmxM00xDtrnXFGc2RYTgoTuLWFF6uioAmzxZSdIphLPiPwDMs5KCypW+lTOn8pztdAhAylXqiG7yFhReP7oEyb8IcNlUulJaloIfTWyLuQI1fEXA2gdkRULiOuxjGM3Wt2I6OOnZVzT7/+3/h7HVF4EI/xDpET6hQw7YszDr39AgMBAAEwDQYJKoZIhvcNAQELBQADggEBACaRkpf12OxGpdrsfsR5uslWl3GPA7HaKFHkRN3+0owf4j61rRJdxpkNmFKLGEZGAn3F+IBVzXIOx+mOq71BLKj/hxJ82bYJeUtK0a/fsX3S7z8TMXMgzzIQXS+XE4X7E8M3JEF+OKSuwG6bcaPJR8xscQ7i6z0rW14P1QgoEFAA6xhoHxK/AH2CTH/f8ojc2F5pPaYQJkuznd0OfcLAhPwMJ8btKGq9rNV/1EI59V+srA9lHvSWPfg6jXPsX96PSjTGljuHbZGMIka2mz4YOUvn9jlCGgv+gruIxeq8VKKPxfmDlSs9Jeof93MtYY92s4dDaJOru04mlqyKeFBic6o=' + sp: + entityId: 'http://localhost:8000/saml/metadata' + assertionConsumerService: + url: 'http://localhost:8000/saml/acs' + binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' + singleLogoutService: + url: 'http://localhost:8000/saml/logout' + binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + x509cert: 'MIICmzCCAYMCBgGGcIfLmDANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZwYXJ0ZGIwHhcNMjMwMjIwMjAzMTMzWhcNMzMwMjIwMjAzMzEzWjARMQ8wDQYDVQQDDAZwYXJ0ZGIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCmz9Lr8PxVjqqhGjjH4FbhVDbEPIplcQ1cFhTKu1NYpwHIm01pNnEwiyoLMBrZ16qq0PvCX+KCIjuJbC44k6N/+UCjbSZ8Fu+vyT9OkKVLvimc5sg9m9Kys+WiuaxzSEa2dA88khyGb6O9huuVYjibjROAN5I20h8f0FLfvolqWfEXVM00LIYYCHcvKNGq6PH0SS+2P6CGHoRrEFOoi7v7kkL6gWqYzPA/T7GSb78W14gU6FKcvQXPYOdsL7dYDy634BCd9k/tNF6JkiFi9U1IP8LEYkQUq3mKGRSjoPKKztws00ol3Nfd04XAdPKxIb3ff3io0vONE3QfJsgTzdR7AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEkYH4pCKxqoABjYGRml+ea5nteF/qsff6qHcNU8Unw1XLZO7ES1GU4Ij3RSFdt1SoKnphXnBtlVIechkOnOaECGs4RdbG4RGSFfusiEcWHbp1C0n3cnaUoV0/PJ3iETeeaPduqz9I/dlwhOxEOkpCaomYm4j2hfNcI9pmdM8bZuG+6dedh02ILrufQiCREmF6Sp+AOjueoFOoGwsXMbTuKo9pNizCO2OTT6nrjbNhjzoIvzbmlqiOGFs7YmpyTHcyqm66ACOnu1Nq9z9QLGkhvwjAmEytp6A9yEEkl0QO+DVhlAnvfdSMaR9jgyGtnBXQGOHv4osPtCtrk3IU4nB3s=' + privateKey: 'MIIEogIBAAKCAQEAps/S6/D8VY6qoRo4x+BW4VQ2xDyKZXENXBYUyrtTWKcByJtNaTZxMIsqCzAa2deqqtD7wl/igiI7iWwuOJOjf/lAo20mfBbvr8k/TpClS74pnObIPZvSsrPlormsc0hGtnQPPJIchm+jvYbrlWI4m40TgDeSNtIfH9BS376JalnxF1TNNCyGGAh3LyjRqujx9Ekvtj+ghh6EaxBTqIu7+5JC+oFqmMzwP0+xkm+/FteIFOhSnL0Fz2DnbC+3WA8ut+AQnfZP7TReiZIhYvVNSD/CxGJEFKt5ihkUo6Dyis7cLNNKJdzX3dOFwHTysSG93394qNLzjRN0HybIE83UewIDAQABAoIBAA1M7dLtPJlvzjAZQKTDQPondlRwRVKwUHHiutatWAhuDIjbxTDZ6+2Ecx5AQCvVc+C52BEYBx38L8YVz5uoPfWiwKInPlXPmF3qTHdtthhTecruZdHvvj2MdYdjiZoJjcXXfC2GsuqPNT2T5+3Zzoysk3z6MVjYqS2mtSzs6tUFZOY13/zMnrELjR2dFrigndjQgb5vENJ+/WNH4k9rt7KGDaMniJWHi3bDDNwilR0XrwhJVav0PIl7h8bMk95ixqo5B+RN0EMhfS+j3+8QJhYbGzXIyJbgMGTUGb23tBIDl6RFblLoic/PEVpA5yhVbLtdoMy5Mn+Y28yZPh6/VB0CgYEA1xtwZVbrRObv76bIkqgCXEYbiXhbFHzGTaI23UEBrV60VNr5LZl/vqA6JowDFUYl30Ytzr7TvC5KwfrNhaYOqVd4rMg145cn8J61bRHArMaXouVYVwWKEni50gCktVy8Tt2Nbzfja2rccCqv3JM2HHp49+e9CLi/D95XPAip3vcCgYEAxoYB7/OQmGMHnqwz2CUnLhGcnC1X4fZ+EJuRo1BXRcntCS4lwXCoyC3Es+YLZ6tfhUp17i/HaT3tJyp6F+EM43zIxwyDVCl6t0yFkjLHRdZHR3xGMMfcMLZGRY99Y25QyW1/GJFKff7D+rKvrpBVcl+9xLYcpIN6ULuHqs9f4Z0CgYBrDL2/wSTusltAEemJitE56K31mQ8CwCHUKuFQ9PQHurTV8e/F8LkxPf4ShuVV5gYc+oj7dd5brVII/W7gj0aGogBtRGoFLIl05xb1A7u2gFKgf7CaBiizjp8zUpyloVQZj4q+ibrFD3ZK4AOLKzvnqk+fWBWsTHzRQd56Avm++wKBgF3eR1Q6CojDanrwWaM+DgSOd0qxdfh2IK2hoX9jIaDyFY5dr6SDrIraeUPG5mWidowD5Tc2iEeO7G+0ef6IfxuhiR31ILPO2SOKny29rNOsug9nB5lRJyAxT5DchCFbq/9SMuJe8KYarHgBvWgA/yYRdx1oLqrrMA60XTW60E9RAoGALmoMX1CLe7BBrqaZrHBAqkV/6koTPSMuYVUZpbKgTmA7VsBScUZaX8huS7ARjLfoujxcNuFT4haPlY7gybvhEN0dMlqRwVNeiD3jo+PdDxu3h9SM6GTuVks1XWoifHBRj2NngBEipV3WDXLJR2sAbUArUdL58y12f7EWFdX41+s=' + # Optional settings + #baseurl: 'http://myapp.com' + #strict: true + debug: true + security: + allowRepeatAttributeName: true + # nameIdEncrypted: false + authnRequestsSigned: true + logoutRequestSigned: true + # logoutResponseSigned: false + # wantMessagesSigned: false + # wantAssertionsSigned: true + # wantNameIdEncrypted: false + # requestedAuthnContext: true + # signMetadata: false + # wantXMLValidation: true + # relaxDestinationValidation: false + # destinationStrictlyMatches: true + # rejectUnsolicitedResponsesWithInResponseTo: false + # signatureAlgorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' + # digestAlgorithm: 'http://www.w3.org/2001/04/xmlenc#sha256' + #contactPerson: + # technical: + # givenName: 'Tech User' + # emailAddress: 'techuser@example.com' + # support: + # givenName: 'Support User' + # emailAddress: 'supportuser@example.com' + # administrative: + # givenName: 'Administrative User' + # emailAddress: 'administrativeuser@example.com' + #organization: + # en: + # name: 'Example' + # displayname: 'Example' + # url: 'http://example.com' \ No newline at end of file diff --git a/config/packages/security.yaml b/config/packages/security.yaml index e58ab1ec..0e3b5c88 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -6,12 +6,22 @@ security: # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers providers: - # used to reload user from session & other features (e.g. switch_user) - app_user_provider: + local_users: entity: class: App\Entity\UserSystem\User property: name + saml_users: + saml: + user_class: App\Entity\UserSystem\User + default_roles: [ 'ROLE_USER' ] + + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + chain: + providers: ['local_users', 'saml_users'] + + firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ @@ -20,6 +30,7 @@ security: provider: app_user_provider lazy: true user_checker: App\Security\UserChecker + entry_point: form_login two_factor: auth_form_path: 2fa_login @@ -29,6 +40,13 @@ security: login_throttling: max_attempts: 5 # per minute + saml: + #username_attribute: username + #use_attribute_friendly_name: false + check_path: saml_acs + login_path: saml_login + failure_path: saml_login + # https://symfony.com/doc/current/security/form_login_setup.html form_login: login_path: login diff --git a/config/routes/hslavich_saml.yaml b/config/routes/hslavich_saml.yaml new file mode 100644 index 00000000..bbba3429 --- /dev/null +++ b/config/routes/hslavich_saml.yaml @@ -0,0 +1,2 @@ +hslavich_saml_sp: + resource: "@HslavichOneloginSamlBundle/Resources/config/routing.yml" \ No newline at end of file diff --git a/symfony.lock b/symfony.lock index d453ff2e..a4bb23ff 100644 --- a/symfony.lock +++ b/symfony.lock @@ -173,6 +173,9 @@ "gregwar/captcha-bundle": { "version": "v2.0.6" }, + "hslavich/oneloginsaml-bundle": { + "version": "v2.10.0" + }, "imagine/imagine": { "version": "1.2.2" }, diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig index 44d98397..1dd6c52a 100644 --- a/templates/security/login.html.twig +++ b/templates/security/login.html.twig @@ -27,6 +27,8 @@ + SAML Login +
From 78ec0f1ea3046d324ed1e4b5ec708c62bfb6db45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Mon, 20 Feb 2023 23:04:20 +0100 Subject: [PATCH 02/25] Create a new DB user when somebody logs in using SAML --- config/packages/hslavich_onelogin_saml.yaml | 2 +- config/packages/security.yaml | 19 +++------ config/services.yaml | 3 ++ src/Entity/UserSystem/User.php | 23 ++++++++++- .../UserSystem/LoginSuccessSubscriber.php | 5 ++- src/Security/SamlUserFactory.php | 41 +++++++++++++++++++ 6 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 src/Security/SamlUserFactory.php diff --git a/config/packages/hslavich_onelogin_saml.yaml b/config/packages/hslavich_onelogin_saml.yaml index de3882ab..d33376d3 100644 --- a/config/packages/hslavich_onelogin_saml.yaml +++ b/config/packages/hslavich_onelogin_saml.yaml @@ -28,7 +28,7 @@ hslavich_onelogin_saml: # nameIdEncrypted: false authnRequestsSigned: true logoutRequestSigned: true - # logoutResponseSigned: false + logoutResponseSigned: true # wantMessagesSigned: false # wantAssertionsSigned: true # wantNameIdEncrypted: false diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 0e3b5c88..6758cf80 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -4,23 +4,13 @@ security: password_hashers: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' - # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers providers: - local_users: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: entity: class: App\Entity\UserSystem\User property: name - saml_users: - saml: - user_class: App\Entity\UserSystem\User - default_roles: [ 'ROLE_USER' ] - - # used to reload user from session & other features (e.g. switch_user) - app_user_provider: - chain: - providers: ['local_users', 'saml_users'] - firewalls: dev: @@ -41,8 +31,9 @@ security: max_attempts: 5 # per minute saml: - #username_attribute: username - #use_attribute_friendly_name: false + use_referer: true + user_factory: saml_user_factory + persist_user: true check_path: saml_acs login_path: saml_login failure_path: saml_login diff --git a/config/services.yaml b/config/services.yaml index f2200115..5b5f1f35 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -127,6 +127,9 @@ services: # Security #################################################################################################################### + saml_user_factory: + class: App\Security\SamlUserFactory + #################################################################################################################### # Cache #################################################################################################################### diff --git a/src/Entity/UserSystem/User.php b/src/Entity/UserSystem/User.php index 56034dfd..e29880db 100644 --- a/src/Entity/UserSystem/User.php +++ b/src/Entity/UserSystem/User.php @@ -30,6 +30,7 @@ use App\Security\Interfaces\HasPermissionsInterface; use App\Validator\Constraints\Selectable; use App\Validator\Constraints\ValidPermission; use App\Validator\Constraints\ValidTheme; +use Hslavich\OneloginSamlBundle\Security\User\SamlUserInterface; use Jbtronics\TFAWebauthn\Model\LegacyU2FKeyInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Webauthn\PublicKeyCredentialUserEntity; @@ -60,7 +61,8 @@ use Jbtronics\TFAWebauthn\Model\TwoFactorInterface as WebauthnTwoFactorInterface * @ORM\EntityListeners({"App\EntityListeners\TreeCacheInvalidationListener"}) * @UniqueEntity("name", message="validator.user.username_already_used") */ -class User extends AttachmentContainingDBElement implements UserInterface, HasPermissionsInterface, TwoFactorInterface, BackupCodeInterface, TrustedDeviceInterface, WebauthnTwoFactorInterface, PreferredProviderInterface, PasswordAuthenticatedUserInterface +class User extends AttachmentContainingDBElement implements UserInterface, HasPermissionsInterface, TwoFactorInterface, + BackupCodeInterface, TrustedDeviceInterface, WebauthnTwoFactorInterface, PreferredProviderInterface, PasswordAuthenticatedUserInterface, SamlUserInterface { //use MasterAttachmentTrait; @@ -860,4 +862,23 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe { $this->webauthn_keys->add($webauthnKey); } + + public function setSamlAttributes(array $attributes) + { + //When mail attribute exists, set it + if (isset($attributes['email'])) { + $this->setEmail($attributes['email'][0]); + } + //When first name attribute exists, set it + if (isset($attributes['firstName'])) { + $this->setFirstName($attributes['firstName'][0]); + } + //When last name attribute exists, set it + if (isset($attributes['lastName'])) { + $this->setLastName($attributes['lastName'][0]); + } + if (isset($attributes['department'])) { + $this->setDepartment($attributes['department'][0]); + } + } } diff --git a/src/EventSubscriber/UserSystem/LoginSuccessSubscriber.php b/src/EventSubscriber/UserSystem/LoginSuccessSubscriber.php index 00b99fca..5e18826b 100644 --- a/src/EventSubscriber/UserSystem/LoginSuccessSubscriber.php +++ b/src/EventSubscriber/UserSystem/LoginSuccessSubscriber.php @@ -57,10 +57,11 @@ final class LoginSuccessSubscriber implements EventSubscriberInterface $ip = $event->getRequest()->getClientIp(); $log = new UserLoginLogEntry($ip, $this->gpdr_compliance); $user = $event->getAuthenticationToken()->getUser(); - if ($user instanceof User) { + if ($user instanceof User && $user->getID()) { $log->setTargetElement($user); + $this->eventLogger->logAndFlush($log); } - $this->eventLogger->logAndFlush($log); + $this->flashBag->add('notice', $this->translator->trans('flash.login_successful')); } diff --git a/src/Security/SamlUserFactory.php b/src/Security/SamlUserFactory.php new file mode 100644 index 00000000..b5659da6 --- /dev/null +++ b/src/Security/SamlUserFactory.php @@ -0,0 +1,41 @@ +. + */ + +namespace App\Security; + +use App\Entity\UserSystem\User; +use Hslavich\OneloginSamlBundle\Security\User\SamlUserFactoryInterface; +use Symfony\Component\Security\Core\User\UserInterface; + +class SamlUserFactory implements SamlUserFactoryInterface +{ + public function createUser($username, array $attributes = []): UserInterface + { + $user = new User(); + $user->setName($username); + $user->setNeedPwChange(false); + $user->setPassword('$$SAML$$'); + + $user->setSamlAttributes($attributes); + + + return $user; + } +} \ No newline at end of file From 97c3b9002af5a0bb3599a6cb9d3f8dfccc758b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 21 Feb 2023 00:29:50 +0100 Subject: [PATCH 03/25] Mark SAML users as so in database and disable local password changing then. --- migrations/Version20230220221024.php | 40 + src/Controller/UserSettingsController.php | 29 +- .../WebauthnKeyRegistrationController.php | 23 + src/Entity/UserSystem/User.php | 30 +- src/Form/UserAdminForm.php | 6 +- src/Form/UserSettingsType.php | 2 +- src/Repository/NamedDBElementRepository.php | 12 +- src/Security/SamlUserFactory.php | 2 + templates/admin/user_admin.html.twig | 4 + templates/users/_2fa_settings.html.twig | 6 +- templates/users/user_info.html.twig | 17 +- templates/users/user_settings.html.twig | 9 + translations/messages.en.xlf | 2440 +++++++++-------- translations/security.en.xlf | 2 +- translations/validators.en.xlf | 56 +- 15 files changed, 1414 insertions(+), 1264 deletions(-) create mode 100644 migrations/Version20230220221024.php diff --git a/migrations/Version20230220221024.php b/migrations/Version20230220221024.php new file mode 100644 index 00000000..01d65d51 --- /dev/null +++ b/migrations/Version20230220221024.php @@ -0,0 +1,40 @@ +addSql('ALTER TABLE `users` ADD saml_user TINYINT(1) NOT NULL DEFAULT 0'); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE `users` DROP saml_user'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('ALTER TABLE users ADD saml_user BOOLEAN NOT NULL DEFAULT 0'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('ALTER TABLE `users` DROP saml_user'); + } +} diff --git a/src/Controller/UserSettingsController.php b/src/Controller/UserSettingsController.php index 152189ff..80353ac9 100644 --- a/src/Controller/UserSettingsController.php +++ b/src/Controller/UserSettingsController.php @@ -83,6 +83,10 @@ class UserSettingsController extends AbstractController return new RuntimeException('This controller only works only for Part-DB User objects!'); } + if ($user->isSamlUser()) { + throw new RuntimeException('You can not remove U2F keys from SAML users!'); + } + if (empty($user->getBackupCodes())) { $this->addFlash('error', 'tfa_backup.no_codes_enabled'); @@ -112,6 +116,10 @@ class UserSettingsController extends AbstractController throw new RuntimeException('This controller only works only for Part-DB User objects!'); } + if ($user->isSamlUser()) { + throw new RuntimeException('You can not remove U2F keys from SAML users!'); + } + if ($this->isCsrfTokenValid('delete'.$user->getId(), $request->request->get('_token'))) { //Handle U2F key removal if ($request->request->has('key_id')) { @@ -192,6 +200,10 @@ class UserSettingsController extends AbstractController return new RuntimeException('This controller only works only for Part-DB User objects!'); } + if ($user->isSamlUser()) { + throw new RuntimeException('You can not remove U2F keys from SAML users!'); + } + if ($this->isCsrfTokenValid('devices_reset'.$user->getId(), $request->request->get('_token'))) { $user->invalidateTrustedDeviceTokens(); $entityManager->flush(); @@ -281,14 +293,14 @@ class UserSettingsController extends AbstractController ]) ->add('old_password', PasswordType::class, [ 'label' => 'user.settings.pw_old.label', - 'disabled' => $this->demo_mode, + 'disabled' => $this->demo_mode || $user->isSamlUser(), 'attr' => [ 'autocomplete' => 'current-password', ], 'constraints' => [new UserPassword()], ]) //This constraint checks, if the current user pw was inputted. ->add('new_password', RepeatedType::class, [ - 'disabled' => $this->demo_mode, + 'disabled' => $this->demo_mode || $user->isSamlUser(), 'type' => PasswordType::class, 'first_options' => [ 'label' => 'user.settings.pw_new.label', @@ -307,7 +319,10 @@ class UserSettingsController extends AbstractController 'max' => 128, ])], ]) - ->add('submit', SubmitType::class, ['label' => 'save']) + ->add('submit', SubmitType::class, [ + 'label' => 'save', + 'disabled' => $this->demo_mode || $user->isSamlUser(), + ]) ->getForm(); $pw_form->handleRequest($request); @@ -327,7 +342,9 @@ class UserSettingsController extends AbstractController } //Handle 2FA things - $google_form = $this->createForm(TFAGoogleSettingsType::class, $user); + $google_form = $this->createForm(TFAGoogleSettingsType::class, $user, [ + 'disabled' => $this->demo_mode || $user->isSamlUser(), + ]); $google_enabled = $user->isGoogleAuthenticatorEnabled(); if (!$google_enabled && !$form->isSubmitted()) { $user->setGoogleAuthenticatorSecret($googleAuthenticator->generateSecret()); @@ -335,7 +352,7 @@ class UserSettingsController extends AbstractController } $google_form->handleRequest($request); - if (!$this->demo_mode && $google_form->isSubmitted() && $google_form->isValid()) { + if (!$this->demo_mode && !$user->isSamlUser() && $google_form->isSubmitted() && $google_form->isValid()) { if (!$google_enabled) { //Save 2FA settings (save secrets) $user->setGoogleAuthenticatorSecret($google_form->get('googleAuthenticatorSecret')->getData()); @@ -369,7 +386,7 @@ class UserSettingsController extends AbstractController ])->getForm(); $backup_form->handleRequest($request); - if (!$this->demo_mode && $backup_form->isSubmitted() && $backup_form->isValid()) { + if (!$this->demo_mode && !$user->isSamlUser() && $backup_form->isSubmitted() && $backup_form->isValid()) { $backupCodeManager->regenerateBackupCodes($user); $em->flush(); $this->addFlash('success', 'user.settings.2fa.backup_codes.regenerated'); diff --git a/src/Controller/WebauthnKeyRegistrationController.php b/src/Controller/WebauthnKeyRegistrationController.php index c60f8e94..8a26346a 100644 --- a/src/Controller/WebauthnKeyRegistrationController.php +++ b/src/Controller/WebauthnKeyRegistrationController.php @@ -20,9 +20,11 @@ namespace App\Controller; +use App\Entity\UserSystem\User; use App\Entity\UserSystem\WebauthnKey; use Doctrine\ORM\EntityManagerInterface; use Jbtronics\TFAWebauthn\Services\TFAWebauthnRegistrationHelper; +use RuntimeException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; @@ -31,6 +33,13 @@ use function Symfony\Component\Translation\t; class WebauthnKeyRegistrationController extends AbstractController { + private bool $demo_mode; + + public function __construct(bool $demo_mode) + { + $this->demo_mode = $demo_mode; + } + /** * @Route("/webauthn/register", name="webauthn_register") */ @@ -39,6 +48,20 @@ class WebauthnKeyRegistrationController extends AbstractController //When user change its settings, he should be logged in fully. $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + if ($this->demo_mode) { + throw new RuntimeException('You can not do 2FA things in demo mode'); + } + + $user = $this->getUser(); + + if (!$user instanceof User) { + throw new RuntimeException('This controller only works only for Part-DB User objects!'); + } + + if ($user->isSamlUser()) { + throw new RuntimeException('You can not remove U2F keys from SAML users!'); + } + //If form was submitted, check the auth response if ($request->getMethod() === 'POST') { $webauthnResponse = $request->request->get('_auth_code'); diff --git a/src/Entity/UserSystem/User.php b/src/Entity/UserSystem/User.php index e29880db..5b336e4b 100644 --- a/src/Entity/UserSystem/User.php +++ b/src/Entity/UserSystem/User.php @@ -240,10 +240,16 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * @var DateTime the time until the password reset token is valid - * @ORM\Column(type="datetime", nullable=true) + * @ORM\Column(type="datetime", nullable=true, columnDefinition="DEFAULT NULL") */ protected $pw_reset_expires; + /** + * @var bool True if the user was created by a SAML provider (and therefore cannot change its password) + * @ORM\Column(type="boolean") + */ + protected bool $saml_user = false; + public function __construct() { parent::__construct(); @@ -863,6 +869,28 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe $this->webauthn_keys->add($webauthnKey); } + /** + * Returns true, if the user was created by the SAML authentication. + * @return bool + */ + public function isSamlUser(): bool + { + return $this->saml_user; + } + + /** + * Sets the saml_user flag. + * @param bool $saml_user + * @return User + */ + public function setSamlUser(bool $saml_user): User + { + $this->saml_user = $saml_user; + return $this; + } + + + public function setSamlAttributes(array $attributes) { //When mail attribute exists, set it diff --git a/src/Form/UserAdminForm.php b/src/Form/UserAdminForm.php index 774cf8f7..380d8c8f 100644 --- a/src/Form/UserAdminForm.php +++ b/src/Form/UserAdminForm.php @@ -65,7 +65,7 @@ class UserAdminForm extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { - /** @var AbstractStructuralDBElement $entity */ + /** @var User $entity */ $entity = $options['data']; $is_new = null === $entity->getID(); @@ -164,7 +164,7 @@ class UserAdminForm extends AbstractType 'invalid_message' => 'password_must_match', 'required' => false, 'mapped' => false, - 'disabled' => !$this->security->isGranted('set_password', $entity), + 'disabled' => !$this->security->isGranted('set_password', $entity) || $entity->isSamlUser(), 'constraints' => [new Length([ 'min' => 6, 'max' => 128, @@ -174,7 +174,7 @@ class UserAdminForm extends AbstractType ->add('need_pw_change', CheckboxType::class, [ 'required' => false, 'label' => 'user.edit.needs_pw_change', - 'disabled' => !$this->security->isGranted('set_password', $entity), + 'disabled' => !$this->security->isGranted('set_password', $entity) || $entity->isSamlUser(), ]) ->add('disabled', CheckboxType::class, [ diff --git a/src/Form/UserSettingsType.php b/src/Form/UserSettingsType.php index fd07ea3b..685ac14d 100644 --- a/src/Form/UserSettingsType.php +++ b/src/Form/UserSettingsType.php @@ -57,7 +57,7 @@ class UserSettingsType extends AbstractType $builder ->add('name', TextType::class, [ 'label' => 'user.username.label', - 'disabled' => !$this->security->isGranted('edit_username', $options['data']) || $this->demo_mode, + 'disabled' => !$this->security->isGranted('edit_username', $options['data']) || $this->demo_mode || $options['data']->isSamlUser(), ]) ->add('first_name', TextType::class, [ 'required' => false, diff --git a/src/Repository/NamedDBElementRepository.php b/src/Repository/NamedDBElementRepository.php index 0985b8e1..aa89453d 100644 --- a/src/Repository/NamedDBElementRepository.php +++ b/src/Repository/NamedDBElementRepository.php @@ -45,10 +45,16 @@ class NamedDBElementRepository extends DBElementRepository $node->setId($entity->getID()); $result[] = $node; - if ($entity instanceof User && $entity->isDisabled()) { - //If this is an user, then add a badge when it is disabled - $node->setIcon('fa-fw fa-treeview fa-solid fa-user-lock text-muted'); + if ($entity instanceof User) { + if ($entity->isDisabled()) { + //If this is an user, then add a badge when it is disabled + $node->setIcon('fa-fw fa-treeview fa-solid fa-user-lock text-muted'); + } + if ($entity->isSamlUser()) { + $node->setIcon('fa-fw fa-treeview fa-solid fa-house-user text-muted'); + } } + } return $result; diff --git a/src/Security/SamlUserFactory.php b/src/Security/SamlUserFactory.php index b5659da6..06e31264 100644 --- a/src/Security/SamlUserFactory.php +++ b/src/Security/SamlUserFactory.php @@ -32,6 +32,8 @@ class SamlUserFactory implements SamlUserFactoryInterface $user->setName($username); $user->setNeedPwChange(false); $user->setPassword('$$SAML$$'); + //This is a SAML user now! + $user->setSamlUser(true); $user->setSamlAttributes($attributes); diff --git a/templates/admin/user_admin.html.twig b/templates/admin/user_admin.html.twig index 322ae33d..5343377e 100644 --- a/templates/admin/user_admin.html.twig +++ b/templates/admin/user_admin.html.twig @@ -33,6 +33,10 @@
+
+ {% trans %}user.saml_user{% endtrans %} +
+ {{ form_row(form.new_password) }} {{ form_row(form.need_pw_change) }} {{ form_row(form.disabled) }} diff --git a/templates/users/_2fa_settings.html.twig b/templates/users/_2fa_settings.html.twig index 0c06dcc3..2b7f9c4c 100644 --- a/templates/users/_2fa_settings.html.twig +++ b/templates/users/_2fa_settings.html.twig @@ -151,7 +151,9 @@

{% trans %}tfa_u2f.no_keys_registered{% endtrans %}

{% endif %} - {% trans %}tfa_u2f.add_new_key{% endtrans %} + {% if not user.samlUser %} + {% trans %}tfa_u2f.add_new_key{% endtrans %} + {% endif %}
@@ -163,7 +165,7 @@ - +
diff --git a/templates/users/user_info.html.twig b/templates/users/user_info.html.twig index f18df094..fd5a6374 100644 --- a/templates/users/user_info.html.twig +++ b/templates/users/user_info.html.twig @@ -52,9 +52,16 @@
-

{{ user.group.fullPath }}

+

{{ user.group.fullPath ?? '' }}

+ {% if user.samlUser %} +
+
+ {% trans %}user.saml_user{% endtrans %} +
+
+ {% endif %}
@@ -74,9 +81,9 @@ {% endif %}
- {% if datatable is defined and datatable is not null %} - {% import "components/history_log_macros.html.twig" as log %} - {{ log.element_history_component(datatable) }} - {% endif %} + {% if datatable is defined and datatable is not null %} + {% import "components/history_log_macros.html.twig" as log %} + {{ log.element_history_component(datatable) }} + {% endif %}
{% endblock %} \ No newline at end of file diff --git a/templates/users/user_settings.html.twig b/templates/users/user_settings.html.twig index 5aa1b9ad..b67708b9 100644 --- a/templates/users/user_settings.html.twig +++ b/templates/users/user_settings.html.twig @@ -52,6 +52,15 @@ {% block content %} {{ parent() }} + {% if user.samlUser %} + + {% endif %} + {% include "users/_2fa_settings.html.twig" %}
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 16e88fbf..7db2dadc 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -7,7 +7,7 @@ Part-DB1\templates\AdminPages\AttachmentTypeAdmin.html.twig:4 templates\AdminPages\AttachmentTypeAdmin.html.twig:4 - + attachment_type.caption File types for attachments @@ -17,7 +17,7 @@ Part-DB1\templates\AdminPages\AttachmentTypeAdmin.html.twig:12 new - + attachment_type.edit Edit file type @@ -27,7 +27,7 @@ Part-DB1\templates\AdminPages\AttachmentTypeAdmin.html.twig:16 new - + attachment_type.new New file type @@ -46,7 +46,7 @@ templates\base.html.twig:197 templates\base.html.twig:225 - + category.labelp Categories @@ -59,7 +59,7 @@ Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:11 templates\AdminPages\CategoryAdmin.html.twig:8 - + admin.options Options @@ -72,7 +72,7 @@ Part-DB1\templates\AdminPages\CompanyAdminBase.html.twig:15 templates\AdminPages\CategoryAdmin.html.twig:9 - + admin.advanced Advanced @@ -82,7 +82,7 @@ Part-DB1\templates\AdminPages\CategoryAdmin.html.twig:13 new - + category.edit Edit category @@ -92,7 +92,7 @@ Part-DB1\templates\AdminPages\CategoryAdmin.html.twig:17 new - + category.new New category @@ -102,7 +102,7 @@ Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:4 Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:4 - + currency.caption Currency @@ -112,7 +112,7 @@ Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:12 Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:12 - + currency.iso_code.caption ISO code @@ -122,7 +122,7 @@ Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:15 Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:15 - + currency.symbol.caption Currency symbol @@ -132,7 +132,7 @@ Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:29 new - + currency.edit Edit currency @@ -142,7 +142,7 @@ Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:33 new - + currency.new New currency @@ -153,7 +153,7 @@ Part-DB1\templates\AdminPages\DeviceAdmin.html.twig:4 templates\AdminPages\DeviceAdmin.html.twig:4 - + project.caption Project @@ -163,7 +163,7 @@ Part-DB1\templates\AdminPages\DeviceAdmin.html.twig:8 new - + project.edit Edit project @@ -173,7 +173,7 @@ Part-DB1\templates\AdminPages\DeviceAdmin.html.twig:12 new - + project.new New project @@ -196,7 +196,7 @@ templates\base.html.twig:206 templates\base.html.twig:237 - + search.placeholder Search @@ -212,7 +212,7 @@ templates\base.html.twig:193 templates\base.html.twig:221 - + expandAll Expand All @@ -228,7 +228,7 @@ templates\base.html.twig:194 templates\base.html.twig:222 - + reduceAll Reduce All @@ -240,9 +240,9 @@ Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:54 Part-DB1\templates\Parts\info\_sidebar.html.twig:4 - + part.info.timetravel_hint - This is how the part appeared before %timestamp%. <i>Please note that this feature is experimental, so the infos are maybe not correct.</i> + Please note that this feature is experimental, so the infos are maybe not correct.]]> @@ -251,7 +251,7 @@ Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:60 templates\AdminPages\EntityAdminBase.html.twig:42 - + standard.label Properties @@ -262,7 +262,7 @@ Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:61 templates\AdminPages\EntityAdminBase.html.twig:43 - + infos.label Infos @@ -273,7 +273,7 @@ Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:63 new - + history.label History @@ -284,7 +284,7 @@ Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:66 templates\AdminPages\EntityAdminBase.html.twig:45 - + export.label Export @@ -295,7 +295,7 @@ Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:68 templates\AdminPages\EntityAdminBase.html.twig:47 - + import_export.label Import / Export @@ -305,7 +305,7 @@ Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:69 Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:69 - + mass_creation.label Mass creation @@ -316,7 +316,7 @@ Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:82 templates\AdminPages\EntityAdminBase.html.twig:59 - + admin.common Common @@ -326,7 +326,7 @@ Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:86 Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:86 - + admin.attachments Attachments @@ -335,7 +335,7 @@ Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:90 - + admin.parameters Parameters @@ -346,7 +346,7 @@ Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:167 templates\AdminPages\EntityAdminBase.html.twig:142 - + export_all.label Export all elements @@ -356,7 +356,7 @@ Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:173 - + mass_creation.help Each line will be interpreted as a name of an element, which will be created. You can create nested structures by indentations. @@ -367,7 +367,7 @@ Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:45 templates\AdminPages\EntityAdminBase.html.twig:35 - + edit.caption Edit element "%name" @@ -378,7 +378,7 @@ Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:50 templates\AdminPages\EntityAdminBase.html.twig:37 - + new.caption New element @@ -393,7 +393,7 @@ templates\base.html.twig:199 templates\base.html.twig:227 - + footprint.labelp Footprints @@ -403,7 +403,7 @@ Part-DB1\templates\AdminPages\FootprintAdmin.html.twig:13 new - + footprint.edit Edit footprint @@ -413,7 +413,7 @@ Part-DB1\templates\AdminPages\FootprintAdmin.html.twig:17 new - + footprint.new New footprint @@ -423,7 +423,7 @@ Part-DB1\templates\AdminPages\GroupAdmin.html.twig:4 Part-DB1\templates\AdminPages\GroupAdmin.html.twig:4 - + group.edit.caption Groups @@ -435,7 +435,7 @@ Part-DB1\templates\AdminPages\GroupAdmin.html.twig:9 Part-DB1\templates\AdminPages\UserAdmin.html.twig:16 - + user.edit.permissions Permissions @@ -445,7 +445,7 @@ Part-DB1\templates\AdminPages\GroupAdmin.html.twig:24 new - + group.edit Edit group @@ -455,7 +455,7 @@ Part-DB1\templates\AdminPages\GroupAdmin.html.twig:28 new - + group.new New group @@ -464,7 +464,7 @@ Part-DB1\templates\AdminPages\LabelProfileAdmin.html.twig:4 - + label_profile.caption Label profiles @@ -473,7 +473,7 @@ Part-DB1\templates\AdminPages\LabelProfileAdmin.html.twig:8 - + label_profile.advanced Advanced @@ -482,7 +482,7 @@ Part-DB1\templates\AdminPages\LabelProfileAdmin.html.twig:9 - + label_profile.comment Notes @@ -492,7 +492,7 @@ Part-DB1\templates\AdminPages\LabelProfileAdmin.html.twig:55 new - + label_profile.edit Edit label profile @@ -502,7 +502,7 @@ Part-DB1\templates\AdminPages\LabelProfileAdmin.html.twig:59 new - + label_profile.new New label profile @@ -513,7 +513,7 @@ Part-DB1\templates\AdminPages\ManufacturerAdmin.html.twig:4 templates\AdminPages\ManufacturerAdmin.html.twig:4 - + manufacturer.caption Manufacturers @@ -523,7 +523,7 @@ Part-DB1\templates\AdminPages\ManufacturerAdmin.html.twig:8 new - + manufacturer.edit Edit manufacturer @@ -533,7 +533,7 @@ Part-DB1\templates\AdminPages\ManufacturerAdmin.html.twig:12 new - + manufacturer.new New manufacturer @@ -543,7 +543,7 @@ Part-DB1\templates\AdminPages\MeasurementUnitAdmin.html.twig:4 Part-DB1\templates\AdminPages\MeasurementUnitAdmin.html.twig:4 - + measurement_unit.caption Measurement Unit @@ -558,7 +558,7 @@ templates\base.html.twig:198 templates\base.html.twig:226 - + storelocation.labelp Store locations @@ -568,7 +568,7 @@ Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:32 new - + storelocation.edit Edit store location @@ -578,7 +578,7 @@ Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:36 new - + storelocation.new New store location @@ -589,7 +589,7 @@ Part-DB1\templates\AdminPages\SupplierAdmin.html.twig:4 templates\AdminPages\SupplierAdmin.html.twig:4 - + supplier.caption Suppliers @@ -599,7 +599,7 @@ Part-DB1\templates\AdminPages\SupplierAdmin.html.twig:16 new - + supplier.edit Edit supplier @@ -609,7 +609,7 @@ Part-DB1\templates\AdminPages\SupplierAdmin.html.twig:20 new - + supplier.new New supplier @@ -619,7 +619,7 @@ Part-DB1\templates\AdminPages\UserAdmin.html.twig:8 Part-DB1\templates\AdminPages\UserAdmin.html.twig:8 - + user.edit.caption Users @@ -629,7 +629,7 @@ Part-DB1\templates\AdminPages\UserAdmin.html.twig:14 Part-DB1\templates\AdminPages\UserAdmin.html.twig:14 - + user.edit.configuration Configuration @@ -639,7 +639,7 @@ Part-DB1\templates\AdminPages\UserAdmin.html.twig:15 Part-DB1\templates\AdminPages\UserAdmin.html.twig:15 - + user.edit.password Password @@ -649,7 +649,7 @@ Part-DB1\templates\AdminPages\UserAdmin.html.twig:45 Part-DB1\templates\AdminPages\UserAdmin.html.twig:45 - + user.edit.tfa.caption Two-factor authentication @@ -659,7 +659,7 @@ Part-DB1\templates\AdminPages\UserAdmin.html.twig:47 Part-DB1\templates\AdminPages\UserAdmin.html.twig:47 - + user.edit.tfa.google_active Authenticator app active @@ -673,7 +673,7 @@ Part-DB1\templates\Users\backup_codes.html.twig:15 Part-DB1\templates\Users\_2fa_settings.html.twig:95 - + tfa_backup.remaining_tokens Remaining backup codes count @@ -687,7 +687,7 @@ Part-DB1\templates\Users\backup_codes.html.twig:17 Part-DB1\templates\Users\_2fa_settings.html.twig:96 - + tfa_backup.generation_date Generation date of the backup codes @@ -699,7 +699,7 @@ Part-DB1\templates\AdminPages\UserAdmin.html.twig:53 Part-DB1\templates\AdminPages\UserAdmin.html.twig:60 - + user.edit.tfa.disabled Method not enabled @@ -709,7 +709,7 @@ Part-DB1\templates\AdminPages\UserAdmin.html.twig:56 Part-DB1\templates\AdminPages\UserAdmin.html.twig:56 - + user.edit.tfa.u2f_keys_count Active security keys @@ -719,7 +719,7 @@ Part-DB1\templates\AdminPages\UserAdmin.html.twig:72 Part-DB1\templates\AdminPages\UserAdmin.html.twig:72 - + user.edit.tfa.disable_tfa_title Do you really want to proceed? @@ -729,12 +729,12 @@ Part-DB1\templates\AdminPages\UserAdmin.html.twig:72 Part-DB1\templates\AdminPages\UserAdmin.html.twig:72 - + user.edit.tfa.disable_tfa_message - This will disable <b>all active two-factor authentication methods of the user</b> and delete the <b>backup codes</b>! -<br> -The user will have to set up all two-factor authentication methods again and print new backup codes! <br><br> -<b>Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!</b> + all active two-factor authentication methods of the user and delete the backup codes! +
+The user will have to set up all two-factor authentication methods again and print new backup codes!

+Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!]]>
@@ -742,7 +742,7 @@ The user will have to set up all two-factor authentication methods again and pri Part-DB1\templates\AdminPages\UserAdmin.html.twig:73 Part-DB1\templates\AdminPages\UserAdmin.html.twig:73 - + user.edit.tfa.disable_tfa.btn Disable all two-factor authentication methods @@ -752,7 +752,7 @@ The user will have to set up all two-factor authentication methods again and pri Part-DB1\templates\AdminPages\UserAdmin.html.twig:85 new - + user.edit Edit user @@ -762,7 +762,7 @@ The user will have to set up all two-factor authentication methods again and pri Part-DB1\templates\AdminPages\UserAdmin.html.twig:89 new - + user.new New user @@ -775,7 +775,7 @@ The user will have to set up all two-factor authentication methods again and pri Part-DB1\templates\Parts\edit\_attachments.html.twig:4 Part-DB1\templates\Parts\info\_attachments_info.html.twig:63 - + attachment.delete Delete @@ -789,7 +789,7 @@ The user will have to set up all two-factor authentication methods again and pri Part-DB1\templates\Parts\edit\_attachments.html.twig:38 Part-DB1\src\DataTables\AttachmentDataTable.php:159 - + attachment.external External @@ -801,7 +801,7 @@ The user will have to set up all two-factor authentication methods again and pri Part-DB1\templates\AdminPages\_attachments.html.twig:47 Part-DB1\templates\Parts\edit\_attachments.html.twig:45 - + attachment.preview.alt Attachment thumbnail @@ -815,7 +815,7 @@ The user will have to set up all two-factor authentication methods again and pri Part-DB1\templates\Parts\edit\_attachments.html.twig:48 Part-DB1\templates\Parts\info\_attachments_info.html.twig:45 - + attachment.view View @@ -831,7 +831,7 @@ The user will have to set up all two-factor authentication methods again and pri Part-DB1\templates\Parts\info\_attachments_info.html.twig:38 Part-DB1\src\DataTables\AttachmentDataTable.php:166 - + attachment.file_not_found File not found @@ -843,7 +843,7 @@ The user will have to set up all two-factor authentication methods again and pri Part-DB1\templates\Parts\info\_attachments_info.html.twig:48 Part-DB1\templates\Parts\edit\_attachments.html.twig:62 - + attachment.secure Private attachment @@ -855,7 +855,7 @@ The user will have to set up all two-factor authentication methods again and pri Part-DB1\templates\AdminPages\_attachments.html.twig:77 Part-DB1\templates\Parts\edit\_attachments.html.twig:75 - + attachment.create Add attachment @@ -869,7 +869,7 @@ The user will have to set up all two-factor authentication methods again and pri Part-DB1\templates\Parts\edit\_attachments.html.twig:80 Part-DB1\templates\Parts\edit\_lots.html.twig:33 - + part_lot.edit.delete.confirm Do you really want to delete this stock? This can not be undone! @@ -880,7 +880,7 @@ The user will have to set up all two-factor authentication methods again and pri Part-DB1\templates\AdminPages\_delete_form.html.twig:2 templates\AdminPages\_delete_form.html.twig:2 - + entity.delete.confirm_title You really want to delete %name%? @@ -891,11 +891,11 @@ The user will have to set up all two-factor authentication methods again and pri Part-DB1\templates\AdminPages\_delete_form.html.twig:3 templates\AdminPages\_delete_form.html.twig:3 - + entity.delete.message - This can not be undone! -<br> -Sub elements will be moved upwards. + +Sub elements will be moved upwards.]]> @@ -904,7 +904,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_delete_form.html.twig:11 templates\AdminPages\_delete_form.html.twig:9 - + entity.delete Delete element @@ -919,7 +919,7 @@ Sub elements will be moved upwards. Part-DB1\src\Form\Part\PartBaseType.php:267 new - + edit.log_comment Change comment @@ -930,7 +930,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_delete_form.html.twig:24 templates\AdminPages\_delete_form.html.twig:12 - + entity.delete.recursive Delete recursive (all sub elements) @@ -939,7 +939,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_duplicate.html.twig:3 - + entity.duplicate Duplicate element @@ -953,7 +953,7 @@ Sub elements will be moved upwards. templates\AdminPages\_export_form.html.twig:4 src\Form\ImportType.php:67 - + export.format File format @@ -964,7 +964,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_export_form.html.twig:16 templates\AdminPages\_export_form.html.twig:16 - + export.level Verbosity level @@ -975,7 +975,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_export_form.html.twig:19 templates\AdminPages\_export_form.html.twig:19 - + export.level.simple Simple @@ -986,7 +986,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_export_form.html.twig:20 templates\AdminPages\_export_form.html.twig:20 - + export.level.extended Extended @@ -997,7 +997,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_export_form.html.twig:21 templates\AdminPages\_export_form.html.twig:21 - + export.level.full Full @@ -1008,7 +1008,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_export_form.html.twig:31 templates\AdminPages\_export_form.html.twig:31 - + export.include_children Include children elements in export @@ -1019,7 +1019,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_export_form.html.twig:39 templates\AdminPages\_export_form.html.twig:39 - + export.btn Export @@ -1038,7 +1038,7 @@ Sub elements will be moved upwards. templates\Parts\edit_part_info.html.twig:12 templates\Parts\show_part_info.html.twig:11 - + id.label ID @@ -1062,7 +1062,7 @@ Sub elements will be moved upwards. templates\AdminPages\EntityAdminBase.html.twig:101 templates\Parts\show_part_info.html.twig:248 - + createdAt Created At @@ -1080,7 +1080,7 @@ Sub elements will be moved upwards. templates\AdminPages\EntityAdminBase.html.twig:114 templates\Parts\show_part_info.html.twig:263 - + lastModified Last modified @@ -1090,7 +1090,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_info.html.twig:38 Part-DB1\templates\AdminPages\_info.html.twig:38 - + entity.info.parts_count Number of parts with this element @@ -1101,7 +1101,7 @@ Sub elements will be moved upwards. Part-DB1\templates\helper.twig:125 Part-DB1\templates\Parts\edit\_specifications.html.twig:6 - + specifications.property Parameter @@ -1111,7 +1111,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_parameters.html.twig:7 Part-DB1\templates\Parts\edit\_specifications.html.twig:7 - + specifications.symbol Symbol @@ -1121,7 +1121,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_parameters.html.twig:8 Part-DB1\templates\Parts\edit\_specifications.html.twig:8 - + specifications.value_min Min. @@ -1131,7 +1131,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_parameters.html.twig:9 Part-DB1\templates\Parts\edit\_specifications.html.twig:9 - + specifications.value_typ Typ. @@ -1141,7 +1141,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_parameters.html.twig:10 Part-DB1\templates\Parts\edit\_specifications.html.twig:10 - + specifications.value_max Max. @@ -1151,7 +1151,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_parameters.html.twig:11 Part-DB1\templates\Parts\edit\_specifications.html.twig:11 - + specifications.unit Unit @@ -1161,7 +1161,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_parameters.html.twig:12 Part-DB1\templates\Parts\edit\_specifications.html.twig:12 - + specifications.text Text @@ -1171,7 +1171,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_parameters.html.twig:13 Part-DB1\templates\Parts\edit\_specifications.html.twig:13 - + specifications.group Group @@ -1181,7 +1181,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_parameters.html.twig:26 Part-DB1\templates\Parts\edit\_specifications.html.twig:26 - + specification.create New Parameter @@ -1191,7 +1191,7 @@ Sub elements will be moved upwards. Part-DB1\templates\AdminPages\_parameters.html.twig:31 Part-DB1\templates\Parts\edit\_specifications.html.twig:31 - + parameter.delete.confirm Do you really want to delete this parameter? @@ -1201,7 +1201,7 @@ Sub elements will be moved upwards. Part-DB1\templates\attachment_list.html.twig:3 Part-DB1\templates\attachment_list.html.twig:3 - + attachment.list.title Attachments list @@ -1215,7 +1215,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LogSystem\_log_table.html.twig:8 Part-DB1\templates\Parts\lists\_parts_list.html.twig:6 - + part_list.loading.caption Loading @@ -1229,7 +1229,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LogSystem\_log_table.html.twig:9 Part-DB1\templates\Parts\lists\_parts_list.html.twig:7 - + part_list.loading.message This can take a moment. If this message do not disappear, try to reload the page. @@ -1240,7 +1240,7 @@ Sub elements will be moved upwards. Part-DB1\templates\base.html.twig:68 templates\base.html.twig:246 - + vendor.base.javascript_hint Please activate Javascript to use all features! @@ -1250,7 +1250,7 @@ Sub elements will be moved upwards. Part-DB1\templates\base.html.twig:73 Part-DB1\templates\base.html.twig:73 - + sidebar.big.toggle Show/Hide sidebar @@ -1261,7 +1261,7 @@ Sub elements will be moved upwards. Part-DB1\templates\base.html.twig:95 templates\base.html.twig:271 - + loading.caption Loading: @@ -1272,7 +1272,7 @@ Sub elements will be moved upwards. Part-DB1\templates\base.html.twig:96 templates\base.html.twig:272 - + loading.message This can take a while. If this messages stays for a long time, try to reload the page. @@ -1283,7 +1283,7 @@ Sub elements will be moved upwards. Part-DB1\templates\base.html.twig:101 templates\base.html.twig:277 - + loading.bar Loading... @@ -1294,7 +1294,7 @@ Sub elements will be moved upwards. Part-DB1\templates\base.html.twig:112 templates\base.html.twig:288 - + back_to_top Back to page's top @@ -1304,7 +1304,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Form\permissionLayout.html.twig:35 Part-DB1\templates\Form\permissionLayout.html.twig:35 - + permission.edit.permission Permissions @@ -1314,7 +1314,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Form\permissionLayout.html.twig:36 Part-DB1\templates\Form\permissionLayout.html.twig:36 - + permission.edit.value Value @@ -1324,7 +1324,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Form\permissionLayout.html.twig:53 Part-DB1\templates\Form\permissionLayout.html.twig:53 - + permission.legend.title Explanation of the states: @@ -1334,7 +1334,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Form\permissionLayout.html.twig:57 Part-DB1\templates\Form\permissionLayout.html.twig:57 - + permission.legend.disallow Forbidden @@ -1344,7 +1344,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Form\permissionLayout.html.twig:61 Part-DB1\templates\Form\permissionLayout.html.twig:61 - + permission.legend.allow Allowed @@ -1354,7 +1354,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Form\permissionLayout.html.twig:65 Part-DB1\templates\Form\permissionLayout.html.twig:65 - + permission.legend.inherit Inherit from (parent) group @@ -1364,7 +1364,7 @@ Sub elements will be moved upwards. Part-DB1\templates\helper.twig:3 Part-DB1\templates\helper.twig:3 - + bool.true True @@ -1374,7 +1374,7 @@ Sub elements will be moved upwards. Part-DB1\templates\helper.twig:5 Part-DB1\templates\helper.twig:5 - + bool.false False @@ -1384,7 +1384,7 @@ Sub elements will be moved upwards. Part-DB1\templates\helper.twig:92 Part-DB1\templates\helper.twig:87 - + Yes Yes @@ -1394,7 +1394,7 @@ Sub elements will be moved upwards. Part-DB1\templates\helper.twig:94 Part-DB1\templates\helper.twig:89 - + No No @@ -1403,7 +1403,7 @@ Sub elements will be moved upwards. Part-DB1\templates\helper.twig:126 - + specifications.value Value @@ -1414,7 +1414,7 @@ Sub elements will be moved upwards. Part-DB1\templates\homepage.html.twig:7 templates\homepage.html.twig:7 - + version.caption Version @@ -1425,7 +1425,7 @@ Sub elements will be moved upwards. Part-DB1\templates\homepage.html.twig:22 templates\homepage.html.twig:19 - + homepage.license License information @@ -1436,7 +1436,7 @@ Sub elements will be moved upwards. Part-DB1\templates\homepage.html.twig:31 templates\homepage.html.twig:28 - + homepage.github.caption Project page @@ -1447,9 +1447,9 @@ Sub elements will be moved upwards. Part-DB1\templates\homepage.html.twig:31 templates\homepage.html.twig:28 - + homepage.github.text - Source, downloads, bug reports, to-do-list etc. can be found on <a href="%href%" class="link-external" target="_blank">GitHub project page</a> + GitHub project page]]> @@ -1458,7 +1458,7 @@ Sub elements will be moved upwards. Part-DB1\templates\homepage.html.twig:32 templates\homepage.html.twig:29 - + homepage.help.caption Help @@ -1469,9 +1469,9 @@ Sub elements will be moved upwards. Part-DB1\templates\homepage.html.twig:32 templates\homepage.html.twig:29 - + homepage.help.text - Help and tips can be found in Wiki the <a href="%href%" class="link-external" target="_blank">GitHub page</a> + GitHub page]]> @@ -1480,7 +1480,7 @@ Sub elements will be moved upwards. Part-DB1\templates\homepage.html.twig:33 templates\homepage.html.twig:30 - + homepage.forum.caption Forum @@ -1491,7 +1491,7 @@ Sub elements will be moved upwards. Part-DB1\templates\homepage.html.twig:36 templates\homepage.html.twig:33 - + homepage.basedOn Based on the original Part-DB by @@ -1502,7 +1502,7 @@ Sub elements will be moved upwards. Part-DB1\templates\homepage.html.twig:39 templates\homepage.html.twig:36 - + homepage.others and others @@ -1513,7 +1513,7 @@ Sub elements will be moved upwards. Part-DB1\templates\homepage.html.twig:45 new - + homepage.last_activity Last activity @@ -1523,7 +1523,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LabelSystem\dialog.html.twig:3 Part-DB1\templates\LabelSystem\dialog.html.twig:6 - + label_generator.title Label generator @@ -1532,7 +1532,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LabelSystem\dialog.html.twig:16 - + label_generator.common Common @@ -1541,7 +1541,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LabelSystem\dialog.html.twig:20 - + label_generator.advanced Advanced @@ -1550,7 +1550,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LabelSystem\dialog.html.twig:24 - + label_generator.profiles Profiles @@ -1559,7 +1559,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LabelSystem\dialog.html.twig:58 - + label_generator.selected_profile Currently selected profile @@ -1568,7 +1568,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LabelSystem\dialog.html.twig:62 - + label_generator.edit_profile Edit profile @@ -1577,7 +1577,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LabelSystem\dialog.html.twig:75 - + label_generator.load_profile Load profile @@ -1586,7 +1586,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LabelSystem\dialog.html.twig:102 - + label_generator.download Download @@ -1596,7 +1596,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LabelSystem\dropdown_macro.html.twig:3 Part-DB1\templates\LabelSystem\dropdown_macro.html.twig:5 - + label_generator.label_btn Generate label @@ -1605,7 +1605,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LabelSystem\dropdown_macro.html.twig:20 - + label_generator.label_empty New empty label @@ -1614,7 +1614,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LabelSystem\Scanner\dialog.html.twig:3 - + label_scanner.title Label scanner @@ -1623,7 +1623,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LabelSystem\Scanner\dialog.html.twig:7 - + label_scanner.no_cam_found.title No webcam found @@ -1632,7 +1632,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LabelSystem\Scanner\dialog.html.twig:7 - + label_scanner.no_cam_found.text You need a webcam and give permission to use the scanner function. You can input the barcode code manually below. @@ -1641,7 +1641,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LabelSystem\Scanner\dialog.html.twig:27 - + label_scanner.source_select Select source @@ -1651,7 +1651,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LogSystem\log_list.html.twig:3 Part-DB1\templates\LogSystem\log_list.html.twig:3 - + log.list.title System log @@ -1662,7 +1662,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LogSystem\_log_table.html.twig:1 new - + log.undo.confirm_title Really undo change / revert to timestamp? @@ -1673,7 +1673,7 @@ Sub elements will be moved upwards. Part-DB1\templates\LogSystem\_log_table.html.twig:2 new - + log.undo.confirm_message Do you really want to undo the given change / reset the element to the given timestamp? @@ -1683,7 +1683,7 @@ Sub elements will be moved upwards. Part-DB1\templates\mail\base.html.twig:24 Part-DB1\templates\mail\base.html.twig:24 - + mail.footer.email_sent_by This email was sent automatically by @@ -1693,7 +1693,7 @@ Sub elements will be moved upwards. Part-DB1\templates\mail\base.html.twig:24 Part-DB1\templates\mail\base.html.twig:24 - + mail.footer.dont_reply Do not answer to this email. @@ -1703,7 +1703,7 @@ Sub elements will be moved upwards. Part-DB1\templates\mail\pw_reset.html.twig:6 Part-DB1\templates\mail\pw_reset.html.twig:6 - + email.hi %name% Hi %name% @@ -1713,7 +1713,7 @@ Sub elements will be moved upwards. Part-DB1\templates\mail\pw_reset.html.twig:7 Part-DB1\templates\mail\pw_reset.html.twig:7 - + email.pw_reset.message somebody (hopefully you) requested a reset of your password. If this request was not made by you, ignore this mail. @@ -1723,7 +1723,7 @@ Sub elements will be moved upwards. Part-DB1\templates\mail\pw_reset.html.twig:9 Part-DB1\templates\mail\pw_reset.html.twig:9 - + email.pw_reset.button Click here to reset password @@ -1733,9 +1733,9 @@ Sub elements will be moved upwards. Part-DB1\templates\mail\pw_reset.html.twig:11 Part-DB1\templates\mail\pw_reset.html.twig:11 - + email.pw_reset.fallback - If this does not work for you, go to <a href="%url%">%url%</a> and enter the following info + %url% and enter the following info]]> @@ -1743,7 +1743,7 @@ Sub elements will be moved upwards. Part-DB1\templates\mail\pw_reset.html.twig:16 Part-DB1\templates\mail\pw_reset.html.twig:16 - + email.pw_reset.username Username @@ -1753,7 +1753,7 @@ Sub elements will be moved upwards. Part-DB1\templates\mail\pw_reset.html.twig:19 Part-DB1\templates\mail\pw_reset.html.twig:19 - + email.pw_reset.token Token @@ -1763,9 +1763,9 @@ Sub elements will be moved upwards. Part-DB1\templates\mail\pw_reset.html.twig:24 Part-DB1\templates\mail\pw_reset.html.twig:24 - + email.pw_reset.valid_unit %date% - The reset token will be valid until <i>%date%</i>. + %date%.]]> @@ -1775,7 +1775,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:78 Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:58 - + orderdetail.delete Delete @@ -1785,7 +1785,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:39 Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:39 - + pricedetails.edit.min_qty Minimum discount quantity @@ -1795,7 +1795,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:40 Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:40 - + pricedetails.edit.price Price @@ -1805,7 +1805,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:41 Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:41 - + pricedetails.edit.price_qty for amount @@ -1815,7 +1815,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:54 Part-DB1\templates\Parts\edit\edit_form_styles.html.twig:54 - + pricedetail.create Add price @@ -1826,7 +1826,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\edit_part_info.html.twig:4 templates\Parts\edit_part_info.html.twig:4 - + part.edit.title Edit part @@ -1837,7 +1837,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\edit_part_info.html.twig:9 templates\Parts\edit_part_info.html.twig:9 - + part.edit.card_title Edit part @@ -1847,7 +1847,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\edit_part_info.html.twig:22 Part-DB1\templates\Parts\edit\edit_part_info.html.twig:22 - + part.edit.tab.common Common @@ -1857,7 +1857,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\edit_part_info.html.twig:28 Part-DB1\templates\Parts\edit\edit_part_info.html.twig:28 - + part.edit.tab.manufacturer Manufacturer @@ -1867,7 +1867,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\edit_part_info.html.twig:34 Part-DB1\templates\Parts\edit\edit_part_info.html.twig:34 - + part.edit.tab.advanced Advanced @@ -1877,7 +1877,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 - + part.edit.tab.part_lots Stocks @@ -1887,7 +1887,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\edit_part_info.html.twig:46 Part-DB1\templates\Parts\edit\edit_part_info.html.twig:46 - + part.edit.tab.attachments Attachments @@ -1897,7 +1897,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\edit_part_info.html.twig:52 Part-DB1\templates\Parts\edit\edit_part_info.html.twig:52 - + part.edit.tab.orderdetails Purchase informations @@ -1906,7 +1906,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\edit_part_info.html.twig:58 - + part.edit.tab.specifications Parameters @@ -1916,7 +1916,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\edit_part_info.html.twig:64 Part-DB1\templates\Parts\edit\edit_part_info.html.twig:58 - + part.edit.tab.comment Notes @@ -1927,7 +1927,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\new_part.html.twig:8 templates\Parts\new_part.html.twig:8 - + part.new.card_title Create new part @@ -1937,7 +1937,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\_lots.html.twig:5 Part-DB1\templates\Parts\edit\_lots.html.twig:5 - + part_lot.delete Delete @@ -1947,7 +1947,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\_lots.html.twig:28 Part-DB1\templates\Parts\edit\_lots.html.twig:28 - + part_lot.create Add stock @@ -1957,7 +1957,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\_orderdetails.html.twig:13 Part-DB1\templates\Parts\edit\_orderdetails.html.twig:13 - + orderdetail.create Add distributor @@ -1967,7 +1967,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\_orderdetails.html.twig:18 Part-DB1\templates\Parts\edit\_orderdetails.html.twig:18 - + pricedetails.edit.delete.confirm Do you really want to delete this price? This can not be undone. @@ -1977,7 +1977,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\edit\_orderdetails.html.twig:62 Part-DB1\templates\Parts\edit\_orderdetails.html.twig:61 - + orderdetails.edit.delete.confirm Do you really want to delete this distributor info? This can not be undone! @@ -1991,7 +1991,7 @@ Sub elements will be moved upwards. templates\Parts\show_part_info.html.twig:4 templates\Parts\show_part_info.html.twig:9 - + part.info.title Detail info for part @@ -2001,7 +2001,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\show_part_info.html.twig:47 Part-DB1\templates\Parts\info\show_part_info.html.twig:47 - + part.part_lots.label Stocks @@ -2016,7 +2016,7 @@ Sub elements will be moved upwards. templates\Parts\show_part_info.html.twig:74 src\Form\PartType.php:86 - + comment.label Notes @@ -2025,7 +2025,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\show_part_info.html.twig:64 - + part.info.specifications Parameters @@ -2036,7 +2036,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\show_part_info.html.twig:64 templates\Parts\show_part_info.html.twig:82 - + attachment.labelp Attachments @@ -2047,7 +2047,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\show_part_info.html.twig:71 templates\Parts\show_part_info.html.twig:88 - + vendor.partinfo.shopping_infos Shopping informations @@ -2058,7 +2058,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\show_part_info.html.twig:78 templates\Parts\show_part_info.html.twig:94 - + vendor.partinfo.history History @@ -2077,7 +2077,7 @@ Sub elements will be moved upwards. templates\base.html.twig:231 templates\Parts\show_part_info.html.twig:100 - + tools.label Tools @@ -2087,7 +2087,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\show_part_info.html.twig:103 Part-DB1\templates\Parts\info\show_part_info.html.twig:90 - + extended_info.label Extended infos @@ -2097,7 +2097,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_attachments_info.html.twig:7 Part-DB1\templates\Parts\info\_attachments_info.html.twig:7 - + attachment.name Name @@ -2107,7 +2107,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_attachments_info.html.twig:8 Part-DB1\templates\Parts\info\_attachments_info.html.twig:8 - + attachment.attachment_type Attachment Type @@ -2117,7 +2117,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_attachments_info.html.twig:9 Part-DB1\templates\Parts\info\_attachments_info.html.twig:9 - + attachment.file_name File name @@ -2127,7 +2127,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_attachments_info.html.twig:10 Part-DB1\templates\Parts\info\_attachments_info.html.twig:10 - + attachment.file_size File size @@ -2136,7 +2136,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_attachments_info.html.twig:54 - + attachment.preview Preview picture @@ -2146,7 +2146,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_attachments_info.html.twig:67 Part-DB1\templates\Parts\info\_attachments_info.html.twig:50 - + attachment.download Download @@ -2157,7 +2157,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_extended_infos.html.twig:11 new - + user.creating_user User who created this part @@ -2171,7 +2171,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_extended_infos.html.twig:28 Part-DB1\templates\Parts\info\_extended_infos.html.twig:50 - + Unknown Unknown @@ -2184,7 +2184,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_extended_infos.html.twig:30 new - + accessDenied Access Denied @@ -2195,7 +2195,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_extended_infos.html.twig:26 new - + user.last_editing_user User who edited this part last @@ -2205,7 +2205,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_extended_infos.html.twig:41 Part-DB1\templates\Parts\info\_extended_infos.html.twig:41 - + part.isFavorite Favorite @@ -2215,7 +2215,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_extended_infos.html.twig:46 Part-DB1\templates\Parts\info\_extended_infos.html.twig:46 - + part.minOrderAmount Minimum order amount @@ -2232,7 +2232,7 @@ Sub elements will be moved upwards. templates\Parts\show_part_info.html.twig:24 src\Form\PartType.php:80 - + manufacturer.label Manufacturer @@ -2244,7 +2244,7 @@ Sub elements will be moved upwards. templates\base.html.twig:54 src\Form\PartType.php:62 - + name.label Name @@ -2255,7 +2255,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_main_infos.html.twig:27 new - + part.back_to_info Back to current version @@ -2270,7 +2270,7 @@ Sub elements will be moved upwards. templates\Parts\show_part_info.html.twig:31 src\Form\PartType.php:65 - + description.label Description @@ -2287,7 +2287,7 @@ Sub elements will be moved upwards. templates\Parts\show_part_info.html.twig:32 src\Form\PartType.php:74 - + category.label Category @@ -2299,7 +2299,7 @@ Sub elements will be moved upwards. templates\Parts\show_part_info.html.twig:42 src\Form\PartType.php:69 - + instock.label Instock @@ -2311,7 +2311,7 @@ Sub elements will be moved upwards. templates\Parts\show_part_info.html.twig:44 src\Form\PartType.php:72 - + mininstock.label Minimum Instock @@ -2327,7 +2327,7 @@ Sub elements will be moved upwards. templates\base.html.twig:73 templates\Parts\show_part_info.html.twig:47 - + footprint.label Footprint @@ -2340,7 +2340,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_main_infos.html.twig:60 templates\Parts\show_part_info.html.twig:51 - + part.avg_price.label Average Price @@ -2350,7 +2350,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_order_infos.html.twig:5 Part-DB1\templates\Parts\info\_order_infos.html.twig:5 - + part.supplier.name Name @@ -2360,7 +2360,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_order_infos.html.twig:6 Part-DB1\templates\Parts\info\_order_infos.html.twig:6 - + part.supplier.partnr Partnr. @@ -2370,7 +2370,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_order_infos.html.twig:28 Part-DB1\templates\Parts\info\_order_infos.html.twig:28 - + part.order.minamount Minimum amount @@ -2380,7 +2380,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_order_infos.html.twig:29 Part-DB1\templates\Parts\info\_order_infos.html.twig:29 - + part.order.price Price @@ -2390,7 +2390,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_order_infos.html.twig:31 Part-DB1\templates\Parts\info\_order_infos.html.twig:31 - + part.order.single_price Unit Price @@ -2400,7 +2400,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_order_infos.html.twig:71 Part-DB1\templates\Parts\info\_order_infos.html.twig:71 - + edit.caption_short Edit @@ -2410,7 +2410,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_order_infos.html.twig:72 Part-DB1\templates\Parts\info\_order_infos.html.twig:72 - + delete.caption Delete @@ -2420,7 +2420,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_part_lots.html.twig:7 Part-DB1\templates\Parts\info\_part_lots.html.twig:6 - + part_lots.description Description @@ -2430,7 +2430,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_part_lots.html.twig:8 Part-DB1\templates\Parts\info\_part_lots.html.twig:7 - + part_lots.storage_location Storage location @@ -2440,7 +2440,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_part_lots.html.twig:9 Part-DB1\templates\Parts\info\_part_lots.html.twig:8 - + part_lots.amount Amount @@ -2450,7 +2450,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_part_lots.html.twig:24 Part-DB1\templates\Parts\info\_part_lots.html.twig:22 - + part_lots.location_unknown Storage location unknown @@ -2460,7 +2460,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_part_lots.html.twig:31 Part-DB1\templates\Parts\info\_part_lots.html.twig:29 - + part_lots.instock_unknown Amount unknown @@ -2470,7 +2470,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_part_lots.html.twig:40 Part-DB1\templates\Parts\info\_part_lots.html.twig:38 - + part_lots.expiration_date Expiration date @@ -2480,7 +2480,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_part_lots.html.twig:48 Part-DB1\templates\Parts\info\_part_lots.html.twig:46 - + part_lots.is_expired Expired @@ -2490,7 +2490,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_part_lots.html.twig:55 Part-DB1\templates\Parts\info\_part_lots.html.twig:53 - + part_lots.need_refill Needs refill @@ -2500,7 +2500,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_picture.html.twig:15 Part-DB1\templates\Parts\info\_picture.html.twig:15 - + part.info.prev_picture Previous picture @@ -2510,7 +2510,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_picture.html.twig:19 Part-DB1\templates\Parts\info\_picture.html.twig:19 - + part.info.next_picture Next picture @@ -2520,7 +2520,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_sidebar.html.twig:21 Part-DB1\templates\Parts\info\_sidebar.html.twig:21 - + part.mass.tooltip Mass @@ -2530,7 +2530,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_sidebar.html.twig:30 Part-DB1\templates\Parts\info\_sidebar.html.twig:30 - + part.needs_review.badge Needs review @@ -2540,7 +2540,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_sidebar.html.twig:39 Part-DB1\templates\Parts\info\_sidebar.html.twig:39 - + part.favorite.badge Favorite @@ -2550,7 +2550,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_sidebar.html.twig:47 Part-DB1\templates\Parts\info\_sidebar.html.twig:47 - + part.obsolete.badge No longer available @@ -2559,7 +2559,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_specifications.html.twig:10 - + parameters.extracted_from_description Automatically extracted from description @@ -2568,7 +2568,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_specifications.html.twig:15 - + parameters.auto_extracted_from_comment Automatically extracted from notes @@ -2579,7 +2579,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_tools.html.twig:4 templates\Parts\show_part_info.html.twig:125 - + part.edit.btn Edit part @@ -2590,7 +2590,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_tools.html.twig:14 templates\Parts\show_part_info.html.twig:135 - + part.clone.btn Clone part @@ -2601,7 +2601,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\lists\_action_bar.html.twig:4 templates\Parts\show_part_info.html.twig:143 - + part.create.btn Create new part @@ -2611,7 +2611,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_tools.html.twig:31 Part-DB1\templates\Parts\info\_tools.html.twig:29 - + part.delete.confirm_title Do you really want to delete this part? @@ -2621,7 +2621,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_tools.html.twig:32 Part-DB1\templates\Parts\info\_tools.html.twig:30 - + part.delete.message This part and any associated information (like attachments, price informations, etc.) will be deleted. This can not be undone! @@ -2631,7 +2631,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\info\_tools.html.twig:39 Part-DB1\templates\Parts\info\_tools.html.twig:37 - + part.delete Delete part @@ -2641,7 +2641,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\lists\all_list.html.twig:4 Part-DB1\templates\Parts\lists\all_list.html.twig:4 - + parts_list.all.title All parts @@ -2651,7 +2651,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\lists\category_list.html.twig:4 Part-DB1\templates\Parts\lists\category_list.html.twig:4 - + parts_list.category.title Parts with category @@ -2661,7 +2661,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\lists\footprint_list.html.twig:4 Part-DB1\templates\Parts\lists\footprint_list.html.twig:4 - + parts_list.footprint.title Parts with footprint @@ -2671,7 +2671,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\lists\manufacturer_list.html.twig:4 Part-DB1\templates\Parts\lists\manufacturer_list.html.twig:4 - + parts_list.manufacturer.title Parts with manufacturer @@ -2681,7 +2681,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\lists\search_list.html.twig:4 Part-DB1\templates\Parts\lists\search_list.html.twig:4 - + parts_list.search.title Search Parts @@ -2691,7 +2691,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\lists\store_location_list.html.twig:4 Part-DB1\templates\Parts\lists\store_location_list.html.twig:4 - + parts_list.storelocation.title Parts with store locations @@ -2701,7 +2701,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\lists\supplier_list.html.twig:4 Part-DB1\templates\Parts\lists\supplier_list.html.twig:4 - + parts_list.supplier.title Parts with supplier @@ -2711,7 +2711,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\lists\tags_list.html.twig:4 Part-DB1\templates\Parts\lists\tags_list.html.twig:4 - + parts_list.tags.title Parts with tag @@ -2721,7 +2721,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\lists\_info_card.html.twig:22 Part-DB1\templates\Parts\lists\_info_card.html.twig:17 - + entity.info.common.tab Common @@ -2731,7 +2731,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\lists\_info_card.html.twig:26 Part-DB1\templates\Parts\lists\_info_card.html.twig:20 - + entity.info.statistics.tab Statistics @@ -2740,7 +2740,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\lists\_info_card.html.twig:31 - + entity.info.attachments.tab Attachments @@ -2749,7 +2749,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\lists\_info_card.html.twig:37 - + entity.info.parameters.tab Parameters @@ -2759,7 +2759,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\lists\_info_card.html.twig:54 Part-DB1\templates\Parts\lists\_info_card.html.twig:30 - + entity.info.name Name @@ -2771,7 +2771,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\lists\_info_card.html.twig:34 Part-DB1\templates\Parts\lists\_info_card.html.twig:67 - + entity.info.parent Parent @@ -2781,7 +2781,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\lists\_info_card.html.twig:70 Part-DB1\templates\Parts\lists\_info_card.html.twig:46 - + entity.edit.btn Edit @@ -2791,7 +2791,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Parts\lists\_info_card.html.twig:92 Part-DB1\templates\Parts\lists\_info_card.html.twig:63 - + entity.info.children_count Count of children elements @@ -2803,7 +2803,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\2fa_base_form.html.twig:3 Part-DB1\templates\security\2fa_base_form.html.twig:5 - + tfa.check.title Two-factor authentication needed @@ -2813,7 +2813,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\2fa_base_form.html.twig:39 Part-DB1\templates\security\2fa_base_form.html.twig:39 - + tfa.code.trusted_pc This is a trusted computer (if this is enabled, no further two-factor queries are performed on this computer) @@ -2825,7 +2825,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\2fa_base_form.html.twig:52 Part-DB1\templates\security\login.html.twig:58 - + login.btn Login @@ -2839,7 +2839,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\U2F\u2f_login.html.twig:13 Part-DB1\templates\_navbar.html.twig:40 - + user.logout Logout @@ -2849,7 +2849,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\2fa_form.html.twig:6 Part-DB1\templates\security\2fa_form.html.twig:6 - + tfa.check.code.label Authenticator app code @@ -2859,7 +2859,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\2fa_form.html.twig:10 Part-DB1\templates\security\2fa_form.html.twig:10 - + tfa.check.code.help Enter the 6-digit code from your Authenticator App or one of your backup codes if the Authenticator is not available. @@ -2870,7 +2870,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\login.html.twig:3 templates\security\login.html.twig:3 - + login.title Login @@ -2881,7 +2881,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\login.html.twig:7 templates\security\login.html.twig:7 - + login.card_title Login @@ -2892,7 +2892,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\login.html.twig:31 templates\security\login.html.twig:31 - + login.username.label Username @@ -2903,7 +2903,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\login.html.twig:34 templates\security\login.html.twig:34 - + login.username.placeholder Username @@ -2914,7 +2914,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\login.html.twig:38 templates\security\login.html.twig:38 - + login.password.label Password @@ -2925,7 +2925,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\login.html.twig:40 templates\security\login.html.twig:40 - + login.password.placeholder Password @@ -2936,7 +2936,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\login.html.twig:50 templates\security\login.html.twig:50 - + login.rememberme Remember me (should not be used on shared computers) @@ -2946,7 +2946,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\login.html.twig:64 Part-DB1\templates\security\login.html.twig:64 - + pw_reset.password_forget Forgot username/password? @@ -2956,7 +2956,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\pw_reset_new_pw.html.twig:5 Part-DB1\templates\security\pw_reset_new_pw.html.twig:5 - + pw_reset.new_pw.header.title Set new password @@ -2966,7 +2966,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\pw_reset_request.html.twig:5 Part-DB1\templates\security\pw_reset_request.html.twig:5 - + pw_reset.request.header.title Request a new password @@ -2978,7 +2978,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\U2F\u2f_login.html.twig:7 Part-DB1\templates\security\U2F\u2f_register.html.twig:10 - + tfa_u2f.http_warning You are accessing this page using the insecure HTTP method, so U2F will most likely not work (Bad Request error message). Ask an administrator to set up the secure HTTPS method if you want to use security keys. @@ -2990,7 +2990,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\U2F\u2f_login.html.twig:10 Part-DB1\templates\security\U2F\u2f_register.html.twig:22 - + r_u2f_two_factor.pressbutton Please plug in your security key and press its button! @@ -3000,7 +3000,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\U2F\u2f_register.html.twig:3 Part-DB1\templates\security\U2F\u2f_register.html.twig:3 - + tfa_u2f.add_key.title Add security key @@ -3012,7 +3012,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\U2F\u2f_register.html.twig:6 Part-DB1\templates\Users\_2fa_settings.html.twig:111 - + tfa_u2f.explanation With the help of a U2F/FIDO compatible security key (e.g. YubiKey or NitroKey), user-friendly and secure two-factor authentication can be achieved. The security keys can be registered here, and if two-factor verification is required, the key only needs to be inserted via USB or typed against the device via NFC. @@ -3022,7 +3022,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\U2F\u2f_register.html.twig:7 Part-DB1\templates\security\U2F\u2f_register.html.twig:7 - + tfa_u2f.add_key.backup_hint To ensure access even if the key is lost, it is recommended to register a second key as backup and store it in a safe place! @@ -3032,7 +3032,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\U2F\u2f_register.html.twig:16 Part-DB1\templates\security\U2F\u2f_register.html.twig:16 - + r_u2f_two_factor.name Shown key name (e.g. Backup) @@ -3042,7 +3042,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\U2F\u2f_register.html.twig:19 Part-DB1\templates\security\U2F\u2f_register.html.twig:19 - + tfa_u2f.add_key.add_button Add security key @@ -3052,7 +3052,7 @@ Sub elements will be moved upwards. Part-DB1\templates\security\U2F\u2f_register.html.twig:27 Part-DB1\templates\security\U2F\u2f_register.html.twig:27 - + tfa_u2f.add_key.back_to_settings Back to settings @@ -3065,7 +3065,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:8 new - + statistics.title Statistics @@ -3076,7 +3076,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:14 new - + statistics.parts Parts @@ -3087,7 +3087,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:19 new - + statistics.data_structures Data structures @@ -3098,7 +3098,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:24 new - + statistics.attachments Attachments @@ -3113,7 +3113,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:104 new - + statistics.property Property @@ -3128,7 +3128,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:105 new - + statistics.value Value @@ -3139,7 +3139,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:40 new - + statistics.distinct_parts_count Number of distinct parts @@ -3150,7 +3150,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:44 new - + statistics.parts_instock_sum Sum of all part instocks @@ -3161,7 +3161,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:48 new - + statistics.parts_with_price Number of parts with price information @@ -3172,7 +3172,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:65 new - + statistics.categories_count Number of categories @@ -3183,7 +3183,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:69 new - + statistics.footprints_count Number of footprints @@ -3194,7 +3194,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:73 new - + statistics.manufacturers_count Number of manufacturers @@ -3205,7 +3205,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:77 new - + statistics.storelocations_count Number of storelocations @@ -3216,7 +3216,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:81 new - + statistics.suppliers_count Number of suppliers @@ -3227,7 +3227,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:85 new - + statistics.currencies_count Number of currencies @@ -3238,7 +3238,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:89 new - + statistics.measurement_units_count Number of measurement units @@ -3249,7 +3249,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:93 new - + statistics.devices_count Number of projects @@ -3260,7 +3260,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:110 new - + statistics.attachment_types_count Number of attachment types @@ -3271,7 +3271,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:114 new - + statistics.all_attachments_count Number of all attachments @@ -3282,7 +3282,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:118 new - + statistics.user_uploaded_attachments_count Number of user uploaded attachments @@ -3293,7 +3293,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:122 new - + statistics.private_attachments_count Number of private attachments @@ -3304,7 +3304,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Statistics\statistics.html.twig:126 new - + statistics.external_attachments_count Number of external attachments (URL) @@ -3316,7 +3316,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\backup_codes.html.twig:3 Part-DB1\templates\Users\backup_codes.html.twig:9 - + tfa_backup.codes.title Backup codes @@ -3326,7 +3326,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\backup_codes.html.twig:12 Part-DB1\templates\Users\backup_codes.html.twig:12 - + tfa_backup.codes.explanation Print out these codes and keep them in a safe place! @@ -3336,7 +3336,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\backup_codes.html.twig:13 Part-DB1\templates\Users\backup_codes.html.twig:13 - + tfa_backup.codes.help If you no longer have access to your device with the Authenticator App (lost smartphone, data loss, etc.) you can use one of these codes to access your account and possibly set up a new Authenticator App. Each of these codes can be used once, it is recommended to delete used codes. Anyone with access to these codes can potentially access your account, so keep them in a safe place. @@ -3346,7 +3346,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\backup_codes.html.twig:16 Part-DB1\templates\Users\backup_codes.html.twig:16 - + tfa_backup.username Username @@ -3356,7 +3356,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\backup_codes.html.twig:29 Part-DB1\templates\Users\backup_codes.html.twig:29 - + tfa_backup.codes.page_generated_on Page generated on %date% @@ -3366,7 +3366,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\backup_codes.html.twig:32 Part-DB1\templates\Users\backup_codes.html.twig:32 - + tfa_backup.codes.print Print @@ -3376,7 +3376,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\backup_codes.html.twig:35 Part-DB1\templates\Users\backup_codes.html.twig:35 - + tfa_backup.codes.copy_clipboard Copy to clipboard @@ -3393,7 +3393,7 @@ Sub elements will be moved upwards. templates\Users\user_info.html.twig:3 templates\Users\user_info.html.twig:6 - + user.info.label User informations @@ -3407,7 +3407,7 @@ Sub elements will be moved upwards. templates\Users\user_info.html.twig:18 src\Form\UserSettingsType.php:32 - + user.firstName.label First name @@ -3421,7 +3421,7 @@ Sub elements will be moved upwards. templates\Users\user_info.html.twig:24 src\Form\UserSettingsType.php:35 - + user.lastName.label Last name @@ -3435,7 +3435,7 @@ Sub elements will be moved upwards. templates\Users\user_info.html.twig:30 src\Form\UserSettingsType.php:41 - + user.email.label Email @@ -3449,7 +3449,7 @@ Sub elements will be moved upwards. templates\Users\user_info.html.twig:37 src\Form\UserSettingsType.php:38 - + user.department.label Department @@ -3463,7 +3463,7 @@ Sub elements will be moved upwards. templates\Users\user_info.html.twig:47 src\Form\UserSettingsType.php:30 - + user.username.label User name @@ -3476,7 +3476,7 @@ Sub elements will be moved upwards. Part-DB1\src\Services\ElementTypeNameGenerator.php:93 templates\Users\user_info.html.twig:53 - + group.label Group: @@ -3486,7 +3486,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\user_info.html.twig:67 Part-DB1\templates\Users\user_info.html.twig:67 - + user.permissions Permissions @@ -3503,7 +3503,7 @@ Sub elements will be moved upwards. templates\Users\user_settings.html.twig:3 templates\Users\user_settings.html.twig:6 - + user.settings.label User settings @@ -3514,7 +3514,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\user_settings.html.twig:18 templates\Users\user_settings.html.twig:14 - + user_settings.data.label Personal data @@ -3525,7 +3525,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\user_settings.html.twig:22 templates\Users\user_settings.html.twig:18 - + user_settings.configuration.label Configuration @@ -3536,7 +3536,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\user_settings.html.twig:55 templates\Users\user_settings.html.twig:48 - + user.settings.change_pw Change password @@ -3546,7 +3546,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\_2fa_settings.html.twig:6 Part-DB1\templates\Users\_2fa_settings.html.twig:6 - + user.settings.2fa_settings Two-Factor Authentication @@ -3556,7 +3556,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\_2fa_settings.html.twig:13 Part-DB1\templates\Users\_2fa_settings.html.twig:13 - + tfa.settings.google.tab Authenticator app @@ -3566,7 +3566,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\_2fa_settings.html.twig:17 Part-DB1\templates\Users\_2fa_settings.html.twig:17 - + tfa.settings.bakup.tab Backup codes @@ -3576,7 +3576,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\_2fa_settings.html.twig:21 Part-DB1\templates\Users\_2fa_settings.html.twig:21 - + tfa.settings.u2f.tab Security keys (U2F) @@ -3586,7 +3586,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\_2fa_settings.html.twig:25 Part-DB1\templates\Users\_2fa_settings.html.twig:25 - + tfa.settings.trustedDevices.tab Trusted devices @@ -3596,7 +3596,7 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\_2fa_settings.html.twig:33 Part-DB1\templates\Users\_2fa_settings.html.twig:33 - + tfa_google.disable.confirm_title Do you really want to disable the Authenticator App? @@ -3606,10 +3606,10 @@ Sub elements will be moved upwards. Part-DB1\templates\Users\_2fa_settings.html.twig:33 Part-DB1\templates\Users\_2fa_settings.html.twig:33 - + tfa_google.disable.confirm_message - If you disable the Authenticator App, all backup codes will be deleted, so you may need to reprint them.<br> -Also note that without two-factor authentication your account is not as well protected against attackers! + +Also note that without two-factor authentication your account is not as well protected against attackers!]]> @@ -3617,7 +3617,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:39 Part-DB1\templates\Users\_2fa_settings.html.twig:39 - + tfa_google.disabled_message Authenticator app deactivated! @@ -3627,9 +3627,9 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:48 Part-DB1\templates\Users\_2fa_settings.html.twig:48 - + tfa_google.step.download - Download an authenticator app (e.g. <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Google Authenticator</a> oder <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp">FreeOTP Authenticator</a>) + Google Authenticator oder FreeOTP Authenticator)]]> @@ -3637,7 +3637,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:49 Part-DB1\templates\Users\_2fa_settings.html.twig:49 - + tfa_google.step.scan Scan the adjoining QR Code with the app or enter the data manually @@ -3647,7 +3647,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:50 Part-DB1\templates\Users\_2fa_settings.html.twig:50 - + tfa_google.step.input_code Enter the generated code in the field below and confirm @@ -3657,7 +3657,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:51 Part-DB1\templates\Users\_2fa_settings.html.twig:51 - + tfa_google.step.download_backup Print out your backup codes and store them in a safe place @@ -3667,7 +3667,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:58 Part-DB1\templates\Users\_2fa_settings.html.twig:58 - + tfa_google.manual_setup Manual setup @@ -3677,7 +3677,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:62 Part-DB1\templates\Users\_2fa_settings.html.twig:62 - + tfa_google.manual_setup.type Type @@ -3687,7 +3687,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:63 Part-DB1\templates\Users\_2fa_settings.html.twig:63 - + tfa_google.manual_setup.username Username @@ -3697,7 +3697,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:64 Part-DB1\templates\Users\_2fa_settings.html.twig:64 - + tfa_google.manual_setup.secret Secret @@ -3707,7 +3707,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:65 Part-DB1\templates\Users\_2fa_settings.html.twig:65 - + tfa_google.manual_setup.digit_count Digit count @@ -3717,7 +3717,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:74 Part-DB1\templates\Users\_2fa_settings.html.twig:74 - + tfa_google.enabled_message Authenticator App enabled @@ -3727,7 +3727,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:83 Part-DB1\templates\Users\_2fa_settings.html.twig:83 - + tfa_backup.disabled Backup codes disabled. Setup authenticator app to enable backup codes. @@ -3739,7 +3739,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:84 Part-DB1\templates\Users\_2fa_settings.html.twig:92 - + tfa_backup.explanation You can use these backup codes to access your account even if you lose the device with the Authenticator App. Print out the codes and keep them in a safe place. @@ -3749,7 +3749,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:88 Part-DB1\templates\Users\_2fa_settings.html.twig:88 - + tfa_backup.reset_codes.confirm_title Really reset codes? @@ -3759,7 +3759,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:88 Part-DB1\templates\Users\_2fa_settings.html.twig:88 - + tfa_backup.reset_codes.confirm_message This will delete all previous codes and generate a set of new codes. This cannot be undone. Remember to print out the new codes and store them in a safe place! @@ -3769,7 +3769,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:91 Part-DB1\templates\Users\_2fa_settings.html.twig:91 - + tfa_backup.enabled Backup codes enabled @@ -3779,7 +3779,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:99 Part-DB1\templates\Users\_2fa_settings.html.twig:99 - + tfa_backup.show_codes Show backup codes @@ -3789,7 +3789,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:114 Part-DB1\templates\Users\_2fa_settings.html.twig:114 - + tfa_u2f.table_caption Registered security keys @@ -3799,7 +3799,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:115 Part-DB1\templates\Users\_2fa_settings.html.twig:115 - + tfa_u2f.delete_u2f.confirm_title Really remove this security key? @@ -3809,7 +3809,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:116 Part-DB1\templates\Users\_2fa_settings.html.twig:116 - + tfa_u2f.delete_u2f.confirm_message If you remove this key, then no more login with this key will be possible. If no security keys remain, two-factor authentication will be disabled. @@ -3819,7 +3819,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:123 Part-DB1\templates\Users\_2fa_settings.html.twig:123 - + tfa_u2f.keys.name Key name @@ -3829,7 +3829,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:124 Part-DB1\templates\Users\_2fa_settings.html.twig:124 - + tfa_u2f.keys.added_date Registration date @@ -3839,7 +3839,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:134 Part-DB1\templates\Users\_2fa_settings.html.twig:134 - + tfa_u2f.key_delete Delete key @@ -3849,7 +3849,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:141 Part-DB1\templates\Users\_2fa_settings.html.twig:141 - + tfa_u2f.no_keys_registered No keys registered yet. @@ -3859,7 +3859,7 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:144 Part-DB1\templates\Users\_2fa_settings.html.twig:144 - + tfa_u2f.add_new_key Register new security key @@ -3869,10 +3869,10 @@ Also note that without two-factor authentication your account is not as well pro Part-DB1\templates\Users\_2fa_settings.html.twig:148 Part-DB1\templates\Users\_2fa_settings.html.twig:148 - + tfa_trustedDevices.explanation - When checking the second factor, the current computer can be marked as trustworthy, so no more two-factor checks on this computer are needed. -If you have done this incorrectly or if a computer is no longer trusted, you can reset the status of <i>all </i>computers here. + all computers here.]]> @@ -3880,7 +3880,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\templates\Users\_2fa_settings.html.twig:149 Part-DB1\templates\Users\_2fa_settings.html.twig:149 - + tfa_trustedDevices.invalidate.confirm_title Really remove all trusted computers? @@ -3890,7 +3890,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\templates\Users\_2fa_settings.html.twig:150 Part-DB1\templates\Users\_2fa_settings.html.twig:150 - + tfa_trustedDevices.invalidate.confirm_message You will have to perform two-factor authentication again on all computers. Make sure you have your two-factor device at hand. @@ -3900,7 +3900,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\templates\Users\_2fa_settings.html.twig:154 Part-DB1\templates\Users\_2fa_settings.html.twig:154 - + tfa_trustedDevices.invalidate.btn Reset trusted devices @@ -3911,7 +3911,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\templates\_navbar.html.twig:4 templates\base.html.twig:29 - + sidebar.toggle Toggle Sidebar @@ -3920,7 +3920,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\templates\_navbar.html.twig:22 - + navbar.scanner.link Scanner @@ -3931,7 +3931,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\templates\_navbar.html.twig:36 templates\base.html.twig:97 - + user.loggedin.label Logged in as @@ -3942,7 +3942,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\templates\_navbar.html.twig:42 templates\base.html.twig:103 - + user.login Login @@ -3952,7 +3952,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\templates\_navbar.html.twig:50 Part-DB1\templates\_navbar.html.twig:48 - + ui.toggle_darkmode Darkmode @@ -3966,7 +3966,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can templates\base.html.twig:106 src\Form\UserSettingsType.php:44 - + user.language_select Switch Language @@ -3977,7 +3977,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\templates\_navbar_search.html.twig:4 templates\base.html.twig:49 - + search.options.label Search options @@ -3986,7 +3986,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\templates\_navbar_search.html.twig:23 - + tags.label Tags @@ -4001,7 +4001,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can templates\Parts\show_part_info.html.twig:36 src\Form\PartType.php:77 - + storelocation.label Store location @@ -4012,7 +4012,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\templates\_navbar_search.html.twig:31 templates\base.html.twig:65 - + ordernumber.label.short supplier partnr. @@ -4025,7 +4025,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\ElementTypeNameGenerator.php:89 templates\base.html.twig:67 - + supplier.label Supplier @@ -4036,7 +4036,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\templates\_navbar_search.html.twig:52 templates\base.html.twig:75 - + search.deactivateBarcode Deact. Barcode @@ -4047,7 +4047,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\templates\_navbar_search.html.twig:56 templates\base.html.twig:77 - + search.regexmatching Reg.Ex. Matching @@ -4057,7 +4057,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\templates\_navbar_search.html.twig:68 Part-DB1\templates\_navbar_search.html.twig:62 - + search.submit Go! @@ -4073,7 +4073,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can templates\base.html.twig:202 templates\base.html.twig:230 - + project.labelp Projects @@ -4086,7 +4086,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can templates\base.html.twig:192 templates\base.html.twig:220 - + actions Actions @@ -4099,7 +4099,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can templates\base.html.twig:196 templates\base.html.twig:224 - + datasource Data source @@ -4112,7 +4112,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can templates\base.html.twig:200 templates\base.html.twig:228 - + manufacturer.labelp Manufacturers @@ -4125,7 +4125,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can templates\base.html.twig:201 templates\base.html.twig:229 - + supplier.labelp Suppliers @@ -4141,7 +4141,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\PartController.php:173 Part-DB1\src\Controller\PartController.php:268 - + attachment.download_failed Download of the external attachment failed. @@ -4151,7 +4151,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\AdminPages\BaseAdminController.php:222 Part-DB1\src\Controller\AdminPages\BaseAdminController.php:190 - + entity.edit_flash Changes saved successful. @@ -4161,7 +4161,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\AdminPages\BaseAdminController.php:231 Part-DB1\src\Controller\AdminPages\BaseAdminController.php:196 - + entity.edit_flash.invalid Can not save changed. Please check your input! @@ -4171,7 +4171,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\AdminPages\BaseAdminController.php:302 Part-DB1\src\Controller\AdminPages\BaseAdminController.php:252 - + entity.created_flash Element created. @@ -4181,7 +4181,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\AdminPages\BaseAdminController.php:308 Part-DB1\src\Controller\AdminPages\BaseAdminController.php:258 - + entity.created_flash.invalid Could not create element. Please check your input! @@ -4192,7 +4192,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\AdminPages\BaseAdminController.php:352 src\Controller\BaseAdminController.php:154 - + attachment_type.deleted Element deleted! @@ -4208,7 +4208,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\UserSettingsController.php:150 Part-DB1\src\Controller\UserSettingsController.php:182 - + csfr_invalid CSFR Token invalid. Please reload this page or contact an administrator if this message stays. @@ -4217,7 +4217,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\LabelController.php:125 - + label_generator.no_entities_found No entities matching the range found. @@ -4228,7 +4228,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\LogController.php:154 new - + log.undo.target_not_found Target element could not be found in DB! @@ -4239,7 +4239,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\LogController.php:160 new - + log.undo.revert_success Reverted to timestamp successfully. @@ -4250,7 +4250,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\LogController.php:180 new - + log.undo.element_undelete_success Undeleted element successfully. @@ -4261,7 +4261,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\LogController.php:182 new - + log.undo.element_element_already_undeleted Element was already undeleted! @@ -4272,7 +4272,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\LogController.php:189 new - + log.undo.element_delete_success Element deleted successfully. @@ -4283,7 +4283,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\LogController.php:191 new - + log.undo.element.element_already_delted Element was already deleted! @@ -4294,7 +4294,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\LogController.php:198 new - + log.undo.element_change_undone Change undone successfully! @@ -4305,7 +4305,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\LogController.php:200 new - + log.undo.do_undelete_before You have to undelete the element before you can undo this change! @@ -4316,7 +4316,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\LogController.php:203 new - + log.undo.log_type_invalid This log entry can not be undone! @@ -4327,7 +4327,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\PartController.php:182 src\Controller\PartController.php:80 - + part.edited_flash Saved changes! @@ -4337,7 +4337,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\PartController.php:186 Part-DB1\src\Controller\PartController.php:186 - + part.edited_flash.invalid Error during saving: Please check your inputs! @@ -4347,7 +4347,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\PartController.php:216 Part-DB1\src\Controller\PartController.php:219 - + part.deleted Part deleted successful. @@ -4360,7 +4360,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can src\Controller\PartController.php:113 src\Controller\PartController.php:142 - + part.created_flash Part created! @@ -4370,7 +4370,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\PartController.php:308 Part-DB1\src\Controller\PartController.php:283 - + part.created_flash.invalid Error during creation: Please check your inputs! @@ -4380,7 +4380,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\ScanController.php:68 Part-DB1\src\Controller\ScanController.php:90 - + scan.qr_not_found No element found for the given barcode. @@ -4389,7 +4389,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\ScanController.php:71 - + scan.format_unknown Format unknown! @@ -4398,7 +4398,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\ScanController.php:86 - + scan.qr_success Element found. @@ -4408,7 +4408,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\SecurityController.php:114 Part-DB1\src\Controller\SecurityController.php:109 - + pw_reset.user_or_email Username / Email @@ -4418,7 +4418,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\SecurityController.php:131 Part-DB1\src\Controller\SecurityController.php:126 - + pw_reset.request.success Reset request was successful! Please check your emails for further instructions. @@ -4428,7 +4428,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\SecurityController.php:162 Part-DB1\src\Controller\SecurityController.php:160 - + pw_reset.username Username @@ -4438,7 +4438,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\SecurityController.php:165 Part-DB1\src\Controller\SecurityController.php:163 - + pw_reset.token Token @@ -4448,7 +4448,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\SecurityController.php:194 Part-DB1\src\Controller\SecurityController.php:192 - + pw_reset.new_pw.error Username or Token invalid! Please check your input. @@ -4458,7 +4458,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\SecurityController.php:196 Part-DB1\src\Controller\SecurityController.php:194 - + pw_reset.new_pw.success Password was reset successfully. You can now login with your new password. @@ -4468,7 +4468,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\UserController.php:107 Part-DB1\src\Controller\UserController.php:99 - + user.edit.reset_success All two-factor authentication methods were successfully disabled. @@ -4478,7 +4478,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\UserSettingsController.php:101 Part-DB1\src\Controller\UserSettingsController.php:92 - + tfa_backup.no_codes_enabled No backup codes enabled! @@ -4488,7 +4488,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\UserSettingsController.php:138 Part-DB1\src\Controller\UserSettingsController.php:132 - + tfa_u2f.u2f_delete.not_existing No security key with this ID is existing. @@ -4498,7 +4498,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\UserSettingsController.php:145 Part-DB1\src\Controller\UserSettingsController.php:139 - + tfa_u2f.u2f_delete.access_denied You can not delete the security keys of other users! @@ -4508,7 +4508,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\UserSettingsController.php:153 Part-DB1\src\Controller\UserSettingsController.php:147 - + tfa.u2f.u2f_delete.success Security key successfully removed. @@ -4518,7 +4518,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\UserSettingsController.php:188 Part-DB1\src\Controller\UserSettingsController.php:180 - + tfa_trustedDevice.invalidate.success Trusted devices successfully reset. @@ -4529,7 +4529,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\UserSettingsController.php:226 src\Controller\UserController.php:98 - + user.settings.saved_flash Settings saved! @@ -4540,7 +4540,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\UserSettingsController.php:288 src\Controller\UserController.php:130 - + user.settings.pw_changed_flash Password changed! @@ -4550,7 +4550,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\UserSettingsController.php:317 Part-DB1\src\Controller\UserSettingsController.php:306 - + user.settings.2fa.google.activated Authenticator App successfully activated. @@ -4560,7 +4560,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\UserSettingsController.php:328 Part-DB1\src\Controller\UserSettingsController.php:315 - + user.settings.2fa.google.disabled Authenticator App successfully deactivated. @@ -4570,7 +4570,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Controller\UserSettingsController.php:346 Part-DB1\src\Controller\UserSettingsController.php:332 - + user.settings.2fa.backup_codes.regenerated New backup codes successfully generated. @@ -4580,7 +4580,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\AttachmentDataTable.php:148 Part-DB1\src\DataTables\AttachmentDataTable.php:148 - + attachment.table.filename File name @@ -4590,7 +4590,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\AttachmentDataTable.php:153 Part-DB1\src\DataTables\AttachmentDataTable.php:153 - + attachment.table.filesize File size @@ -4610,7 +4610,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:193 Part-DB1\src\DataTables\PartsDataTable.php:200 - + true true @@ -4632,7 +4632,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:201 Part-DB1\src\Form\Type\SIUnitType.php:139 - + false false @@ -4642,7 +4642,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\Column\LogEntryTargetColumn.php:128 Part-DB1\src\DataTables\Column\LogEntryTargetColumn.php:119 - + log.target_deleted deleted @@ -4653,7 +4653,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\Column\RevertLogColumn.php:60 new - + log.undo.undelete Undelete element @@ -4664,7 +4664,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\Column\RevertLogColumn.php:66 new - + log.undo.undo Undo change @@ -4675,7 +4675,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\Column\RevertLogColumn.php:86 new - + log.undo.revert Revert element to this timestamp @@ -4685,7 +4685,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\LogDataTable.php:173 Part-DB1\src\DataTables\LogDataTable.php:161 - + log.id ID @@ -4695,7 +4695,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\LogDataTable.php:178 Part-DB1\src\DataTables\LogDataTable.php:166 - + log.timestamp Timestamp @@ -4705,7 +4705,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\LogDataTable.php:183 Part-DB1\src\DataTables\LogDataTable.php:171 - + log.type Event @@ -4715,7 +4715,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\LogDataTable.php:191 Part-DB1\src\DataTables\LogDataTable.php:179 - + log.level Level @@ -4725,7 +4725,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\LogDataTable.php:200 Part-DB1\src\DataTables\LogDataTable.php:188 - + log.user User @@ -4735,7 +4735,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\LogDataTable.php:213 Part-DB1\src\DataTables\LogDataTable.php:201 - + log.target_type Target type @@ -4745,7 +4745,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\LogDataTable.php:226 Part-DB1\src\DataTables\LogDataTable.php:214 - + log.target Target @@ -4756,7 +4756,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\LogDataTable.php:218 new - + log.extra Extra @@ -4766,7 +4766,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:168 Part-DB1\src\DataTables\PartsDataTable.php:116 - + part.table.name Name @@ -4776,7 +4776,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:178 Part-DB1\src\DataTables\PartsDataTable.php:126 - + part.table.id Id @@ -4786,7 +4786,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:182 Part-DB1\src\DataTables\PartsDataTable.php:130 - + part.table.description Description @@ -4796,7 +4796,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:185 Part-DB1\src\DataTables\PartsDataTable.php:133 - + part.table.category Category @@ -4806,7 +4806,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:190 Part-DB1\src\DataTables\PartsDataTable.php:138 - + part.table.footprint Footprint @@ -4816,7 +4816,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:194 Part-DB1\src\DataTables\PartsDataTable.php:142 - + part.table.manufacturer Manufacturer @@ -4826,7 +4826,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:197 Part-DB1\src\DataTables\PartsDataTable.php:145 - + part.table.storeLocations Store locations @@ -4836,7 +4836,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:216 Part-DB1\src\DataTables\PartsDataTable.php:164 - + part.table.amount Amount @@ -4846,7 +4846,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:224 Part-DB1\src\DataTables\PartsDataTable.php:172 - + part.table.minamount Min. Amount @@ -4856,7 +4856,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:232 Part-DB1\src\DataTables\PartsDataTable.php:180 - + part.table.partUnit Measurement Unit @@ -4866,7 +4866,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:236 Part-DB1\src\DataTables\PartsDataTable.php:184 - + part.table.addedDate Created at @@ -4876,7 +4876,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:240 Part-DB1\src\DataTables\PartsDataTable.php:188 - + part.table.lastModified Last modified @@ -4886,7 +4886,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:244 Part-DB1\src\DataTables\PartsDataTable.php:192 - + part.table.needsReview Needs review @@ -4896,7 +4896,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:251 Part-DB1\src\DataTables\PartsDataTable.php:199 - + part.table.favorite Favorite @@ -4906,7 +4906,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:258 Part-DB1\src\DataTables\PartsDataTable.php:206 - + part.table.manufacturingStatus Status @@ -4920,7 +4920,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:210 Part-DB1\src\Form\Part\PartBaseType.php:88 - + m_status.unknown Unknown @@ -4932,7 +4932,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:211 Part-DB1\src\Form\Part\PartBaseType.php:88 - + m_status.announced Announced @@ -4944,7 +4944,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:212 Part-DB1\src\Form\Part\PartBaseType.php:88 - + m_status.active Active @@ -4956,7 +4956,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:213 Part-DB1\src\Form\Part\PartBaseType.php:88 - + m_status.nrfnd Not recommended for new designs @@ -4968,7 +4968,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:214 Part-DB1\src\Form\Part\PartBaseType.php:88 - + m_status.eol End of life @@ -4980,7 +4980,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:215 Part-DB1\src\Form\Part\PartBaseType.php:88 - + m_status.discontinued Discontinued @@ -4990,7 +4990,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:271 Part-DB1\src\DataTables\PartsDataTable.php:219 - + part.table.mpn MPN @@ -5000,7 +5000,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:275 Part-DB1\src\DataTables\PartsDataTable.php:223 - + part.table.mass Mass @@ -5010,7 +5010,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:279 Part-DB1\src\DataTables\PartsDataTable.php:227 - + part.table.tags Tags @@ -5020,7 +5020,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\DataTables\PartsDataTable.php:283 Part-DB1\src\DataTables\PartsDataTable.php:231 - + part.table.attachments Attachments @@ -5030,7 +5030,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\EventSubscriber\UserSystem\LoginSuccessSubscriber.php:82 Part-DB1\src\EventSubscriber\LoginSuccessListener.php:82 - + flash.login_successful Login successful @@ -5041,7 +5041,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AdminPages\ImportType.php:77 src\Form\ImportType.php:68 - + JSON JSON @@ -5052,7 +5052,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AdminPages\ImportType.php:77 src\Form\ImportType.php:68 - + XML XML @@ -5063,7 +5063,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AdminPages\ImportType.php:77 src\Form\ImportType.php:68 - + CSV CSV @@ -5074,7 +5074,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AdminPages\ImportType.php:77 src\Form\ImportType.php:68 - + YAML YAML @@ -5084,7 +5084,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AdminPages\ImportType.php:124 Part-DB1\src\Form\AdminPages\ImportType.php:124 - + import.abort_on_validation.help When this option is activated, the whole import process is aborted if invalid data is detected. If not selected, the invalid data is ignored and the importer will try to import the other elements. @@ -5095,7 +5095,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AdminPages\ImportType.php:86 src\Form\ImportType.php:70 - + import.csv_separator CSV separator @@ -5106,7 +5106,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AdminPages\ImportType.php:93 src\Form\ImportType.php:72 - + parent.label Parent element @@ -5117,7 +5117,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AdminPages\ImportType.php:101 src\Form\ImportType.php:75 - + import.file File @@ -5128,7 +5128,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AdminPages\ImportType.php:111 src\Form\ImportType.php:78 - + import.preserve_children Preserve child elements on import @@ -5139,7 +5139,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AdminPages\ImportType.php:120 src\Form\ImportType.php:80 - + import.abort_on_validation Abort on invalid data @@ -5150,7 +5150,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AdminPages\ImportType.php:132 src\Form\ImportType.php:85 - + import.btn Import @@ -5160,7 +5160,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AttachmentFormType.php:113 Part-DB1\src\Form\AttachmentFormType.php:109 - + attachment.edit.secure_file.help An attachment marked private can only be accessed by authenticated users with the proper permission. If this is activated no thumbnails are generated and access to file is less performant. @@ -5170,7 +5170,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AttachmentFormType.php:127 Part-DB1\src\Form\AttachmentFormType.php:123 - + attachment.edit.url.help You can specify an URL to an external file here, or input an keyword which is used to search in built-in resources (e.g. footprints) @@ -5180,7 +5180,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AttachmentFormType.php:82 Part-DB1\src\Form\AttachmentFormType.php:79 - + attachment.edit.name Name @@ -5190,7 +5190,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AttachmentFormType.php:85 Part-DB1\src\Form\AttachmentFormType.php:82 - + attachment.edit.attachment_type Attachment type @@ -5200,7 +5200,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AttachmentFormType.php:94 Part-DB1\src\Form\AttachmentFormType.php:91 - + attachment.edit.show_in_table Show in table @@ -5210,7 +5210,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AttachmentFormType.php:105 Part-DB1\src\Form\AttachmentFormType.php:102 - + attachment.edit.secure_file Private attachment @@ -5220,7 +5220,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AttachmentFormType.php:119 Part-DB1\src\Form\AttachmentFormType.php:115 - + attachment.edit.url URL @@ -5230,7 +5230,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AttachmentFormType.php:133 Part-DB1\src\Form\AttachmentFormType.php:129 - + attachment.edit.download_url Download external file @@ -5240,7 +5240,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\AttachmentFormType.php:146 Part-DB1\src\Form\AttachmentFormType.php:142 - + attachment.edit.file Upload file @@ -5250,7 +5250,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:68 Part-DB1\src\Services\ElementTypeNameGenerator.php:86 - + part.label Part @@ -5260,7 +5260,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:68 Part-DB1\src\Services\ElementTypeNameGenerator.php:87 - + part_lot.label Part lot @@ -5269,7 +5269,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:78 - + label_options.barcode_type.none None @@ -5278,7 +5278,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:78 - + label_options.barcode_type.qr QR Code (recommended) @@ -5287,7 +5287,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:78 - + label_options.barcode_type.code128 Code 128 (recommended) @@ -5296,7 +5296,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:78 - + label_options.barcode_type.code39 Code 39 (recommended) @@ -5305,7 +5305,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:78 - + label_options.barcode_type.code93 Code 93 @@ -5314,7 +5314,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:78 - + label_options.barcode_type.datamatrix Datamatrix @@ -5323,7 +5323,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:122 - + label_options.lines_mode.html Placeholders @@ -5332,7 +5332,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:122 - + label.options.lines_mode.twig Twig @@ -5341,16 +5341,16 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:126 - + label_options.lines_mode.help - If you select Twig here, the content field is interpreted as Twig template. See <a href="https://twig.symfony.com/doc/3.x/templates.html">Twig documentation</a> and <a href="https://docs.part-db.de/usage/labels.html#twig-mode">Wiki</a> for more information. + Twig documentation and Wiki for more information.]]> Part-DB1\src\Form\LabelOptionsType.php:47 - + label_options.page_size.label Label size @@ -5359,7 +5359,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:66 - + label_options.supported_elements.label Target type @@ -5368,7 +5368,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:75 - + label_options.barcode_type.label Barcode @@ -5377,7 +5377,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:102 - + label_profile.lines.label Content @@ -5386,7 +5386,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:111 - + label_options.additional_css.label Additional styles (CSS) @@ -5395,7 +5395,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:120 - + label_options.lines_mode.label Parser mode @@ -5404,7 +5404,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:51 - + label_options.width.placeholder Width @@ -5413,7 +5413,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelOptionsType.php:60 - + label_options.height.placeholder Height @@ -5422,7 +5422,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelSystem\LabelDialogType.php:49 - + label_generator.target_id.range_hint You can specify multiple IDs (e.g. 1,2,3) and/or a range (1-3) here to generate labels for multiple elements at once. @@ -5431,7 +5431,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelSystem\LabelDialogType.php:46 - + label_generator.target_id.label Target IDs @@ -5440,7 +5440,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelSystem\LabelDialogType.php:59 - + label_generator.update Update @@ -5449,7 +5449,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelSystem\ScanDialogType.php:36 - + scan_dialog.input Input @@ -5458,7 +5458,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\LabelSystem\ScanDialogType.php:44 - + scan_dialog.submit Submit @@ -5467,7 +5467,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\ParameterType.php:41 - + parameters.name.placeholder e.g. DC Current Gain @@ -5476,7 +5476,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\ParameterType.php:50 - + parameters.symbol.placeholder e.g. h_{FE} @@ -5485,7 +5485,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\ParameterType.php:60 - + parameters.text.placeholder e.g. Test conditions @@ -5494,7 +5494,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\ParameterType.php:71 - + parameters.max.placeholder e.g. 350 @@ -5503,7 +5503,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\ParameterType.php:82 - + parameters.min.placeholder e.g. 100 @@ -5512,7 +5512,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\ParameterType.php:93 - + parameters.typical.placeholder e.g. 200 @@ -5521,7 +5521,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\ParameterType.php:103 - + parameters.unit.placeholder e.g. V @@ -5530,7 +5530,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\ParameterType.php:114 - + parameter.group.placeholder e.g. Technical Specifications @@ -5540,7 +5540,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\OrderdetailType.php:72 Part-DB1\src\Form\Part\OrderdetailType.php:75 - + orderdetails.edit.supplierpartnr Supplier part number @@ -5550,7 +5550,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\OrderdetailType.php:81 Part-DB1\src\Form\Part\OrderdetailType.php:84 - + orderdetails.edit.supplier Supplier @@ -5560,7 +5560,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\OrderdetailType.php:87 Part-DB1\src\Form\Part\OrderdetailType.php:90 - + orderdetails.edit.url Link to offer @@ -5570,7 +5570,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\OrderdetailType.php:93 Part-DB1\src\Form\Part\OrderdetailType.php:96 - + orderdetails.edit.obsolete No longer available @@ -5580,7 +5580,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\OrderdetailType.php:75 Part-DB1\src\Form\Part\OrderdetailType.php:78 - + orderdetails.edit.supplierpartnr.placeholder e.g. BC 547 @@ -5590,7 +5590,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:101 Part-DB1\src\Form\Part\PartBaseType.php:99 - + part.edit.name Name @@ -5600,7 +5600,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:109 Part-DB1\src\Form\Part\PartBaseType.php:107 - + part.edit.description Description @@ -5610,7 +5610,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:120 Part-DB1\src\Form\Part\PartBaseType.php:118 - + part.edit.mininstock Minimum stock @@ -5620,7 +5620,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:129 Part-DB1\src\Form\Part\PartBaseType.php:127 - + part.edit.category Category @@ -5630,7 +5630,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:135 Part-DB1\src\Form\Part\PartBaseType.php:133 - + part.edit.footprint Footprint @@ -5640,7 +5640,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:142 Part-DB1\src\Form\Part\PartBaseType.php:140 - + part.edit.tags Tags @@ -5650,7 +5650,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:154 Part-DB1\src\Form\Part\PartBaseType.php:152 - + part.edit.manufacturer.label Manufacturer @@ -5660,7 +5660,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:161 Part-DB1\src\Form\Part\PartBaseType.php:159 - + part.edit.manufacturer_url.label Link to product page @@ -5670,7 +5670,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:167 Part-DB1\src\Form\Part\PartBaseType.php:165 - + part.edit.mpn Manufacturer part number @@ -5680,7 +5680,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:173 Part-DB1\src\Form\Part\PartBaseType.php:171 - + part.edit.manufacturing_status Manufacturing status @@ -5690,7 +5690,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:181 Part-DB1\src\Form\Part\PartBaseType.php:179 - + part.edit.needs_review Needs review @@ -5700,7 +5700,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:189 Part-DB1\src\Form\Part\PartBaseType.php:187 - + part.edit.is_favorite Favorite @@ -5710,7 +5710,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:197 Part-DB1\src\Form\Part\PartBaseType.php:195 - + part.edit.mass Mass @@ -5720,7 +5720,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:203 Part-DB1\src\Form\Part\PartBaseType.php:201 - + part.edit.partUnit Measuring unit @@ -5730,7 +5730,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:212 Part-DB1\src\Form\Part\PartBaseType.php:210 - + part.edit.comment Notes @@ -5740,7 +5740,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:250 Part-DB1\src\Form\Part\PartBaseType.php:246 - + part.edit.master_attachment Preview image @@ -5751,7 +5751,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:276 src\Form\PartType.php:91 - + part.edit.save Save changes @@ -5762,7 +5762,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:277 src\Form\PartType.php:92 - + part.edit.reset Reset changes @@ -5772,7 +5772,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:105 Part-DB1\src\Form\Part\PartBaseType.php:103 - + part.edit.name.placeholder e.g. BC547 @@ -5782,7 +5782,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:115 Part-DB1\src\Form\Part\PartBaseType.php:113 - + part.edit.description.placeholder e.g. NPN 45V, 0,1A, 0,5W @@ -5792,7 +5792,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartBaseType.php:123 Part-DB1\src\Form\Part\PartBaseType.php:121 - + part.editmininstock.placeholder e.g. 1 @@ -5802,7 +5802,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartLotType.php:69 Part-DB1\src\Form\Part\PartLotType.php:69 - + part_lot.edit.description Description @@ -5812,7 +5812,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartLotType.php:78 Part-DB1\src\Form\Part\PartLotType.php:78 - + part_lot.edit.location Storage location @@ -5822,7 +5822,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartLotType.php:89 Part-DB1\src\Form\Part\PartLotType.php:89 - + part_lot.edit.amount Amount @@ -5832,7 +5832,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartLotType.php:98 Part-DB1\src\Form\Part\PartLotType.php:97 - + part_lot.edit.instock_unknown Amount unknown @@ -5842,7 +5842,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartLotType.php:109 Part-DB1\src\Form\Part\PartLotType.php:108 - + part_lot.edit.needs_refill Needs refill @@ -5852,7 +5852,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartLotType.php:120 Part-DB1\src\Form\Part\PartLotType.php:119 - + part_lot.edit.expiration_date Expiration date @@ -5862,7 +5862,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Part\PartLotType.php:128 Part-DB1\src\Form\Part\PartLotType.php:125 - + part_lot.edit.comment Notes @@ -5872,7 +5872,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Permissions\PermissionsType.php:99 Part-DB1\src\Form\Permissions\PermissionsType.php:99 - + perm.group.other Miscellaneous @@ -5882,7 +5882,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\TFAGoogleSettingsType.php:97 Part-DB1\src\Form\TFAGoogleSettingsType.php:97 - + tfa_google.enable Enable authenticator app @@ -5892,7 +5892,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\TFAGoogleSettingsType.php:101 Part-DB1\src\Form\TFAGoogleSettingsType.php:101 - + tfa_google.disable Deactivate authenticator app @@ -5902,7 +5902,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\TFAGoogleSettingsType.php:74 Part-DB1\src\Form\TFAGoogleSettingsType.php:74 - + google_confirmation Confirmation code @@ -5913,7 +5913,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\UserSettingsType.php:108 src\Form\UserSettingsType.php:46 - + user.timezone.label Timezone @@ -5923,7 +5923,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\UserSettingsType.php:133 Part-DB1\src\Form\UserSettingsType.php:132 - + user.currency.label Preferred currency @@ -5934,7 +5934,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\UserSettingsType.php:139 src\Form\UserSettingsType.php:53 - + save Apply changes @@ -5945,7 +5945,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\UserSettingsType.php:140 src\Form\UserSettingsType.php:54 - + reset Discard changes @@ -5956,7 +5956,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\UserSettingsType.php:104 src\Form\UserSettingsType.php:45 - + user_settings.language.placeholder Serverwide language @@ -5967,7 +5967,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\UserSettingsType.php:115 src\Form\UserSettingsType.php:48 - + user_settings.timezone.placeholder Serverwide Timezone @@ -5977,7 +5977,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\ElementTypeNameGenerator.php:79 Part-DB1\src\Services\ElementTypeNameGenerator.php:79 - + attachment.label Attachment @@ -5987,7 +5987,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\ElementTypeNameGenerator.php:81 Part-DB1\src\Services\ElementTypeNameGenerator.php:81 - + attachment_type.label Attachment type @@ -5997,7 +5997,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\ElementTypeNameGenerator.php:82 Part-DB1\src\Services\ElementTypeNameGenerator.php:82 - + project.label Project @@ -6007,7 +6007,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\ElementTypeNameGenerator.php:85 Part-DB1\src\Services\ElementTypeNameGenerator.php:85 - + measurement_unit.label Measurement unit @@ -6017,7 +6017,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\ElementTypeNameGenerator.php:90 Part-DB1\src\Services\ElementTypeNameGenerator.php:90 - + currency.label Currency @@ -6027,7 +6027,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\ElementTypeNameGenerator.php:91 Part-DB1\src\Services\ElementTypeNameGenerator.php:91 - + orderdetail.label Order detail @@ -6037,7 +6037,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\ElementTypeNameGenerator.php:92 Part-DB1\src\Services\ElementTypeNameGenerator.php:92 - + pricedetail.label Price detail @@ -6047,7 +6047,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\ElementTypeNameGenerator.php:94 Part-DB1\src\Services\ElementTypeNameGenerator.php:94 - + user.label User @@ -6056,7 +6056,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\ElementTypeNameGenerator.php:95 - + parameter.label Parameter @@ -6065,7 +6065,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\ElementTypeNameGenerator.php:96 - + label_profile.label Label profile @@ -6076,7 +6076,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\LogSystem\LogEntryExtraFormatter.php:161 new - + log.element_deleted.old_name.unknown Unknown @@ -6086,7 +6086,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\MarkdownParser.php:73 Part-DB1\src\Services\MarkdownParser.php:73 - + markdown.loading Loading markdown. If this message does not disappear, try to reload the page. @@ -6096,7 +6096,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\PasswordResetManager.php:98 Part-DB1\src\Services\PasswordResetManager.php:98 - + pw_reset.email.subject Password reset for your Part-DB account @@ -6105,7 +6105,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:108 - + tree.tools.tools Tools @@ -6116,7 +6116,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:107 src\Services\ToolsTreeBuilder.php:74 - + tree.tools.edit Edit @@ -6127,7 +6127,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:108 src\Services\ToolsTreeBuilder.php:81 - + tree.tools.show Show @@ -6137,7 +6137,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:111 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:109 - + tree.tools.system System @@ -6146,7 +6146,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:123 - + tree.tools.tools.label_dialog Label generator @@ -6155,7 +6155,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:130 - + tree.tools.tools.label_scanner Scanner @@ -6166,7 +6166,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:126 src\Services\ToolsTreeBuilder.php:62 - + tree.tools.edit.attachment_types Attachment types @@ -6177,7 +6177,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:132 src\Services\ToolsTreeBuilder.php:64 - + tree.tools.edit.categories Categories @@ -6188,7 +6188,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:138 src\Services\ToolsTreeBuilder.php:66 - + tree.tools.edit.projects Projects @@ -6199,7 +6199,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:144 src\Services\ToolsTreeBuilder.php:68 - + tree.tools.edit.suppliers Suppliers @@ -6210,7 +6210,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:150 src\Services\ToolsTreeBuilder.php:70 - + tree.tools.edit.manufacturer Manufacturers @@ -6220,7 +6220,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:179 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:156 - + tree.tools.edit.storelocation Storage locations @@ -6230,7 +6230,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:185 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:162 - + tree.tools.edit.footprint Footprints @@ -6240,7 +6240,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:191 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:168 - + tree.tools.edit.currency Currencies @@ -6250,7 +6250,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:197 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:174 - + tree.tools.edit.measurement_unit Measurement Unit @@ -6259,7 +6259,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 - + tree.tools.edit.label_profile Label profiles @@ -6269,7 +6269,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:209 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:180 - + tree.tools.edit.part New part @@ -6280,7 +6280,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:197 src\Services\ToolsTreeBuilder.php:77 - + tree.tools.show.all_parts Show all parts @@ -6290,7 +6290,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:232 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:203 - + tree.tools.show.all_attachments Attachments @@ -6301,7 +6301,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:210 new - + tree.tools.show.statistics Statistics @@ -6311,7 +6311,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:258 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:229 - + tree.tools.system.users Users @@ -6321,7 +6321,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:264 Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:235 - + tree.tools.system.groups Groups @@ -6332,7 +6332,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\ToolsTreeBuilder.php:242 new - + tree.tools.system.event_log Event log @@ -6343,7 +6343,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Services\Trees\TreeViewGenerator.php:95 src\Services\TreeBuilder.php:124 - + entity.tree.new New Element @@ -6353,7 +6353,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\templates\Parts\info\_attachments_info.html.twig:34 obsolete - + attachment.external_file External file @@ -6363,7 +6363,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\templates\Parts\info\_attachments_info.html.twig:62 obsolete - + attachment.edit Edit @@ -6374,7 +6374,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can templates\base.html.twig:88 obsolete - + barcode.scan Scan Barcode @@ -6385,7 +6385,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can src\Form\UserSettingsType.php:49 obsolete - + user.theme.label Theme @@ -6396,7 +6396,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can src\Form\UserSettingsType.php:50 obsolete - + user_settings.theme.placeholder Serverwide Theme @@ -6406,7 +6406,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Type\SIUnitType.php:141 obsolete - + M M @@ -6416,14 +6416,14 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Type\SIUnitType.php:141 obsolete - + k k - + @@ -6432,7 +6432,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Type\SIUnitType.php:141 obsolete - + m m @@ -6442,7 +6442,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Part-DB1\src\Form\Type\SIUnitType.php:141 obsolete - + µ µ @@ -6453,7 +6453,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can new obsolete - + log.user_login.ip IP @@ -6467,7 +6467,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can new obsolete - + log.undo_mode.undo Change undone @@ -6481,7 +6481,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can new obsolete - + log.undo_mode.revert Element reverted @@ -6492,7 +6492,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can new obsolete - + log.element_created.original_instock Old instock @@ -6503,7 +6503,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can new obsolete - + log.element_deleted.old_name Old name @@ -6514,7 +6514,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can new obsolete - + log.element_edited.changed_fields Changed fields @@ -6525,7 +6525,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can new obsolete - + log.instock_changed.comment Comment @@ -6536,7 +6536,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can new obsolete - + log.collection_deleted.deleted Deleted element: @@ -6547,7 +6547,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + go.exclamation Go! @@ -6558,7 +6558,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + language.english English @@ -6569,7 +6569,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + language.german German @@ -6579,7 +6579,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + flash.password_change_needed Password change needed! @@ -6589,7 +6589,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + attachment.table.type Attachment type @@ -6599,7 +6599,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + attachment.table.element Associated element @@ -6609,7 +6609,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + attachment.edit.isPicture Picture? @@ -6619,7 +6619,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + attachment.edit.is3DModel 3D model? @@ -6629,7 +6629,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + attachment.edit.isBuiltin Builtin? @@ -6639,7 +6639,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + category.edit.default_comment.placeholder e.g. useful for switching @@ -6649,7 +6649,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + tfa_backup.regenerate_codes Generate new backup codes @@ -6659,7 +6659,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + validator.noneofitschild.self A element can not be its own parent. @@ -6669,7 +6669,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + validator.noneofitschild.children The parent can not be one of the children of itself. @@ -6679,7 +6679,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + validator.isSelectable The element must be selectable. @@ -6689,7 +6689,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + validator.part_lot.location_full.no_increasment The storage location was marked as full, so you can not increase the instock amount. (New amount max. {{ old_amount }}) @@ -6699,7 +6699,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + validator.part_lot.location_full The storage location was marked as full, so you can not add a new part to it. @@ -6709,7 +6709,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + validator.part_lot.only_existing The storage location was marked as "only existing", so you can not add new part to it. @@ -6719,7 +6719,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + validator.part_lot.single_part The storage location was marked as "single part", so you can not add a new part to it. @@ -6729,7 +6729,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + m_status.active.help The part is currently and in the foreseeable future in production @@ -6739,7 +6739,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + m_status.announced.help The part was announced but is not available yet. @@ -6749,7 +6749,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + m_status.discontinued.help The part is discontinued and not produced anymore. @@ -6759,7 +6759,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + m_status.eol.help The product has reached its end-of-life and the production will be stopped soon. @@ -6769,7 +6769,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + m_status.nrfnd.help The part is currently in production but is not recommended for new designs. @@ -6779,7 +6779,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + m_status.unknown.help The manufacturing status of the part is not known. @@ -6789,7 +6789,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + flash.success Success @@ -6799,7 +6799,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + flash.error Error @@ -6809,7 +6809,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + flash.warning Warning @@ -6819,7 +6819,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + flash.notice Notice @@ -6829,7 +6829,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + flash.info Info @@ -6839,7 +6839,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + validator.noLockout You can not withdraw yourself the "change permission" permission, to prevent that you lockout yourself accidentally. @@ -6849,7 +6849,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + attachment_type.edit.filetype_filter Allowed file extensions @@ -6859,7 +6859,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + attachment_type.edit.filetype_filter.help You can specify a comma separated list of file extension or mime types, which an uploaded file must have when assigned to this attachment type. To allow all supported image files, you can use image/*. @@ -6869,7 +6869,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + attachment_type.edit.filetype_filter.placeholder e.g. .txt, application/pdf, image/* @@ -6880,7 +6880,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + part.name.placeholder e.g. BC547 @@ -6890,7 +6890,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + entity.edit.not_selectable Not selectable @@ -6900,7 +6900,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + entity.edit.not_selectable.help If this option is activated, this element can not be assigned to a part property. Useful if this element is just used for grouping. @@ -6910,7 +6910,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + bbcode.hint You can use BBCode here (e.g. [b]Bold[/b]) @@ -6920,7 +6920,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + entity.create Create element @@ -6930,7 +6930,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + entity.edit.save Save @@ -6940,7 +6940,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + category.edit.disable_footprints Disable footprints @@ -6950,7 +6950,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + category.edit.disable_footprints.help If this option is activated, the footprint property is disabled for all parts with this category. @@ -6960,7 +6960,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + category.edit.disable_manufacturers Disable manufacturers @@ -6970,7 +6970,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + category.edit.disable_manufacturers.help If this option is activated, the manufacturer property is disabled for all parts with this category. @@ -6980,7 +6980,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + category.edit.disable_autodatasheets Disable automatic datasheet links @@ -6990,7 +6990,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + category.edit.disable_autodatasheets.help If this option is activated, no automatic links to datasheets are created for parts with this category. @@ -7000,7 +7000,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + category.edit.disable_properties Disable properties @@ -7010,7 +7010,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + category.edit.disable_properties.help If this option is activated, the part properties are disabled for parts with this category. @@ -7020,7 +7020,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + category.edit.partname_hint Part name hint @@ -7030,7 +7030,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + category.edit.partname_hint.placeholder e.g. 100nF @@ -7040,7 +7040,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + category.edit.partname_regex Name filter @@ -7050,7 +7050,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + category.edit.default_description Default description @@ -7060,7 +7060,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + category.edit.default_description.placeholder e.g. Capacitor, 10mm x 10mm, SMD @@ -7070,7 +7070,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + category.edit.default_comment Default notes @@ -7080,7 +7080,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + company.edit.address Address @@ -7090,7 +7090,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can obsolete obsolete - + company.edit.address.placeholder e.g. Examplestreet 314 Exampletown @@ -7101,7 +7101,7 @@ Exampletown obsolete obsolete - + company.edit.phone_number Phone number @@ -7111,7 +7111,7 @@ Exampletown obsolete obsolete - + company.edit.phone_number.placeholder +49 12345 6789 @@ -7121,7 +7121,7 @@ Exampletown obsolete obsolete - + company.edit.fax_number Fax number @@ -7131,7 +7131,7 @@ Exampletown obsolete obsolete - + company.edit.email Email @@ -7141,7 +7141,7 @@ Exampletown obsolete obsolete - + company.edit.email.placeholder e.g. contact@foo.bar @@ -7151,7 +7151,7 @@ Exampletown obsolete obsolete - + company.edit.website Website @@ -7161,7 +7161,7 @@ Exampletown obsolete obsolete - + company.edit.website.placeholder https://www.foo.bar @@ -7171,7 +7171,7 @@ Exampletown obsolete obsolete - + company.edit.auto_product_url Product URL @@ -7181,7 +7181,7 @@ Exampletown obsolete obsolete - + company.edit.auto_product_url.help This field is used to determine a link to the part on the company page. %PARTNUMBER% will be replaced with the order number. @@ -7191,7 +7191,7 @@ Exampletown obsolete obsolete - + company.edit.auto_product_url.placeholder https://foo.bar/product/%PARTNUMBER% @@ -7201,7 +7201,7 @@ Exampletown obsolete obsolete - + currency.edit.iso_code ISO code @@ -7211,7 +7211,7 @@ Exampletown obsolete obsolete - + currency.edit.exchange_rate Exchange rate @@ -7221,7 +7221,7 @@ Exampletown obsolete obsolete - + footprint.edit.3d_model 3D model @@ -7231,7 +7231,7 @@ Exampletown obsolete obsolete - + mass_creation.lines Input @@ -7241,7 +7241,7 @@ Exampletown obsolete obsolete - + mass_creation.lines.placeholder Element 1 Element 1.1 @@ -7256,7 +7256,7 @@ Element 3 obsolete obsolete - + entity.mass_creation.btn Create @@ -7266,7 +7266,7 @@ Element 3 obsolete obsolete - + measurement_unit.edit.is_integer Is integer @@ -7276,7 +7276,7 @@ Element 3 obsolete obsolete - + measurement_unit.edit.is_integer.help If this option is activated, all values with this unit will be rounded to whole numbers. @@ -7286,7 +7286,7 @@ Element 3 obsolete obsolete - + measurement_unit.edit.use_si_prefix Use SI prefix @@ -7296,7 +7296,7 @@ Element 3 obsolete obsolete - + measurement_unit.edit.use_si_prefix.help If this option is activated, values are outputted with SI prefixes (e.g. 1,2kg instead of 1200g) @@ -7306,7 +7306,7 @@ Element 3 obsolete obsolete - + measurement_unit.edit.unit_symbol Unit symbol @@ -7316,7 +7316,7 @@ Element 3 obsolete obsolete - + measurement_unit.edit.unit_symbol.placeholder e.g. m @@ -7326,7 +7326,7 @@ Element 3 obsolete obsolete - + storelocation.edit.is_full.label Storelocation full @@ -7336,7 +7336,7 @@ Element 3 obsolete obsolete - + storelocation.edit.is_full.help If this option is selected, it is neither possible to add new parts to this storelocation or to increase the amount of existing parts. @@ -7346,7 +7346,7 @@ Element 3 obsolete obsolete - + storelocation.limit_to_existing.label Limit to existing parts @@ -7356,7 +7356,7 @@ Element 3 obsolete obsolete - + storelocation.limit_to_existing.help If this option is activated, it is not possible to add new parts to this storelocation, but the amount of existing parts can be increased. @@ -7366,7 +7366,7 @@ Element 3 obsolete obsolete - + storelocation.only_single_part.label Only single part @@ -7376,7 +7376,7 @@ Element 3 obsolete obsolete - + storelocation.only_single_part.help If this option is activated, only a single part (with every amount) can be assigned to this store location. Useful for small SMD boxes or feeders. @@ -7386,7 +7386,7 @@ Element 3 obsolete obsolete - + storelocation.storage_type.label Storage type @@ -7396,7 +7396,7 @@ Element 3 obsolete obsolete - + storelocation.storage_type.help You can select a measurement unit here, which a part must have to be able to be assigned to this storage location @@ -7406,7 +7406,7 @@ Element 3 obsolete obsolete - + supplier.edit.default_currency Default currency @@ -7416,7 +7416,7 @@ Element 3 obsolete obsolete - + supplier.shipping_costs.label Shipping Costs @@ -7426,7 +7426,7 @@ Element 3 obsolete obsolete - + user.username.placeholder e.g. j.doe @@ -7436,7 +7436,7 @@ Element 3 obsolete obsolete - + user.firstName.placeholder e.g John @@ -7446,7 +7446,7 @@ Element 3 obsolete obsolete - + user.lastName.placeholder e.g. Doe @@ -7456,7 +7456,7 @@ Element 3 obsolete obsolete - + user.email.placeholder j.doe@ecorp.com @@ -7466,7 +7466,7 @@ Element 3 obsolete obsolete - + user.department.placeholder e.g. Development @@ -7476,7 +7476,7 @@ Element 3 obsolete obsolete - + user.settings.pw_new.label New password @@ -7486,7 +7486,7 @@ Element 3 obsolete obsolete - + user.settings.pw_confirm.label Confirm new password @@ -7496,7 +7496,7 @@ Element 3 obsolete obsolete - + user.edit.needs_pw_change User needs to change password @@ -7506,7 +7506,7 @@ Element 3 obsolete obsolete - + user.edit.user_disabled User disabled (no login possible) @@ -7516,7 +7516,7 @@ Element 3 obsolete obsolete - + user.create Create user @@ -7526,7 +7526,7 @@ Element 3 obsolete obsolete - + user.edit.save Save @@ -7536,7 +7536,7 @@ Element 3 obsolete obsolete - + entity.edit.reset Discard changes @@ -7547,7 +7547,7 @@ Element 3 obsolete obsolete - + part.withdraw.caption: Withdraw parts: @@ -7558,7 +7558,7 @@ Element 3 obsolete obsolete - + part.withdraw.btn Withdraw @@ -7569,7 +7569,7 @@ Element 3 obsolete obsolete - + part.withdraw.comment: Comment/Purpose @@ -7580,7 +7580,7 @@ Element 3 obsolete obsolete - + part.add.caption Add parts @@ -7591,7 +7591,7 @@ Element 3 obsolete obsolete - + part.add.btn Add @@ -7602,7 +7602,7 @@ Element 3 obsolete obsolete - + part.add.comment Comment/Purpose @@ -7613,7 +7613,7 @@ Element 3 obsolete obsolete - + admin.comment Notes @@ -7624,7 +7624,7 @@ Element 3 obsolete obsolete - + manufacturer_url.label Manufacturer link @@ -7635,7 +7635,7 @@ Element 3 obsolete obsolete - + part.description.placeholder e.g. NPN 45V 0,1A 0,5W @@ -7646,7 +7646,7 @@ Element 3 obsolete obsolete - + part.instock.placeholder e.g. 10 @@ -7657,7 +7657,7 @@ Element 3 obsolete obsolete - + part.mininstock.placeholder e.g. 5 @@ -7667,7 +7667,7 @@ Element 3 obsolete obsolete - + homepage.basedOn Based on work of @@ -7677,7 +7677,7 @@ Element 3 obsolete obsolete - + homepage.others and others @@ -7687,7 +7687,7 @@ Element 3 obsolete obsolete - + part.order.price_per Price per @@ -7697,7 +7697,7 @@ Element 3 obsolete obsolete - + part.withdraw.caption Withdraw parts @@ -7707,7 +7707,7 @@ Element 3 obsolete obsolete - + datatable.datatable.lengthMenu _MENU_ @@ -7717,7 +7717,7 @@ Element 3 obsolete obsolete - + perm.group.parts Parts @@ -7727,7 +7727,7 @@ Element 3 obsolete obsolete - + perm.group.structures Data structures @@ -7737,7 +7737,7 @@ Element 3 obsolete obsolete - + perm.group.system System @@ -7747,7 +7747,7 @@ Element 3 obsolete obsolete - + perm.parts Parts @@ -7757,7 +7757,7 @@ Element 3 obsolete obsolete - + perm.read View @@ -7767,7 +7767,7 @@ Element 3 obsolete obsolete - + perm.edit Edit @@ -7777,7 +7777,7 @@ Element 3 obsolete obsolete - + perm.create Create @@ -7787,7 +7787,7 @@ Element 3 obsolete obsolete - + perm.part.move Change category @@ -7797,7 +7797,7 @@ Element 3 obsolete obsolete - + perm.delete Delete @@ -7807,7 +7807,7 @@ Element 3 obsolete obsolete - + perm.part.search Search @@ -7817,7 +7817,7 @@ Element 3 obsolete obsolete - + perm.part.all_parts List all parts @@ -7827,7 +7827,7 @@ Element 3 obsolete obsolete - + perm.part.no_price_parts List parts without price infos @@ -7837,7 +7837,7 @@ Element 3 obsolete obsolete - + perm.part.obsolete_parts List obsolete parts @@ -7847,7 +7847,7 @@ Element 3 obsolete obsolete - + perm.part.unknown_instock_parts Show parts with unknown instock @@ -7857,7 +7857,7 @@ Element 3 obsolete obsolete - + perm.part.change_favorite Change favorite status @@ -7867,7 +7867,7 @@ Element 3 obsolete obsolete - + perm.part.show_favorite List favorite parts @@ -7877,7 +7877,7 @@ Element 3 obsolete obsolete - + perm.part.show_last_edit_parts Show last edited/added parts @@ -7887,7 +7887,7 @@ Element 3 obsolete obsolete - + perm.part.show_users Show last modifying user @@ -7897,7 +7897,7 @@ Element 3 obsolete obsolete - + perm.part.show_history Show history @@ -7907,7 +7907,7 @@ Element 3 obsolete obsolete - + perm.part.name Name @@ -7917,7 +7917,7 @@ Element 3 obsolete obsolete - + perm.part.description Description @@ -7927,7 +7927,7 @@ Element 3 obsolete obsolete - + perm.part.instock Instock @@ -7937,7 +7937,7 @@ Element 3 obsolete obsolete - + perm.part.mininstock Minimum instock @@ -7947,7 +7947,7 @@ Element 3 obsolete obsolete - + perm.part.comment Notes @@ -7957,7 +7957,7 @@ Element 3 obsolete obsolete - + perm.part.storelocation Storelocation @@ -7967,7 +7967,7 @@ Element 3 obsolete obsolete - + perm.part.manufacturer Manufacturer @@ -7977,7 +7977,7 @@ Element 3 obsolete obsolete - + perm.part.orderdetails Order informations @@ -7987,7 +7987,7 @@ Element 3 obsolete obsolete - + perm.part.prices Prices @@ -7997,7 +7997,7 @@ Element 3 obsolete obsolete - + perm.part.attachments File attachments @@ -8007,7 +8007,7 @@ Element 3 obsolete obsolete - + perm.part.order Orders @@ -8017,7 +8017,7 @@ Element 3 obsolete obsolete - + perm.storelocations Storelocations @@ -8027,7 +8027,7 @@ Element 3 obsolete obsolete - + perm.move Move @@ -8037,7 +8037,7 @@ Element 3 obsolete obsolete - + perm.list_parts List parts @@ -8047,7 +8047,7 @@ Element 3 obsolete obsolete - + perm.part.footprints Footprints @@ -8057,7 +8057,7 @@ Element 3 obsolete obsolete - + perm.part.categories Categories @@ -8067,7 +8067,7 @@ Element 3 obsolete obsolete - + perm.part.supplier Suppliers @@ -8077,7 +8077,7 @@ Element 3 obsolete obsolete - + perm.part.manufacturers Manufacturers @@ -8087,7 +8087,7 @@ Element 3 obsolete obsolete - + perm.projects Projects @@ -8097,7 +8097,7 @@ Element 3 obsolete obsolete - + perm.part.attachment_types Attachment types @@ -8107,7 +8107,7 @@ Element 3 obsolete obsolete - + perm.tools.import Import @@ -8117,7 +8117,7 @@ Element 3 obsolete obsolete - + perm.tools.labels Labels @@ -8127,7 +8127,7 @@ Element 3 obsolete obsolete - + perm.tools.calculator Resistor calculator @@ -8137,7 +8137,7 @@ Element 3 obsolete obsolete - + perm.tools.footprints Footprints @@ -8147,7 +8147,7 @@ Element 3 obsolete obsolete - + perm.tools.ic_logos IC logos @@ -8157,7 +8157,7 @@ Element 3 obsolete obsolete - + perm.tools.statistics Statistics @@ -8167,7 +8167,7 @@ Element 3 obsolete obsolete - + perm.edit_permissions Edit permissions @@ -8177,7 +8177,7 @@ Element 3 obsolete obsolete - + perm.users.edit_user_name Edit user name @@ -8187,7 +8187,7 @@ Element 3 obsolete obsolete - + perm.users.edit_change_group Change group @@ -8197,7 +8197,7 @@ Element 3 obsolete obsolete - + perm.users.edit_infos Edit infos @@ -8207,7 +8207,7 @@ Element 3 obsolete obsolete - + perm.users.edit_permissions Edit permissions @@ -8217,7 +8217,7 @@ Element 3 obsolete obsolete - + perm.users.set_password Set password @@ -8227,7 +8227,7 @@ Element 3 obsolete obsolete - + perm.users.change_user_settings Change user settings @@ -8237,7 +8237,7 @@ Element 3 obsolete obsolete - + perm.database.see_status Show status @@ -8247,7 +8247,7 @@ Element 3 obsolete obsolete - + perm.database.update_db Update DB @@ -8257,7 +8257,7 @@ Element 3 obsolete obsolete - + perm.database.read_db_settings Read DB settings @@ -8267,7 +8267,7 @@ Element 3 obsolete obsolete - + perm.database.write_db_settings Write DB settings @@ -8277,7 +8277,7 @@ Element 3 obsolete obsolete - + perm.config.read_config Read config @@ -8287,7 +8287,7 @@ Element 3 obsolete obsolete - + perm.config.edit_config Edit config @@ -8297,7 +8297,7 @@ Element 3 obsolete obsolete - + perm.config.server_info Server info @@ -8307,7 +8307,7 @@ Element 3 obsolete obsolete - + perm.config.use_debug Use debug tools @@ -8317,7 +8317,7 @@ Element 3 obsolete obsolete - + perm.show_logs Show logs @@ -8327,7 +8327,7 @@ Element 3 obsolete obsolete - + perm.delete_logs Delete logs @@ -8337,7 +8337,7 @@ Element 3 obsolete obsolete - + perm.self.edit_infos Edit infos @@ -8347,7 +8347,7 @@ Element 3 obsolete obsolete - + perm.self.edit_username Edit username @@ -8357,7 +8357,7 @@ Element 3 obsolete obsolete - + perm.self.show_permissions View permissions @@ -8367,7 +8367,7 @@ Element 3 obsolete obsolete - + perm.self.show_logs Show own log entries @@ -8377,7 +8377,7 @@ Element 3 obsolete obsolete - + perm.self.create_labels Create labels @@ -8387,7 +8387,7 @@ Element 3 obsolete obsolete - + perm.self.edit_options Edit options @@ -8397,7 +8397,7 @@ Element 3 obsolete obsolete - + perm.self.delete_profiles Delete profiles @@ -8407,7 +8407,7 @@ Element 3 obsolete obsolete - + perm.self.edit_profiles Edit profiles @@ -8417,7 +8417,7 @@ Element 3 obsolete obsolete - + perm.part.tools Tools @@ -8427,7 +8427,7 @@ Element 3 obsolete obsolete - + perm.groups Groups @@ -8437,7 +8437,7 @@ Element 3 obsolete obsolete - + perm.users Users @@ -8447,7 +8447,7 @@ Element 3 obsolete obsolete - + perm.database Database @@ -8457,7 +8457,7 @@ Element 3 obsolete obsolete - + perm.config Configuration @@ -8467,7 +8467,7 @@ Element 3 obsolete obsolete - + perm.system System @@ -8477,7 +8477,7 @@ Element 3 obsolete obsolete - + perm.self Own user @@ -8487,7 +8487,7 @@ Element 3 obsolete obsolete - + perm.labels Labels @@ -8497,7 +8497,7 @@ Element 3 obsolete obsolete - + perm.part.category Category @@ -8507,7 +8507,7 @@ Element 3 obsolete obsolete - + perm.part.minamount Minimum amount @@ -8517,7 +8517,7 @@ Element 3 obsolete obsolete - + perm.part.footprint Footprint @@ -8527,7 +8527,7 @@ Element 3 obsolete obsolete - + perm.part.mpn MPN @@ -8537,7 +8537,7 @@ Element 3 obsolete obsolete - + perm.part.status Manufacturing status @@ -8547,7 +8547,7 @@ Element 3 obsolete obsolete - + perm.part.tags Tags @@ -8557,7 +8557,7 @@ Element 3 obsolete obsolete - + perm.part.unit Part unit @@ -8567,7 +8567,7 @@ Element 3 obsolete obsolete - + perm.part.mass Mass @@ -8577,7 +8577,7 @@ Element 3 obsolete obsolete - + perm.part.lots Part lots @@ -8587,7 +8587,7 @@ Element 3 obsolete obsolete - + perm.show_users Show last modifying user @@ -8597,7 +8597,7 @@ Element 3 obsolete obsolete - + perm.currencies Currencies @@ -8607,7 +8607,7 @@ Element 3 obsolete obsolete - + perm.measurement_units Measurement unit @@ -8617,7 +8617,7 @@ Element 3 obsolete obsolete - + user.settings.pw_old.label Old password @@ -8627,7 +8627,7 @@ Element 3 obsolete obsolete - + pw_reset.submit Reset password @@ -8637,7 +8637,7 @@ Element 3 obsolete obsolete - + u2f_two_factor Security key (U2F) @@ -8647,13 +8647,13 @@ Element 3 obsolete obsolete - + google Google - + tfa.provider.webauthn_two_factor_provider Security key @@ -8663,7 +8663,7 @@ Element 3 obsolete obsolete - + tfa.provider.google Authenticator app @@ -8673,7 +8673,7 @@ Element 3 obsolete obsolete - + Login successful Login successful @@ -8683,7 +8683,7 @@ Element 3 obsolete obsolete - + log.type.exception Unhandled exception (obsolete) @@ -8693,7 +8693,7 @@ Element 3 obsolete obsolete - + log.type.user_login User login @@ -8703,7 +8703,7 @@ Element 3 obsolete obsolete - + log.type.user_logout User logout @@ -8713,7 +8713,7 @@ Element 3 obsolete obsolete - + log.type.unknown Unknown @@ -8723,7 +8723,7 @@ Element 3 obsolete obsolete - + log.type.element_created Element created @@ -8733,7 +8733,7 @@ Element 3 obsolete obsolete - + log.type.element_edited Element edited @@ -8743,7 +8743,7 @@ Element 3 obsolete obsolete - + log.type.element_deleted Element deleted @@ -8753,7 +8753,7 @@ Element 3 obsolete obsolete - + log.type.database_updated Database updated @@ -8762,7 +8762,7 @@ Element 3 obsolete - + perm.revert_elements Revert element @@ -8771,7 +8771,7 @@ Element 3 obsolete - + perm.show_history Show history @@ -8780,7 +8780,7 @@ Element 3 obsolete - + perm.tools.lastActivity Show last activity @@ -8789,7 +8789,7 @@ Element 3 obsolete - + perm.tools.timeTravel Show old element versions (time travel) @@ -8798,7 +8798,7 @@ Element 3 obsolete - + log.type. __log.type. @@ -8807,7 +8807,7 @@ Element 3 obsolete - + tfa_u2f.key_added_successful Security key added successfully. @@ -8816,7 +8816,7 @@ Element 3 obsolete - + Username Username @@ -8825,7 +8825,7 @@ Element 3 obsolete - + log.type.security.google_disabled Authenticator App disabled @@ -8834,7 +8834,7 @@ Element 3 obsolete - + log.type.security.u2f_removed Security key removed @@ -8843,7 +8843,7 @@ Element 3 obsolete - + log.type.security.u2f_added Security key added @@ -8852,7 +8852,7 @@ Element 3 obsolete - + log.type.security.backup_keys_reset Backup keys regenerated @@ -8861,7 +8861,7 @@ Element 3 obsolete - + log.type.security.google_enabled Authenticator App enabled @@ -8870,7 +8870,7 @@ Element 3 obsolete - + log.type.security.password_changed Password changed @@ -8879,7 +8879,7 @@ Element 3 obsolete - + log.type.security.trusted_device_reset Trusted devices resetted @@ -8888,7 +8888,7 @@ Element 3 obsolete - + log.type.collection_element_deleted Element of Collection deleted @@ -8897,7 +8897,7 @@ Element 3 obsolete - + log.type.security.password_reset Password reset @@ -8906,7 +8906,7 @@ Element 3 obsolete - + log.type.security.2fa_admin_reset Two Factor Reset by Administrator @@ -8915,7 +8915,7 @@ Element 3 obsolete - + log.type.user_not_allowed Unauthorized access attempt @@ -8924,7 +8924,7 @@ Element 3 obsolete - + log.database_updated.success Success @@ -8933,7 +8933,7 @@ Element 3 obsolete - + label_options.barcode_type.2D 2D @@ -8942,7 +8942,7 @@ Element 3 obsolete - + label_options.barcode_type.1D 1D @@ -8951,7 +8951,7 @@ Element 3 obsolete - + perm.part.parameters Parameters @@ -8960,7 +8960,7 @@ Element 3 obsolete - + perm.attachment_show_private View private attachments @@ -8969,7 +8969,7 @@ Element 3 obsolete - + perm.tools.label_scanner Label scanner @@ -8978,7 +8978,7 @@ Element 3 obsolete - + perm.self.read_profiles Read profiles @@ -8987,7 +8987,7 @@ Element 3 obsolete - + perm.self.create_profiles Create profiles @@ -8996,1978 +8996,1990 @@ Element 3 obsolete - + perm.labels.use_twig Use twig mode - + label_profile.showInDropdown Show in quick select - + group.edit.enforce_2fa Enforce Two-factor authentication (2FA) - + group.edit.enforce_2fa.help If this option is enabled, every direct member of this group, has to configure at least one second-factor for authentication. Recommended for administrative groups with much permissions. - + selectpicker.empty Nothing selected - + selectpicker.nothing_selected Nothing selected - + entity.delete.must_not_contain_parts Element "%PATH%" still contains parts! You have to move the parts, to be able to delete this element. - + entity.delete.must_not_contain_attachments Attachment type still contains attachments. Change their type, to be able to delete this attachment type. - + entity.delete.must_not_contain_prices Currency still contains price details. You have to change their currency to be able to delete this element. - + entity.delete.must_not_contain_users Users still uses this group! Change their group, to be able to delete this group. - + part.table.edit Edit - + part.table.edit.title Edit part - + part_list.action.action.title Select action - + part_list.action.action.group.favorite Favorite status - + part_list.action.action.favorite Favorite - + part_list.action.action.unfavorite Unfavorite - + part_list.action.action.group.change_field Change field - + part_list.action.action.change_category Change category - + part_list.action.action.change_footprint Change footprint - + part_list.action.action.change_manufacturer Change manufacturer - + part_list.action.action.change_unit Change part unit - + part_list.action.action.delete Delete - + part_list.action.submit Submit - + part_list.action.part_count %count% parts selected! - + company.edit.quick.website Open website - + company.edit.quick.email Send email - + company.edit.quick.phone Call phone - + company.edit.quick.fax Send fax - + company.fax_number.placeholder e.g. +49 1234 567890 - + part.edit.save_and_clone Save and clone - + validator.file_ext_not_allowed File extension not allowed for this attachment type. - + tools.reel_calc.title SMD Reel calculator - + tools.reel_calc.inner_dia Inner diameter - + tools.reel_calc.outer_dia Outer diameter - + tools.reel_calc.tape_thick Tape thickness - + tools.reel_calc.part_distance Part distance - + tools.reel_calc.update Update - + tools.reel_calc.parts_per_meter Parts per meter - + tools.reel_calc.result_length Tape length - + tools.reel_calc.result_amount Approx. parts count - + tools.reel_calc.outer_greater_inner_error Error: Outer diameter must be greater than inner diameter! - + tools.reel_calc.missing_values.error Please fill in all values! - + tools.reel_calc.load_preset Load preset - + tools.reel_calc.explanation This calculator gives you an estimation, how many parts are remaining on an SMD reel. Measure the noted the dimensions on the reel (or use some of the presets) and click "Update" to get an result. - + perm.tools.reel_calculator SMD Reel calculator - + tree.tools.tools.reel_calculator SMD Reel calculator - + user.pw_change_needed.flash Your password needs to be changed! Please set a new password. - + tree.root_node.text Root node - + part_list.action.select_null Empty element - + part_list.action.delete-title Do you really want to delete these parts? - + part_list.action.delete-message These parts and any associated information (like attachments, price information, etc.) will be deleted. This can not be undone! - + part.table.actions.success Actions finished successfully. - + attachment.edit.delete.confirm Do you really want to delete this attachment? - + filter.text_constraint.value.operator.EQ Is - + filter.text_constraint.value.operator.NEQ Is not - + filter.text_constraint.value.operator.STARTS Starts with - + filter.text_constraint.value.operator.CONTAINS Contains - + filter.text_constraint.value.operator.ENDS Ends with - + filter.text_constraint.value.operator.LIKE LIKE pattern - + filter.text_constraint.value.operator.REGEX Regular expression - + filter.number_constraint.value.operator.BETWEEN Between - + filter.number_constraint.AND and - + filter.entity_constraint.operator.EQ Is (excluding children) - + filter.entity_constraint.operator.NEQ Is not (excluding children) - + filter.entity_constraint.operator.INCLUDING_CHILDREN Is (including children) - + filter.entity_constraint.operator.EXCLUDING_CHILDREN Is not (excluding children) - + part.filter.dbId Database ID - + filter.tags_constraint.operator.ANY Any of the tags - + filter.tags_constraint.operator.ALL All the tags - + filter.tags_constraint.operator.NONE None of the tags - + part.filter.lot_count Number of lots - + part.filter.attachments_count Number of attachments - + part.filter.orderdetails_count Number of orderdetails - + part.filter.lotExpirationDate Lot expiration date - + part.filter.lotNeedsRefill Any lot needs refill - + part.filter.lotUnknwonAmount Any lot has unknown amount - + part.filter.attachmentName Attachment name - + filter.choice_constraint.operator.ANY Any of - + filter.choice_constraint.operator.NONE None of - + part.filter.amount_sum Total amount - + filter.submit Update - + filter.discard Discard changes - + filter.clear_filters Clear all filters - + filter.title Filter - + filter.parameter_value_constraint.operator.= Typ. Value = - + filter.parameter_value_constraint.operator.!= Typ. Value != - + filter.parameter_value_constraint.operator.< - Typ. Value < + - + filter.parameter_value_constraint.operator.> - Typ. Value > + ]]> - + filter.parameter_value_constraint.operator.<= - Typ. Value <= + - + filter.parameter_value_constraint.operator.>= - Typ. Value >= + =]]> - + filter.parameter_value_constraint.operator.BETWEEN Typ. Value is between - + filter.parameter_value_constraint.operator.IN_RANGE In Value range - + filter.parameter_value_constraint.operator.NOT_IN_RANGE Not in Value range - + filter.parameter_value_constraint.operator.GREATER_THAN_RANGE Greater than Value range - + filter.parameter_value_constraint.operator.GREATER_EQUAL_RANGE Greater equal than Value range - + filter.parameter_value_constraint.operator.LESS_THAN_RANGE Less than Value range - + filter.parameter_value_constraint.operator.LESS_EQUAL_RANGE Less equal than Value range - + filter.parameter_value_constraint.operator.RANGE_IN_RANGE Range is completely in Value range - + filter.parameter_value_constraint.operator.RANGE_INTERSECT_RANGE Range intersects Value range - + filter.text_constraint.value No value set - + filter.number_constraint.value1 No value set - + filter.number_constraint.value2 Maximum value - + filter.datetime_constraint.value1 No datetime set - + filter.datetime_constraint.value2 Maximum datetime - + filter.constraint.add Add constraint - + part.filter.parameters_count Number of parameters - + part.filter.lotDescription Lot description - + parts_list.search.searching_for - Searching parts with keyword <b>%keyword%</b> + %keyword%]]> - + parts_list.search_options.caption Enabled search options - + attachment.table.element_type Associated element type - + log.level.debug Debug - + log.level.info Info - + log.level.notice Notice - + log.level.warning Warning - + log.level.error Error - + log.level.critical Critical - + log.level.alert Alert - + log.level.emergency Emergency - + log.type.security Security related event - + log.type.instock_changed [LEGACY] Instock changed - + log.target_id Target element ID - + entity.info.parts_count_recursive Number of parts with this element or its sub elements - + tools.server_infos.title Server info - + permission.preset.read_only Read-Only - + permission.preset.read_only.desc Only allow read operations on data - + permission.preset.all_inherit Inherit all - + permission.preset.all_inherit.desc Set all permissions to Inherit - + permission.preset.all_forbid Forbid all - + permission.preset.all_forbid.desc Set all permissions to Forbid - + permission.preset.all_allow Allow all - + permission.preset.all_allow.desc Set all permissions to allow - + perm.server_infos Server info - + permission.preset.editor Editor - + permission.preset.editor.desc Allow changing parts and data structures - + permission.preset.admin Admin - + permission.preset.admin.desc Allow administrative actions - + permission.preset.button Apply preset - + perm.attachments.show_private Show private attachments - + perm.attachments.list_attachments Show list of all attachments - + user.edit.permission_success Permission preset applied successfully. Check if the new permissions fit your needs. - + perm.group.data Data - + part_list.action.action.group.needs_review Needs Review - + part_list.action.action.set_needs_review Set Needs Review Status - + part_list.action.action.unset_needs_review Unset Needs Review Status - + part.edit.ipn Internal Part Number (IPN) - + part.ipn.not_defined Not defined - + part.table.ipn IPN - + currency.edit.update_rate Retrieve exchange rate - + currency.edit.exchange_rate_update.unsupported_currency The currency is unsupported by the exchange rate provider. Check your exchange rate provider configuration. - + currency.edit.exchange_rate_update.generic_error Unable to retrieve the exchange rate. Check your exchange rate provider configuration. - + currency.edit.exchange_rate_updated.success Retrieved the exchange rate successfully. - + project.bom.quantity BOM Qty. - + project.bom.mountnames Mount names - + project.bom.name Name - + project.bom.comment Notes - + project.bom.part Part - + project.bom.add_entry Add entry - + part_list.action.group.projects Projects - + part_list.action.projects.add_to_project Add parts to project - + project.bom.delete.confirm Do you really want to delete this BOM entry? - + project.add_parts_to_project Add parts to project BOM - + part.info.add_part_to_project Add this part to a project - + project_bom_entry.label BOM entry - + project.edit.status Project status - + project.status.draft Draft - + project.status.planning Planning - + project.status.in_production In production - + project.status.finished Finished - + project.status.archived Archived - + part.new_build_part.error.build_part_already_exists The project already has a build part! - + project.edit.associated_build_part Associated builds part - + project.edit.associated_build_part.add Add builds part - + project.edit.associated_build.hint This part represents the builds of this project, which are stored somewhere. - + part.info.projectBuildPart.hint This part represents the builds of the following project and is associated with it - + part.is_build_part Is project builds part - + project.info.title Project info - + project.info.bom_entries_count BOM entries - + project.info.sub_projects_count Subprojects - + project.info.bom_add_parts Add BOM entries - + project.info.info.label Info - + project.info.sub_projects.label Subprojects - + project.bom.price Price - + part.info.withdraw_modal.title.withdraw Withdraw parts from lot - + part.info.withdraw_modal.title.add Add parts to lot - + part.info.withdraw_modal.title.move Move parts from lot to another lot - + part.info.withdraw_modal.amount Amount - + part.info.withdraw_modal.move_to Move to - + part.info.withdraw_modal.comment Comment - + part.info.withdraw_modal.comment.hint You can set a comment here describing why you are doing this operation (e.g. for what you need the parts). This info will be saved in the log. - + modal.close Close - + modal.submit Submit - + part.withdraw.success Added/Moved/Withdrawn parts successfully. - + perm.parts_stock Parts Stock - + perm.parts_stock.withdraw Withdraw parts from stock - + perm.parts_stock.add Add parts to stock - + perm.parts_stock.move Move parts between lots - + user.permissions_schema_updated The permission schema of your user were upgraded to the latest version. - + log.type.part_stock_changed Part Stock changed - + log.part_stock_changed.withdraw Stock withdrawn - + log.part_stock_changed.add Stock added - + log.part_stock_changed.move Stock moved - + log.part_stock_changed.comment Comment - + log.part_stock_changed.change Change - + log.part_stock_changed.move_target Move target - + tools.builtin_footprints_viewer.title Builtin footprint image gallery - + tools.builtin_footprints_viewer.hint This gallery lists all available built-in footprint images. If you want to use them in an attachment, type in the name (or a keyword) in the path field of the attachment and select the image from the dropdown select. - + tools.ic_logos.title IC logos - + part_list.action.group.labels Labels - + part_list.action.projects.generate_label Generate labels (for parts) - + part_list.action.projects.generate_label_lot Generate labels (for part lots) - + part_list.action.generate_label.empty Empty label - + project.info.builds.label Build - + project.builds.build_not_possible Build not possible: Parts not stocked - + project.builds.following_bom_entries_miss_instock The following parts have not enough stock to build this project at least once: - + project.builds.stocked stocked - + project.builds.needed needed - + project.builds.build_possible Build possible - + project.builds.number_of_builds_possible - You have enough stocked to build <b>%max_builds%</b> builds of this project. + %max_builds% builds of this project.]]> - + project.builds.check_project_status - The current project status is <b>"%project_status%"</b>. You should check if you really want to build the project with this status! + "%project_status%". You should check if you really want to build the project with this status!]]> - + project.builds.following_bom_entries_miss_instock_n You do not have enough parts stocked to build this project %number_of_builds% times. The following parts have missing instock: - + project.build.flash.invalid_input Can not build project. Check input! - + project.build.required_qty Required quantity - + project.build.btn_build Build - + project.build.help Choose from which part lots the stock to build this project should be taken (and in which amount). Check the checkbox for each BOM Entry, when you are finished withdrawing the parts, or use the top checkbox to check all boxes at once. - + project.build.buildsPartLot.new_lot Create new lot - + project.build.add_builds_to_builds_part Add builds to project builds part - + project.build.builds_part_lot Target lot - + project.builds.number_of_builds Build amount - + project.builds.no_stocked_builds Number of stocked builds - + user.change_avatar.label Change profile picture - + user_settings.change_avatar.label Change profile picture - + user_settings.remove_avatar.label Remove profile picture - + part.edit.name.category_hint Hint from category - + category.edit.partname_regex.placeholder e.g "/Capacitor \d+ nF/i" - + category.edit.partname_regex.help A PCRE-compatible regular expression, which a part name have to match. - + entity.select.add_hint - Use -> to create nested structures, e.g. "Node 1->Node 1.1" + to create nested structures, e.g. "Node 1->Node 1.1"]]> - + entity.select.group.new_not_added_to_DB New (not added to DB yet) - + part.edit.save_and_new Save and create new empty part - + homepage.first_steps.title First steps - + homepage.first_steps.introduction - Your database is still empty. You might want to read the <a href="%url%">documentation</a> or start to creating the following data structures: + documentation or start to creating the following data structures:]]> - + homepage.first_steps.create_part - Or you can directly <a href="%url%">create a new part</a>. + create a new part.]]> - + homepage.first_steps.hide_hint This box will hide as soon as you have created your first part. - + homepage.forum.text - For questions about Part-DB use the <a href="%href%" class="link-external" target="_blank">discussion forum</a> + discussion forum]]> - + log.element_edited.changed_fields.category Category - + log.element_edited.changed_fields.footprint Footprint - + log.element_edited.changed_fields.manufacturer Manufacturer - + log.element_edited.changed_fields.value_typical typ. value - + log.element_edited.changed_fields.pw_reset_expires Password reset - + log.element_edited.changed_fields.comment Notes - + log.element_edited.changed_fields.supplierpartnr Supplier part number - + log.element_edited.changed_fields.supplier_product_url Link to offer - + log.element_edited.changed_fields.price Price - + log.element_edited.changed_fields.min_discount_quantity Minimum discount amount - + log.element_edited.changed_fields.original_filename Original filename - + log.element_edited.changed_fields.path Filepath - + log.element_edited.changed_fields.description Description - + log.element_edited.changed_fields.manufacturing_status Manufacturing status - + log.element_edited.changed_fields.options.barcode_type Barcode type - + log.element_edited.changed_fields.status Status - + log.element_edited.changed_fields.quantity BOM Qty. - + log.element_edited.changed_fields.mountnames Mountnames - + log.element_edited.changed_fields.name Name - + log.element_edited.changed_fields.part Part - + log.element_edited.changed_fields.price_currency Currency of price - + log.element_edited.changed_fields.partname_hint Part name hint - + log.element_edited.changed_fields.partname_regex Name filter - + log.element_edited.changed_fields.disable_footprints Disable footprints - + log.element_edited.changed_fields.disable_manufacturers Disable manufacturers - + log.element_edited.changed_fields.disable_autodatasheets Disable automatic datasheet links - + log.element_edited.changed_fields.disable_properties Disable properties - + log.element_edited.changed_fields.default_description Default description - + log.element_edited.changed_fields.default_comment Default notes - + log.element_edited.changed_fields.filetype_filter Allowed file extensions - + log.element_edited.changed_fields.not_selectable Not selected - + log.element_edited.changed_fields.parent Parent element - + log.element_edited.changed_fields.shipping_costs Shipping costs - + log.element_edited.changed_fields.default_currency Default currency - + log.element_edited.changed_fields.address Address - + log.element_edited.changed_fields.phone_number Phone number - + log.element_edited.changed_fields.fax_number Fax number - + log.element_edited.changed_fields.email_address Email - + log.element_edited.changed_fields.website Website - + log.element_edited.changed_fields.auto_product_url Product URL - + log.element_edited.changed_fields.is_full Storelocation full - + log.element_edited.changed_fields.limit_to_existing_parts Limit to existing parts - + log.element_edited.changed_fields.only_single_part Only single part - + log.element_edited.changed_fields.storage_type Storage type - + log.element_edited.changed_fields.footprint_3d 3D model - + log.element_edited.changed_fields.master_picture_attachment Preview image - + log.element_edited.changed_fields.exchange_rate Exchange rate - + log.element_edited.changed_fields.iso_code Exchange rate - + log.element_edited.changed_fields.unit Unit symbol - + log.element_edited.changed_fields.is_integer Is integer - + log.element_edited.changed_fields.use_si_prefix Use SI prefix - + log.element_edited.changed_fields.options.width Width - + log.element_edited.changed_fields.options.height Height - + log.element_edited.changed_fields.options.supported_element Target type - + log.element_edited.changed_fields.options.additional_css Additional styles (CSS) - + log.element_edited.changed_fields.options.lines Content - + log.element_edited.changed_fields.permissions.data Permissions - + log.element_edited.changed_fields.disabled Disabled - + log.element_edited.changed_fields.theme Theme - + log.element_edited.changed_fields.timezone Timezone - + log.element_edited.changed_fields.language Language - + log.element_edited.changed_fields.email Email - + log.element_edited.changed_fields.department Department - + log.element_edited.changed_fields.last_name Last name - + log.element_edited.changed_fields.first_name First name - + log.element_edited.changed_fields.group Group - + log.element_edited.changed_fields.currency Preferred currency - + log.element_edited.changed_fields.enforce2FA Enforce 2FA - + log.element_edited.changed_fields.symbol Symbol - + log.element_edited.changed_fields.value_min Min. value - + log.element_edited.changed_fields.value_max Max. value - + log.element_edited.changed_fields.value_text Text value - + log.element_edited.changed_fields.show_in_table Show in table - + log.element_edited.changed_fields.attachment_type Show in table - + log.element_edited.changed_fields.needs_review Needs review - + log.element_edited.changed_fields.tags Tags - + log.element_edited.changed_fields.mass Mass - + log.element_edited.changed_fields.ipn IPN - + log.element_edited.changed_fields.favorite Favorite - + log.element_edited.changed_fields.minamount Minimum stock - + log.element_edited.changed_fields.manufacturer_product_url Link to product page - + log.element_edited.changed_fields.manufacturer_product_number MPN - + log.element_edited.changed_fields.partUnit Measuring Unit - + log.element_edited.changed_fields.expiration_date Expiration date - + log.element_edited.changed_fields.amount Amount - + log.element_edited.changed_fields.storage_location Storage location + + + user.saml_user + SSO / SAML user + + + + + user.saml_user.pw_change_hint + Your user uses single sign-on (SSO). You can not change the password and 2FA settings here. Configure them on your central SSO provider instead! + + diff --git a/translations/security.en.xlf b/translations/security.en.xlf index e0bb95f4..8c76136f 100644 --- a/translations/security.en.xlf +++ b/translations/security.en.xlf @@ -2,7 +2,7 @@ - + user.login_error.user_disabled Your account is disabled! Contact an administrator if you think this is wrong. diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index e639fa67..3adcb333 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -37,7 +37,7 @@ Part-DB1\src\Entity\UserSystem\Group.php:0 Part-DB1\src\Entity\UserSystem\User.php:0 - + part.master_attachment.must_be_picture The preview attachment must be a valid picture! @@ -82,7 +82,7 @@ src\Entity\StructuralDBElement.php:0 src\Entity\Supplier.php:0 - + structural.entity.unique_name An element with this name already exists on this level! @@ -102,7 +102,7 @@ Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0 Part-DB1\src\Entity\Parameters\SupplierParameter.php:0 - + parameters.validator.min_lesser_typical Value must be lesser or equal the the typical value ({{ compared_value }}). @@ -122,7 +122,7 @@ Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0 Part-DB1\src\Entity\Parameters\SupplierParameter.php:0 - + parameters.validator.min_lesser_max Value must be lesser than the maximum value ({{ compared_value }}). @@ -142,7 +142,7 @@ Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0 Part-DB1\src\Entity\Parameters\SupplierParameter.php:0 - + parameters.validator.max_greater_typical Value must be greater or equal than the typical value ({{ compared_value }}). @@ -152,7 +152,7 @@ Part-DB1\src\Entity\UserSystem\User.php:0 Part-DB1\src\Entity\UserSystem\User.php:0 - + validator.user.username_already_used A user with this name is already exisiting @@ -162,7 +162,7 @@ Part-DB1\src\Entity\UserSystem\User.php:0 Part-DB1\src\Entity\UserSystem\User.php:0 - + user.invalid_username The username must contain only letters, numbers, underscores, dots, pluses or minuses. @@ -171,7 +171,7 @@ obsolete - + validator.noneofitschild.self An element can not be its own parent. @@ -180,121 +180,121 @@ obsolete - + validator.noneofitschild.children You can not assign children element as parent (This would cause loops). - + validator.select_valid_category Please select a valid category! - + validator.part_lot.only_existing Can not add new parts to this location as it is marked as "Only Existing" - + validator.part_lot.location_full.no_increase Location is full. Amount can not be increased (new value must be smaller than {{ old_amount }}). - + validator.part_lot.location_full Location is full. Can not add new parts to it. - + validator.part_lot.single_part This location can only contain a single part and it is already full! - + validator.attachment.must_not_be_null You must select an attachment type! - + validator.orderdetail.supplier_must_not_be_null You must select an supplier! - + validator.measurement_unit.use_si_prefix_needs_unit To enable SI prefixes, you have to set a unit symbol! - + part.ipn.must_be_unique The internal part number must be unique. {{ value }} is already in use! - + validator.project.bom_entry.name_or_part_needed You have to choose a part for a part BOM entry or set a name for a non-part BOM entry. - + project.bom_entry.name_already_in_bom There is already an BOM entry with this name! - + project.bom_entry.part_already_in_bom This part already exists in the BOM! - + project.bom_entry.mountnames_quantity_mismatch The number of mountnames has to match the BOMs quantity! - + project.bom_entry.can_not_add_own_builds_part You can not add a project's own builds part to the BOM. - + project.bom_has_to_include_all_subelement_parts The project BOM has to include all subprojects builds parts. Part %part_name% of project %project_name% missing! - + project.bom_entry.price_not_allowed_on_parts Prices are not allowed on BOM entries associated with a part. Define the price on the part instead. - + validator.project_build.lot_bigger_than_needed You have selected more quantity to withdraw than needed! Remove unnecessary quantity. - + validator.project_build.lot_smaller_than_needed You have selected less quantity to withdraw than needed for the build! Add additional quantity. - + part.name.must_match_category_regex The part name does not match the regular expression stated by the category: %regex% From 60f926924b53cd8af0bd960e92ddb299376bfce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 21 Feb 2023 00:42:03 +0100 Subject: [PATCH 04/25] Add a specific role to SAML user --- src/Entity/UserSystem/User.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Entity/UserSystem/User.php b/src/Entity/UserSystem/User.php index 5b336e4b..47b2f0a4 100644 --- a/src/Entity/UserSystem/User.php +++ b/src/Entity/UserSystem/User.php @@ -306,6 +306,10 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe // guarantee every user at least has ROLE_USER $roles[] = 'ROLE_USER'; + if ($this->saml_user) { + $roles[] = 'ROLE_SAML_USER'; + } + return array_unique($roles); } From e064ee42632959de7b03996556580e6eb5616076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 21 Feb 2023 21:58:27 +0100 Subject: [PATCH 05/25] Prevent change of password of SAML users via CLI --- src/Command/User/SetPasswordCommand.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Command/User/SetPasswordCommand.php b/src/Command/User/SetPasswordCommand.php index aeab0d36..66e6e97e 100644 --- a/src/Command/User/SetPasswordCommand.php +++ b/src/Command/User/SetPasswordCommand.php @@ -78,6 +78,11 @@ class SetPasswordCommand extends Command $io->note('User found!'); + if ($user->isSamlUser()) { + $io->error('This user is a SAML user, so you can not change the password!'); + return 1; + } + $proceed = $io->confirm( sprintf('You are going to change the password of %s with ID %d. Proceed?', $user->getFullName(true), $user->getID())); From b13655e95177d150cbe1457450d0d088d73a5816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 21 Feb 2023 22:36:43 +0100 Subject: [PATCH 06/25] Prevent login of local users via SSO with the same username --- .../EnsureSAMLUserForSAMLLoginChecker.php | 63 +++++++++++++++++++ src/Security/UserChecker.php | 2 +- translations/security.en.xlf | 6 ++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/Security/EnsureSAMLUserForSAMLLoginChecker.php diff --git a/src/Security/EnsureSAMLUserForSAMLLoginChecker.php b/src/Security/EnsureSAMLUserForSAMLLoginChecker.php new file mode 100644 index 00000000..a65e6146 --- /dev/null +++ b/src/Security/EnsureSAMLUserForSAMLLoginChecker.php @@ -0,0 +1,63 @@ +. + */ + +namespace App\Security; + +use App\Entity\UserSystem\User; +use Hslavich\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException; +use Symfony\Contracts\Translation\TranslatorInterface; + +class EnsureSAMLUserForSAMLLoginChecker implements EventSubscriberInterface +{ + private TranslatorInterface $translator; + + public function __construct(TranslatorInterface $translator) + { + $this->translator = $translator; + } + + public static function getSubscribedEvents() + { + return [ + AuthenticationSuccessEvent::class => 'onAuthenticationSuccess', + ]; + } + + public function onAuthenticationSuccess(AuthenticationSuccessEvent $event) + { + $token = $event->getAuthenticationToken(); + $user = $token->getUser(); + + //If we are using SAML, we need to check that the user is a SAML user. + if ($token instanceof SamlToken) { + if ($user instanceof User && !$user->isSAMLUser()) { + throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_local_user_per_saml', [], 'security')); + } + } else { //Ensure that you can not login locally with a SAML user (even if this should not happen, as the password is not set) + if ($user instanceof User && $user->isSamlUser()) { + throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_saml_user_locally', [], 'security')); + } + } + } +} \ No newline at end of file diff --git a/src/Security/UserChecker.php b/src/Security/UserChecker.php index d42d3390..ae8e3f34 100644 --- a/src/Security/UserChecker.php +++ b/src/Security/UserChecker.php @@ -63,7 +63,7 @@ final class UserChecker implements UserCheckerInterface //Check if user is disabled. Then dont allow login if ($user->isDisabled()) { //throw new DisabledException(); - throw new CustomUserMessageAccountStatusException($this->translator->trans('user.login_error.user_disabled')); + throw new CustomUserMessageAccountStatusException($this->translator->trans('user.login_error.user_disabled', [], 'security')); } } } diff --git a/translations/security.en.xlf b/translations/security.en.xlf index 8c76136f..2c9d8957 100644 --- a/translations/security.en.xlf +++ b/translations/security.en.xlf @@ -7,5 +7,11 @@ Your account is disabled! Contact an administrator if you think this is wrong. + + + saml.error.cannot_login_local_user_per_saml + You can not login as local user via SSO! Use your local user password instead. + + From 91fb861fd34f06b36e09cfe69ff74666d94abac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 21 Feb 2023 23:11:16 +0100 Subject: [PATCH 07/25] Use login form page to show error messages on Part-DB side --- config/packages/hslavich_onelogin_saml.yaml | 10 +++++----- config/packages/security.yaml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/config/packages/hslavich_onelogin_saml.yaml b/config/packages/hslavich_onelogin_saml.yaml index d33376d3..55033396 100644 --- a/config/packages/hslavich_onelogin_saml.yaml +++ b/config/packages/hslavich_onelogin_saml.yaml @@ -15,14 +15,14 @@ hslavich_onelogin_saml: url: 'http://localhost:8000/saml/acs' binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' singleLogoutService: - url: 'http://localhost:8000/saml/logout' + url: 'http://localhost:8000/logout' binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' x509cert: 'MIICmzCCAYMCBgGGcIfLmDANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZwYXJ0ZGIwHhcNMjMwMjIwMjAzMTMzWhcNMzMwMjIwMjAzMzEzWjARMQ8wDQYDVQQDDAZwYXJ0ZGIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCmz9Lr8PxVjqqhGjjH4FbhVDbEPIplcQ1cFhTKu1NYpwHIm01pNnEwiyoLMBrZ16qq0PvCX+KCIjuJbC44k6N/+UCjbSZ8Fu+vyT9OkKVLvimc5sg9m9Kys+WiuaxzSEa2dA88khyGb6O9huuVYjibjROAN5I20h8f0FLfvolqWfEXVM00LIYYCHcvKNGq6PH0SS+2P6CGHoRrEFOoi7v7kkL6gWqYzPA/T7GSb78W14gU6FKcvQXPYOdsL7dYDy634BCd9k/tNF6JkiFi9U1IP8LEYkQUq3mKGRSjoPKKztws00ol3Nfd04XAdPKxIb3ff3io0vONE3QfJsgTzdR7AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEkYH4pCKxqoABjYGRml+ea5nteF/qsff6qHcNU8Unw1XLZO7ES1GU4Ij3RSFdt1SoKnphXnBtlVIechkOnOaECGs4RdbG4RGSFfusiEcWHbp1C0n3cnaUoV0/PJ3iETeeaPduqz9I/dlwhOxEOkpCaomYm4j2hfNcI9pmdM8bZuG+6dedh02ILrufQiCREmF6Sp+AOjueoFOoGwsXMbTuKo9pNizCO2OTT6nrjbNhjzoIvzbmlqiOGFs7YmpyTHcyqm66ACOnu1Nq9z9QLGkhvwjAmEytp6A9yEEkl0QO+DVhlAnvfdSMaR9jgyGtnBXQGOHv4osPtCtrk3IU4nB3s=' privateKey: 'MIIEogIBAAKCAQEAps/S6/D8VY6qoRo4x+BW4VQ2xDyKZXENXBYUyrtTWKcByJtNaTZxMIsqCzAa2deqqtD7wl/igiI7iWwuOJOjf/lAo20mfBbvr8k/TpClS74pnObIPZvSsrPlormsc0hGtnQPPJIchm+jvYbrlWI4m40TgDeSNtIfH9BS376JalnxF1TNNCyGGAh3LyjRqujx9Ekvtj+ghh6EaxBTqIu7+5JC+oFqmMzwP0+xkm+/FteIFOhSnL0Fz2DnbC+3WA8ut+AQnfZP7TReiZIhYvVNSD/CxGJEFKt5ihkUo6Dyis7cLNNKJdzX3dOFwHTysSG93394qNLzjRN0HybIE83UewIDAQABAoIBAA1M7dLtPJlvzjAZQKTDQPondlRwRVKwUHHiutatWAhuDIjbxTDZ6+2Ecx5AQCvVc+C52BEYBx38L8YVz5uoPfWiwKInPlXPmF3qTHdtthhTecruZdHvvj2MdYdjiZoJjcXXfC2GsuqPNT2T5+3Zzoysk3z6MVjYqS2mtSzs6tUFZOY13/zMnrELjR2dFrigndjQgb5vENJ+/WNH4k9rt7KGDaMniJWHi3bDDNwilR0XrwhJVav0PIl7h8bMk95ixqo5B+RN0EMhfS+j3+8QJhYbGzXIyJbgMGTUGb23tBIDl6RFblLoic/PEVpA5yhVbLtdoMy5Mn+Y28yZPh6/VB0CgYEA1xtwZVbrRObv76bIkqgCXEYbiXhbFHzGTaI23UEBrV60VNr5LZl/vqA6JowDFUYl30Ytzr7TvC5KwfrNhaYOqVd4rMg145cn8J61bRHArMaXouVYVwWKEni50gCktVy8Tt2Nbzfja2rccCqv3JM2HHp49+e9CLi/D95XPAip3vcCgYEAxoYB7/OQmGMHnqwz2CUnLhGcnC1X4fZ+EJuRo1BXRcntCS4lwXCoyC3Es+YLZ6tfhUp17i/HaT3tJyp6F+EM43zIxwyDVCl6t0yFkjLHRdZHR3xGMMfcMLZGRY99Y25QyW1/GJFKff7D+rKvrpBVcl+9xLYcpIN6ULuHqs9f4Z0CgYBrDL2/wSTusltAEemJitE56K31mQ8CwCHUKuFQ9PQHurTV8e/F8LkxPf4ShuVV5gYc+oj7dd5brVII/W7gj0aGogBtRGoFLIl05xb1A7u2gFKgf7CaBiizjp8zUpyloVQZj4q+ibrFD3ZK4AOLKzvnqk+fWBWsTHzRQd56Avm++wKBgF3eR1Q6CojDanrwWaM+DgSOd0qxdfh2IK2hoX9jIaDyFY5dr6SDrIraeUPG5mWidowD5Tc2iEeO7G+0ef6IfxuhiR31ILPO2SOKny29rNOsug9nB5lRJyAxT5DchCFbq/9SMuJe8KYarHgBvWgA/yYRdx1oLqrrMA60XTW60E9RAoGALmoMX1CLe7BBrqaZrHBAqkV/6koTPSMuYVUZpbKgTmA7VsBScUZaX8huS7ARjLfoujxcNuFT4haPlY7gybvhEN0dMlqRwVNeiD3jo+PdDxu3h9SM6GTuVks1XWoifHBRj2NngBEipV3WDXLJR2sAbUArUdL58y12f7EWFdX41+s=' # Optional settings #baseurl: 'http://myapp.com' - #strict: true - debug: true + strict: true + debug: false security: allowRepeatAttributeName: true # nameIdEncrypted: false @@ -52,6 +52,6 @@ hslavich_onelogin_saml: # emailAddress: 'administrativeuser@example.com' #organization: # en: - # name: 'Example' - # displayname: 'Example' + # name: 'Part-DB-name' + # displayname: 'Displayname' # url: 'http://example.com' \ No newline at end of file diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 6758cf80..7e0ded3c 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -36,7 +36,7 @@ security: persist_user: true check_path: saml_acs login_path: saml_login - failure_path: saml_login + failure_path: login # https://symfony.com/doc/current/security/form_login_setup.html form_login: From 586a57c2c97741ce4bbdb59b84ca13996402eda0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 21 Feb 2023 23:41:02 +0100 Subject: [PATCH 08/25] Allow X500 attributes for user info and added some tests --- config/services_test.yaml | 9 +++ src/Entity/UserSystem/User.php | 11 +++ .../EnsureSAMLUserForSAMLLoginChecker.php | 2 +- src/Security/SamlUserFactory.php | 3 +- tests/Entity/UserSystem/UserTest.php | 36 ++++++++++ .../EnsureSAMLUserForSAMLLoginCheckerTest.php | 70 +++++++++++++++++++ tests/Security/SamlUserFactoryTest.php | 65 +++++++++++++++++ 7 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 config/services_test.yaml create mode 100644 tests/Security/EnsureSAMLUserForSAMLLoginCheckerTest.php create mode 100644 tests/Security/SamlUserFactoryTest.php diff --git a/config/services_test.yaml b/config/services_test.yaml new file mode 100644 index 00000000..e29da6fc --- /dev/null +++ b/config/services_test.yaml @@ -0,0 +1,9 @@ +# Service overrides for the test environment + +services: + saml_user_factory: + class: App\Security\SamlUserFactory + public: true + + App\Security\SamlUserFactory: + public: true \ No newline at end of file diff --git a/src/Entity/UserSystem/User.php b/src/Entity/UserSystem/User.php index 47b2f0a4..eddf9179 100644 --- a/src/Entity/UserSystem/User.php +++ b/src/Entity/UserSystem/User.php @@ -912,5 +912,16 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe if (isset($attributes['department'])) { $this->setDepartment($attributes['department'][0]); } + + //Use X500 attributes as userinfo + if (isset($attributes['urn:oid:2.5.4.42'])) { + $this->setFirstName($attributes['urn:oid:2.5.4.42'][0]); + } + if (isset($attributes['urn:oid:2.5.4.4'])) { + $this->setLastName($attributes['urn:oid:2.5.4.4'][0]); + } + if (isset($attributes['urn:oid:1.2.840.113549.1.9.1'])) { + $this->setEmail($attributes['urn:oid:1.2.840.113549.1.9.1'][0]); + } } } diff --git a/src/Security/EnsureSAMLUserForSAMLLoginChecker.php b/src/Security/EnsureSAMLUserForSAMLLoginChecker.php index a65e6146..1d4c3cfe 100644 --- a/src/Security/EnsureSAMLUserForSAMLLoginChecker.php +++ b/src/Security/EnsureSAMLUserForSAMLLoginChecker.php @@ -44,7 +44,7 @@ class EnsureSAMLUserForSAMLLoginChecker implements EventSubscriberInterface ]; } - public function onAuthenticationSuccess(AuthenticationSuccessEvent $event) + public function onAuthenticationSuccess(AuthenticationSuccessEvent $event): void { $token = $event->getAuthenticationToken(); $user = $token->getUser(); diff --git a/src/Security/SamlUserFactory.php b/src/Security/SamlUserFactory.php index 06e31264..d212f78b 100644 --- a/src/Security/SamlUserFactory.php +++ b/src/Security/SamlUserFactory.php @@ -31,13 +31,12 @@ class SamlUserFactory implements SamlUserFactoryInterface $user = new User(); $user->setName($username); $user->setNeedPwChange(false); - $user->setPassword('$$SAML$$'); + $user->setPassword('!!SAML!!'); //This is a SAML user now! $user->setSamlUser(true); $user->setSamlAttributes($attributes); - return $user; } } \ No newline at end of file diff --git a/tests/Entity/UserSystem/UserTest.php b/tests/Entity/UserSystem/UserTest.php index 89aab673..456457a4 100644 --- a/tests/Entity/UserSystem/UserTest.php +++ b/tests/Entity/UserSystem/UserTest.php @@ -148,4 +148,40 @@ class UserTest extends TestCase } $this->assertFalse($user->isWebAuthnAuthenticatorEnabled()); } + + public function testSetSAMLAttributes(): void + { + $data = [ + 'firstName' => ['John'], + 'lastName' => ['Doe'], + 'email' => ['j.doe@invalid.invalid'], + 'department' => ['Test Department'], + ]; + + $user = new User(); + $user->setSAMLAttributes($data); + + //Test if the data was set correctly + $this->assertSame('John', $user->getFirstName()); + $this->assertSame('Doe', $user->getLastName()); + $this->assertSame('j.doe@invalid.invalid', $user->getEmail()); + $this->assertSame('Test Department', $user->getDepartment()); + + //Test that it works for X500 attributes + $data = [ + 'urn:oid:2.5.4.42' => ['Jane'], + 'urn:oid:2.5.4.4' => ['Dane'], + 'urn:oid:1.2.840.113549.1.9.1' => ['mail@invalid.invalid'], + ]; + + $user->setSAMLAttributes($data); + + //Data must be changed + $this->assertSame('Jane', $user->getFirstName()); + $this->assertSame('Dane', $user->getLastName()); + $this->assertSame('mail@invalid.invalid', $user->getEmail()); + + //Department must not be changed + $this->assertSame('Test Department', $user->getDepartment()); + } } diff --git a/tests/Security/EnsureSAMLUserForSAMLLoginCheckerTest.php b/tests/Security/EnsureSAMLUserForSAMLLoginCheckerTest.php new file mode 100644 index 00000000..193011cd --- /dev/null +++ b/tests/Security/EnsureSAMLUserForSAMLLoginCheckerTest.php @@ -0,0 +1,70 @@ +. + */ + +namespace App\Tests\Security; + +use App\Entity\UserSystem\User; +use App\Security\EnsureSAMLUserForSAMLLoginChecker; +use Hslavich\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken; +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; +use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException; + +class EnsureSAMLUserForSAMLLoginCheckerTest extends WebTestCase +{ + /** @var EnsureSAMLUserForSAMLLoginChecker */ + protected $service; + + protected function setUp(): void + { + self::bootKernel(); + $this->service = self::getContainer()->get('saml_user_factory'); + } + + public function testOnAuthenticationSuccessFailsOnSSOLoginWithLocalUser(): void + { + $local_user = new User(); + + $saml_token = $this->createMock(SamlToken::class); + $saml_token->method('getUser')->willReturn($local_user); + + $event = new AuthenticationSuccessEvent($saml_token); + + $this->expectException(CustomUserMessageAccountStatusException::class); + + $this->service->onAuthenticationSuccess($event); + } + + public function testOnAuthenticationSuccessFailsOnLocalLoginWithSAMLUser(): void + { + $saml_user = (new User())->setSamlUser(true); + + $saml_token = $this->createMock(UsernamePasswordToken::class); + $saml_token->method('getUser')->willReturn($saml_user); + + $event = new AuthenticationSuccessEvent($saml_token); + + $this->expectException(CustomUserMessageAccountStatusException::class); + + $this->service->onAuthenticationSuccess($event); + } +} diff --git a/tests/Security/SamlUserFactoryTest.php b/tests/Security/SamlUserFactoryTest.php new file mode 100644 index 00000000..6127237a --- /dev/null +++ b/tests/Security/SamlUserFactoryTest.php @@ -0,0 +1,65 @@ +. + */ + +namespace App\Tests\Security; + +use App\Entity\UserSystem\User; +use App\Security\SamlUserFactory; +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +class SamlUserFactoryTest extends WebTestCase +{ + + /** @var SamlUserFactory */ + protected $service; + + protected function setUp(): void + { + self::bootKernel(); + $this->service = self::getContainer()->get(SamlUserFactory::class); + } + + public function testCreateUser() + { + $user = $this->service->createUser('sso_user', [ + 'email' => ['j.doe@invalid.invalid'], + 'urn:oid:2.5.4.42' => ['John'], + 'urn:oid:2.5.4.4' => ['Doe'], + 'department' => ['IT'] + ]); + + $this->assertInstanceOf(User::class, $user); + + $this->assertEquals('sso_user', $user->getUsername()); + //User must not change his password + $this->assertFalse($user->isNeedPwChange()); + //And must not be disabled + $this->assertFalse($user->isDisabled()); + //Password should not be set + $this->assertSame('!!SAML!!', $user->getPassword()); + + //Info should be set + $this->assertEquals('John', $user->getFirstName()); + $this->assertEquals('Doe', $user->getLastName()); + $this->assertEquals('IT', $user->getDepartment()); + $this->assertEquals('j.doe@invalid.invalid', $user->getEmail()); + } +} From c5904303e3ca6dc69537ce8dcdd27e40da318b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Wed, 22 Feb 2023 00:50:51 +0100 Subject: [PATCH 09/25] Allow to configure SAML via env variables --- .env | 29 +++++++++++++++++++++ VERSION | 2 +- config/packages/hslavich_onelogin_saml.yaml | 21 ++++++++------- config/packages/routing.yaml | 2 +- config/packages/twig.yaml | 1 + config/parameters.yaml | 9 +++++++ config/routes/hslavich_saml.yaml | 4 ++- templates/security/login.html.twig | 9 ++++++- translations/messages.en.xlf | 12 +++++++++ 9 files changed, 76 insertions(+), 13 deletions(-) diff --git a/.env b/.env index 0e8adff6..5906397b 100644 --- a/.env +++ b/.env @@ -32,6 +32,10 @@ ALLOW_ATTACHMENT_DOWNLOADS=0 # Use gravatars for user avatars, when user has no own avatar defined USE_GRAVATAR=0 +# The public reachable URL of this Part-DB installation. This is used for generating links to the website in emails and so on +# This must end with a slash! +DEFAULT_URI="https://partdb.changeme.invalid/" + ################################################################################### # Email settings ################################################################################### @@ -69,6 +73,31 @@ ERROR_PAGE_ADMIN_EMAIL='' # If this is set to true, solutions to common problems are shown on error pages. Disable this, if you do not want your users to see them... ERROR_PAGE_SHOW_HELP=1 +################################################################################### +# SAML Single sign on-settings +################################################################################### +# Set this to 1 to enable SAML single sign on +SAML_ENABLED=0 + +# The entity ID of your SAML IDP (e.g. the realm name of your Keycloak server) +SAML_IDP_ENTITY_ID="https://idp.changeme.invalid/realms/master" +# The URL of your SAML IDP SingleSignOnService (e.g. the endpoint of your Keycloak server) +SAML_IDP_SINGLE_SIGN_ON_SERVICE="https://idp.changeme.invalid/realms/master/protocol/saml" +# The URL of your SAML IDP SingleLogoutService (e.g. the endpoint of your Keycloak server) +SAML_IDP_SINGLE_LOGOUT_SERVICE="https://idp.changeme.invalid/realms/master/protocol/saml" +# The public certificate of the SAML IDP (e.g. the certificate of your Keycloak server) +SAML_IDP_X509_CERT="MIIC..." + +# The entity of your SAML SP, must match the SP entityID configured in your SAML IDP (e.g. Keycloak). +# This should be a the domain name of your Part-DB installation, followed by "/sp" +SAML_SP_ENTITY_ID="https://partdb.changeme.invalid/sp" + +# The public certificate of the SAML SP +SAML_SP_X509_CERT="MIIC..." +# The private key of the SAML SP +SAMLP_SP_PRIVATE_KEY="MIIE..." + + ###################################################################################### # Other settings ###################################################################################### diff --git a/VERSION b/VERSION index 7dea76ed..336c3677 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.1 +1.1.0-dev diff --git a/config/packages/hslavich_onelogin_saml.yaml b/config/packages/hslavich_onelogin_saml.yaml index 55033396..cae3c539 100644 --- a/config/packages/hslavich_onelogin_saml.yaml +++ b/config/packages/hslavich_onelogin_saml.yaml @@ -1,24 +1,27 @@ +# See https://github.com/SAML-Toolkits/php-saml for more information about the SAML settings + hslavich_onelogin_saml: # Basic settings idp: - entityId: 'http://localhost:8080/realms/master' + entityId: '%env(string:SAML_IDP_ENTITY_ID)%' singleSignOnService: - url: 'http://localhost:8080/realms/master/protocol/saml' + url: '%env(string:SAML_IDP_SINGLE_SIGN_ON_SERVICE)%' binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' singleLogoutService: - url: 'http://localhost:8080/realms/master/protocol/saml' + url: '%env(string:SAML_IDP_SINGLE_LOGOUT_SERVICE)%' binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' - x509cert: 'MIICmzCCAYMCBgGGcG8PJTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjMwMjIwMjAwNDMyWhcNMzMwMjIwMjAwNjEyWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQAle3ob0ary+Hq+mr2IvGueJicIxNqGeG/eV+NpoHUVggHSdb+9kudy+Os0xhAtz8nffTc8T5PK09GClXy7O5mAg8X9E5p0YeRZOxqgBXXVEtgPXaliD2N2mVrY/Ju2uLNAtrwWdBfnLBZuPZLD26TzOX/Q4u39SbhoA395S/iPwmxM00xDtrnXFGc2RYTgoTuLWFF6uioAmzxZSdIphLPiPwDMs5KCypW+lTOn8pztdAhAylXqiG7yFhReP7oEyb8IcNlUulJaloIfTWyLuQI1fEXA2gdkRULiOuxjGM3Wt2I6OOnZVzT7/+3/h7HVF4EI/xDpET6hQw7YszDr39AgMBAAEwDQYJKoZIhvcNAQELBQADggEBACaRkpf12OxGpdrsfsR5uslWl3GPA7HaKFHkRN3+0owf4j61rRJdxpkNmFKLGEZGAn3F+IBVzXIOx+mOq71BLKj/hxJ82bYJeUtK0a/fsX3S7z8TMXMgzzIQXS+XE4X7E8M3JEF+OKSuwG6bcaPJR8xscQ7i6z0rW14P1QgoEFAA6xhoHxK/AH2CTH/f8ojc2F5pPaYQJkuznd0OfcLAhPwMJ8btKGq9rNV/1EI59V+srA9lHvSWPfg6jXPsX96PSjTGljuHbZGMIka2mz4YOUvn9jlCGgv+gruIxeq8VKKPxfmDlSs9Jeof93MtYY92s4dDaJOru04mlqyKeFBic6o=' + x509cert: '%env(string:SAML_IDP_X509_CERT)%' sp: - entityId: 'http://localhost:8000/saml/metadata' + entityId: '%env(string:SAML_SP_ENTITY_ID)%' assertionConsumerService: - url: 'http://localhost:8000/saml/acs' + url: '%partdb.default_uri%saml/acs' binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' singleLogoutService: - url: 'http://localhost:8000/logout' + url: '%partdb.default_uri%logout' binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' - x509cert: 'MIICmzCCAYMCBgGGcIfLmDANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZwYXJ0ZGIwHhcNMjMwMjIwMjAzMTMzWhcNMzMwMjIwMjAzMzEzWjARMQ8wDQYDVQQDDAZwYXJ0ZGIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCmz9Lr8PxVjqqhGjjH4FbhVDbEPIplcQ1cFhTKu1NYpwHIm01pNnEwiyoLMBrZ16qq0PvCX+KCIjuJbC44k6N/+UCjbSZ8Fu+vyT9OkKVLvimc5sg9m9Kys+WiuaxzSEa2dA88khyGb6O9huuVYjibjROAN5I20h8f0FLfvolqWfEXVM00LIYYCHcvKNGq6PH0SS+2P6CGHoRrEFOoi7v7kkL6gWqYzPA/T7GSb78W14gU6FKcvQXPYOdsL7dYDy634BCd9k/tNF6JkiFi9U1IP8LEYkQUq3mKGRSjoPKKztws00ol3Nfd04XAdPKxIb3ff3io0vONE3QfJsgTzdR7AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEkYH4pCKxqoABjYGRml+ea5nteF/qsff6qHcNU8Unw1XLZO7ES1GU4Ij3RSFdt1SoKnphXnBtlVIechkOnOaECGs4RdbG4RGSFfusiEcWHbp1C0n3cnaUoV0/PJ3iETeeaPduqz9I/dlwhOxEOkpCaomYm4j2hfNcI9pmdM8bZuG+6dedh02ILrufQiCREmF6Sp+AOjueoFOoGwsXMbTuKo9pNizCO2OTT6nrjbNhjzoIvzbmlqiOGFs7YmpyTHcyqm66ACOnu1Nq9z9QLGkhvwjAmEytp6A9yEEkl0QO+DVhlAnvfdSMaR9jgyGtnBXQGOHv4osPtCtrk3IU4nB3s=' - privateKey: 'MIIEogIBAAKCAQEAps/S6/D8VY6qoRo4x+BW4VQ2xDyKZXENXBYUyrtTWKcByJtNaTZxMIsqCzAa2deqqtD7wl/igiI7iWwuOJOjf/lAo20mfBbvr8k/TpClS74pnObIPZvSsrPlormsc0hGtnQPPJIchm+jvYbrlWI4m40TgDeSNtIfH9BS376JalnxF1TNNCyGGAh3LyjRqujx9Ekvtj+ghh6EaxBTqIu7+5JC+oFqmMzwP0+xkm+/FteIFOhSnL0Fz2DnbC+3WA8ut+AQnfZP7TReiZIhYvVNSD/CxGJEFKt5ihkUo6Dyis7cLNNKJdzX3dOFwHTysSG93394qNLzjRN0HybIE83UewIDAQABAoIBAA1M7dLtPJlvzjAZQKTDQPondlRwRVKwUHHiutatWAhuDIjbxTDZ6+2Ecx5AQCvVc+C52BEYBx38L8YVz5uoPfWiwKInPlXPmF3qTHdtthhTecruZdHvvj2MdYdjiZoJjcXXfC2GsuqPNT2T5+3Zzoysk3z6MVjYqS2mtSzs6tUFZOY13/zMnrELjR2dFrigndjQgb5vENJ+/WNH4k9rt7KGDaMniJWHi3bDDNwilR0XrwhJVav0PIl7h8bMk95ixqo5B+RN0EMhfS+j3+8QJhYbGzXIyJbgMGTUGb23tBIDl6RFblLoic/PEVpA5yhVbLtdoMy5Mn+Y28yZPh6/VB0CgYEA1xtwZVbrRObv76bIkqgCXEYbiXhbFHzGTaI23UEBrV60VNr5LZl/vqA6JowDFUYl30Ytzr7TvC5KwfrNhaYOqVd4rMg145cn8J61bRHArMaXouVYVwWKEni50gCktVy8Tt2Nbzfja2rccCqv3JM2HHp49+e9CLi/D95XPAip3vcCgYEAxoYB7/OQmGMHnqwz2CUnLhGcnC1X4fZ+EJuRo1BXRcntCS4lwXCoyC3Es+YLZ6tfhUp17i/HaT3tJyp6F+EM43zIxwyDVCl6t0yFkjLHRdZHR3xGMMfcMLZGRY99Y25QyW1/GJFKff7D+rKvrpBVcl+9xLYcpIN6ULuHqs9f4Z0CgYBrDL2/wSTusltAEemJitE56K31mQ8CwCHUKuFQ9PQHurTV8e/F8LkxPf4ShuVV5gYc+oj7dd5brVII/W7gj0aGogBtRGoFLIl05xb1A7u2gFKgf7CaBiizjp8zUpyloVQZj4q+ibrFD3ZK4AOLKzvnqk+fWBWsTHzRQd56Avm++wKBgF3eR1Q6CojDanrwWaM+DgSOd0qxdfh2IK2hoX9jIaDyFY5dr6SDrIraeUPG5mWidowD5Tc2iEeO7G+0ef6IfxuhiR31ILPO2SOKny29rNOsug9nB5lRJyAxT5DchCFbq/9SMuJe8KYarHgBvWgA/yYRdx1oLqrrMA60XTW60E9RAoGALmoMX1CLe7BBrqaZrHBAqkV/6koTPSMuYVUZpbKgTmA7VsBScUZaX8huS7ARjLfoujxcNuFT4haPlY7gybvhEN0dMlqRwVNeiD3jo+PdDxu3h9SM6GTuVks1XWoifHBRj2NngBEipV3WDXLJR2sAbUArUdL58y12f7EWFdX41+s=' + x509cert: '%env(string:SAML_SP_X509_CERT)%' + privateKey: '%env(string:SAMLP_SP_PRIVATE_KEY)%' + # Optional settings #baseurl: 'http://myapp.com' strict: true diff --git a/config/packages/routing.yaml b/config/packages/routing.yaml index 4b766ce5..df5d98d2 100644 --- a/config/packages/routing.yaml +++ b/config/packages/routing.yaml @@ -4,7 +4,7 @@ framework: # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands - #default_uri: http://localhost + default_uri: '%env(DEFAULT_URI)%' when@prod: framework: diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index fe8bd4ae..05dec32c 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -19,6 +19,7 @@ twig: sidebar_tree_updater: '@App\Services\Trees\SidebarTreeUpdater' avatar_helper: '@App\Services\UserSystem\UserAvatarHelper' available_themes: '%partdb.available_themes%' + saml_enabled: '%partdb.saml.enabled%' when@test: twig: diff --git a/config/parameters.yaml b/config/parameters.yaml index ec80e939..e7b10354 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -13,6 +13,8 @@ parameters: partdb.global_theme: '' # The theme to use globally (see public/build/themes/ for choices, use name without .css). Set to '' for default bootstrap theme partdb.locale_menu: ['en', 'de', 'fr', 'ru', 'ja'] # The languages that are shown in user drop down menu + partdb.default_uri: '%env(string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails + ###################################################################################################################### # Users and Privacy ###################################################################################################################### @@ -39,6 +41,11 @@ parameters: partdb.error_pages.admin_email: '%env(trim:string:ERROR_PAGE_ADMIN_EMAIL)%' # You can set an email address here, which is shown on an error page, how to contact an administrator partdb.error_pages.show_help: '%env(trim:string:ERROR_PAGE_SHOW_HELP)%' # If this is set to true, solutions to common problems are shown on error pages. Disable this, if you do not want your users to see them... + ###################################################################################################################### + # SAML + ###################################################################################################################### + partdb.saml.enabled: '%env(bool:SAML_ENABLED)%' # If this is set to true, SAML authentication is enabled + ###################################################################################################################### # Sidebar ###################################################################################################################### @@ -110,3 +117,5 @@ parameters: env(TRUSTED_PROXIES): '127.0.0.1' #By default trust only our own server env(TRUSTED_HOSTS): '' # Trust all host names by default + + env(DEFAULT_URI): 'https://partdb.changeme.invalid/' diff --git a/config/routes/hslavich_saml.yaml b/config/routes/hslavich_saml.yaml index bbba3429..a902a93e 100644 --- a/config/routes/hslavich_saml.yaml +++ b/config/routes/hslavich_saml.yaml @@ -1,2 +1,4 @@ hslavich_saml_sp: - resource: "@HslavichOneloginSamlBundle/Resources/config/routing.yml" \ No newline at end of file + resource: "@HslavichOneloginSamlBundle/Resources/config/routing.yml" + # Only load the SAML routes if SAML is enabled + condition: "env('SAML_ENABLED') == '1' or env('SAML_ENABLED') == 'true'" diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig index 1dd6c52a..aab566d2 100644 --- a/templates/security/login.html.twig +++ b/templates/security/login.html.twig @@ -27,7 +27,14 @@ - SAML Login + {% if saml_enabled %} +
+ {% trans %}login.sso_saml_login{% endtrans %} + +

{% trans %}login.local_login_hint{% endtrans %}

+
+ + {% endif %}
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 7db2dadc..34be1ce5 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -10981,5 +10981,17 @@ Element 3 Your user uses single sign-on (SSO). You can not change the password and 2FA settings here. Configure them on your central SSO provider instead! + + + login.sso_saml_login + Single Sign-On Login (SSO) + + + + + login.local_login_hint + The form below is only for log in for a local user. If you want to log in via single sign-on, press the button above. + + From c831d57614d07ced23682a6b072b9cc57301e025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Thu, 23 Feb 2023 23:36:40 +0100 Subject: [PATCH 10/25] Added an console command to convert local to SAML users and vice versa --- config/services.yaml | 4 + src/Command/User/ConvertToSAMLUserCommand.php | 115 ++++++++++++++++++ src/Command/User/UserListCommand.php | 24 +++- src/Repository/UserRepository.php | 22 ++++ src/Security/SamlUserFactory.php | 4 +- 5 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 src/Command/User/ConvertToSAMLUserCommand.php diff --git a/config/services.yaml b/config/services.yaml index 5b5f1f35..a5914ee2 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -197,6 +197,10 @@ services: arguments: $available_themes: '%partdb.available_themes%' + App\Command\User\ConvertToSAMLUserCommand: + arguments: + $saml_enabled: '%partdb.saml.enabled%' + #################################################################################################################### # Label system diff --git a/src/Command/User/ConvertToSAMLUserCommand.php b/src/Command/User/ConvertToSAMLUserCommand.php new file mode 100644 index 00000000..df48ce06 --- /dev/null +++ b/src/Command/User/ConvertToSAMLUserCommand.php @@ -0,0 +1,115 @@ +. + */ + +namespace App\Command\User; + +use App\Entity\UserSystem\User; +use App\Security\SamlUserFactory; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class ConvertToSAMLUserCommand extends Command +{ + protected static $defaultName = 'partdb:user:convert-to-saml-user|partdb:users:convert-to-saml-user'; + + protected EntityManagerInterface $entityManager; + protected bool $saml_enabled; + + public function __construct(EntityManagerInterface $entityManager, bool $saml_enabled) + { + parent::__construct(); + $this->entityManager = $entityManager; + $this->saml_enabled = $saml_enabled; + } + + protected function configure(): void + { + $this + ->setDescription('Converts a local user to a SAML user (and vice versa)') + ->setHelp('This converts a local user, which can login via the login form, to a SAML user, which can only login via SAML. This is useful if you want to migrate from a local user system to a SAML user system.') + ->addArgument('user', InputArgument::REQUIRED, 'The username (or email) of the user') + ->addOption('to-local', null, InputOption::VALUE_NONE, 'Converts a SAML user to a local user') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $user_name = $input->getArgument('user'); + $to_local = $input->getOption('to-local'); + + if (!$this->saml_enabled && !$to_local) { + $io->confirm('SAML login is not configured. You will not be able to login with this user anymore, when SSO is not configured. Do you really want to continue?'); + } + + /** @var User $user */ + $user = $this->entityManager->getRepository(User::class)->findByEmailOrName($user_name); + + if (!$user) { + $io->error('User not found!'); + + return 1; + } + + $io->info('User found: '.$user->getFullName(true) . ': '.$user->getEmail().' [ID: ' . $user->getID() . ']'); + + if ($to_local) { + return $this->toLocal($user, $io); + } + + return $this->toSAML($user, $io); + } + + public function toLocal(User $user, SymfonyStyle $io): int + { + $io->confirm('You are going to convert a SAML user to a local user. This means, that the user can only login via the login form. ' + . 'The permissions and groups settings of the user will remain unchanged. Do you really want to continue?'); + + $user->setSAMLUser(false); + $user->setPassword(SamlUserFactory::SAML_PASSWORD_PLACEHOLDER); + + $this->entityManager->flush(); + + $io->success('User converted to local user! You will need to set a password for this user, before you can login with it.'); + + return 0; + } + + public function toSAML(User $user, SymfonyStyle $io): int + { + $io->confirm('You are going to convert a local user to a SAML user. This means, that the user can only login via SAML afterwards. The password in the DB will be removed. ' + . 'The permissions and groups settings of the user will remain unchanged. Do you really want to continue?'); + + $user->setSAMLUser(true); + $user->setPassword(SamlUserFactory::SAML_PASSWORD_PLACEHOLDER); + + $this->entityManager->flush(); + + $io->success('User converted to SAML user! You can now login with this user via SAML.'); + + return 0; + } + +} \ No newline at end of file diff --git a/src/Command/User/UserListCommand.php b/src/Command/User/UserListCommand.php index 0f0e52b7..66265dd8 100644 --- a/src/Command/User/UserListCommand.php +++ b/src/Command/User/UserListCommand.php @@ -46,22 +46,39 @@ class UserListCommand extends Command $this ->setDescription('Lists all users') ->setHelp('This command lists all users in the database.') + ->addOption('local', 'l', null, 'Only list local users') + ->addOption('saml', 's', null, 'Only list SAML users') ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); + $only_local = $input->getOption('local'); + $only_saml = $input->getOption('saml'); - //Get all users from database - $users = $this->entityManager->getRepository(User::class)->findAll(); + if ($only_local && $only_saml) { + $io->error('You can not use --local and --saml at the same time!'); + + return Command::FAILURE; + } + + $repo = $this->entityManager->getRepository(User::class); + + if ($only_local) { + $users = $repo->onlyLocalUsers(); + } elseif ($only_saml) { + $users = $repo->onlySAMLUsers(); + } else { + $users = $repo->findAll(); + } $io->info(sprintf("Found %d users in database.", count($users))); $io->title('Users:'); $table = new Table($output); - $table->setHeaders(['ID', 'Username', 'Name', 'Email', 'Group', 'Login Disabled']); + $table->setHeaders(['ID', 'Username', 'Name', 'Email', 'Group', 'Login Disabled', 'Type']); foreach ($users as $user) { $table->addRow([ @@ -71,6 +88,7 @@ class UserListCommand extends Command $user->getEmail(), $user->getGroup() !== null ? $user->getGroup()->getName() . ' (ID: ' . $user->getGroup()->getID() . ')' : 'No group', $user->isDisabled() ? 'Yes' : 'No', + $user->isSAMLUser() ? 'SAML' : 'Local', ]); } diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 2d4fea12..b0ccd964 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -89,4 +89,26 @@ final class UserRepository extends NamedDBElementRepository implements PasswordU $this->getEntityManager()->flush(); } } + + /** + * Returns the list of all local users (not SAML users). + * @return User[] + */ + public function onlyLocalUsers(): array + { + return $this->findBy([ + 'saml_user' => false, + ]); + } + + /** + * Returns the list of all SAML users. + * @return User[] + */ + public function onlySAMLUsers(): array + { + return $this->findBy([ + 'saml_user' => true, + ]); + } } diff --git a/src/Security/SamlUserFactory.php b/src/Security/SamlUserFactory.php index d212f78b..fd181133 100644 --- a/src/Security/SamlUserFactory.php +++ b/src/Security/SamlUserFactory.php @@ -26,12 +26,14 @@ use Symfony\Component\Security\Core\User\UserInterface; class SamlUserFactory implements SamlUserFactoryInterface { + public const SAML_PASSWORD_PLACEHOLDER = '!!SAML!!'; + public function createUser($username, array $attributes = []): UserInterface { $user = new User(); $user->setName($username); $user->setNeedPwChange(false); - $user->setPassword('!!SAML!!'); + $user->setPassword(self::SAML_PASSWORD_PLACEHOLDER); //This is a SAML user now! $user->setSamlUser(true); From e6d9237bda00afc4a11e3d8a348e892b603e16ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Thu, 23 Feb 2023 23:39:29 +0100 Subject: [PATCH 11/25] Allow to specify a user by username or email with set-password commannd --- src/Command/User/SetPasswordCommand.php | 9 +++------ src/Command/User/UsersPermissionsCommand.php | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Command/User/SetPasswordCommand.php b/src/Command/User/SetPasswordCommand.php index 66e6e97e..78724ecf 100644 --- a/src/Command/User/SetPasswordCommand.php +++ b/src/Command/User/SetPasswordCommand.php @@ -56,7 +56,7 @@ class SetPasswordCommand extends Command $this ->setDescription('Sets the password of a user') ->setHelp('This password allows you to set the password of a user, without knowing the old password.') - ->addArgument('user', InputArgument::REQUIRED, 'The name of the user') + ->addArgument('user', InputArgument::REQUIRED, 'The username or email of the user') ; } @@ -65,17 +65,14 @@ class SetPasswordCommand extends Command $io = new SymfonyStyle($input, $output); $user_name = $input->getArgument('user'); - /** @var User[] $users */ - $users = $this->entityManager->getRepository(User::class)->findBy(['name' => $user_name]); + $user = $this->entityManager->getRepository(User::class)->findByEmailOrName($user_name); - if (empty($users)) { + if (!$user) { $io->error(sprintf('No user with the given username %s found in the database!', $user_name)); return 1; } - $user = $users[0]; - $io->note('User found!'); if ($user->isSamlUser()) { diff --git a/src/Command/User/UsersPermissionsCommand.php b/src/Command/User/UsersPermissionsCommand.php index 3cccd8fd..3d57a4dc 100644 --- a/src/Command/User/UsersPermissionsCommand.php +++ b/src/Command/User/UsersPermissionsCommand.php @@ -57,7 +57,7 @@ class UsersPermissionsCommand extends Command protected function configure(): void { $this - ->addArgument('user', InputArgument::REQUIRED, 'The username of the user to view') + ->addArgument('user', InputArgument::REQUIRED, 'The username or email of the user to view') ->addOption('noInherit', null, InputOption::VALUE_NONE, 'Do not inherit permissions from groups') ->addOption('edit', '', InputOption::VALUE_NONE, 'Edit the permissions of the user') ; From f5a5114999cfb17540b53682d75e6b3f975777ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Thu, 23 Feb 2023 23:43:01 +0100 Subject: [PATCH 12/25] Fixed PHPunit tests --- tests/Security/EnsureSAMLUserForSAMLLoginCheckerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Security/EnsureSAMLUserForSAMLLoginCheckerTest.php b/tests/Security/EnsureSAMLUserForSAMLLoginCheckerTest.php index 193011cd..5751de07 100644 --- a/tests/Security/EnsureSAMLUserForSAMLLoginCheckerTest.php +++ b/tests/Security/EnsureSAMLUserForSAMLLoginCheckerTest.php @@ -37,7 +37,7 @@ class EnsureSAMLUserForSAMLLoginCheckerTest extends WebTestCase protected function setUp(): void { self::bootKernel(); - $this->service = self::getContainer()->get('saml_user_factory'); + $this->service = self::getContainer()->get(EnsureSAMLUserForSAMLLoginChecker::class); } public function testOnAuthenticationSuccessFailsOnSSOLoginWithLocalUser(): void From 960ee342e410bcb1672d2cefae02eaeb10ff5059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Fri, 24 Feb 2023 00:12:44 +0100 Subject: [PATCH 13/25] Moved all user info updating logic into SAMLUserFactory --- config/packages/security.yaml | 2 +- config/services_test.yaml | 9 --- psalm.xml | 14 ++-- src/Entity/UserSystem/User.php | 34 +--------- src/Security/SamlUserFactory.php | 88 +++++++++++++++++++++++++- tests/Entity/UserSystem/UserTest.php | 36 ----------- tests/Security/SamlUserFactoryTest.php | 36 +++++++++++ translations/messages.en.xlf | 2 +- 8 files changed, 133 insertions(+), 88 deletions(-) delete mode 100644 config/services_test.yaml diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 7e0ded3c..f03e0feb 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -33,7 +33,7 @@ security: saml: use_referer: true user_factory: saml_user_factory - persist_user: true + persist_user: false # False as we have our own persisting mechanism in SAMLUserFactory check_path: saml_acs login_path: saml_login failure_path: login diff --git a/config/services_test.yaml b/config/services_test.yaml deleted file mode 100644 index e29da6fc..00000000 --- a/config/services_test.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# Service overrides for the test environment - -services: - saml_user_factory: - class: App\Security\SamlUserFactory - public: true - - App\Security\SamlUserFactory: - public: true \ No newline at end of file diff --git a/psalm.xml b/psalm.xml index 872169ac..71c720ab 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,11 +1,13 @@ @@ -52,4 +54,4 @@ - + diff --git a/src/Entity/UserSystem/User.php b/src/Entity/UserSystem/User.php index eddf9179..4289aa0d 100644 --- a/src/Entity/UserSystem/User.php +++ b/src/Entity/UserSystem/User.php @@ -62,7 +62,7 @@ use Jbtronics\TFAWebauthn\Model\TwoFactorInterface as WebauthnTwoFactorInterface * @UniqueEntity("name", message="validator.user.username_already_used") */ class User extends AttachmentContainingDBElement implements UserInterface, HasPermissionsInterface, TwoFactorInterface, - BackupCodeInterface, TrustedDeviceInterface, WebauthnTwoFactorInterface, PreferredProviderInterface, PasswordAuthenticatedUserInterface, SamlUserInterface + BackupCodeInterface, TrustedDeviceInterface, WebauthnTwoFactorInterface, PreferredProviderInterface, PasswordAuthenticatedUserInterface { //use MasterAttachmentTrait; @@ -892,36 +892,4 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe $this->saml_user = $saml_user; return $this; } - - - - public function setSamlAttributes(array $attributes) - { - //When mail attribute exists, set it - if (isset($attributes['email'])) { - $this->setEmail($attributes['email'][0]); - } - //When first name attribute exists, set it - if (isset($attributes['firstName'])) { - $this->setFirstName($attributes['firstName'][0]); - } - //When last name attribute exists, set it - if (isset($attributes['lastName'])) { - $this->setLastName($attributes['lastName'][0]); - } - if (isset($attributes['department'])) { - $this->setDepartment($attributes['department'][0]); - } - - //Use X500 attributes as userinfo - if (isset($attributes['urn:oid:2.5.4.42'])) { - $this->setFirstName($attributes['urn:oid:2.5.4.42'][0]); - } - if (isset($attributes['urn:oid:2.5.4.4'])) { - $this->setLastName($attributes['urn:oid:2.5.4.4'][0]); - } - if (isset($attributes['urn:oid:1.2.840.113549.1.9.1'])) { - $this->setEmail($attributes['urn:oid:1.2.840.113549.1.9.1'][0]); - } - } } diff --git a/src/Security/SamlUserFactory.php b/src/Security/SamlUserFactory.php index fd181133..168c4269 100644 --- a/src/Security/SamlUserFactory.php +++ b/src/Security/SamlUserFactory.php @@ -21,13 +21,29 @@ namespace App\Security; use App\Entity\UserSystem\User; +use Doctrine\ORM\EntityManagerInterface; +use Hslavich\OneloginSamlBundle\Event\AbstractUserEvent; +use Hslavich\OneloginSamlBundle\Event\UserCreatedEvent; +use Hslavich\OneloginSamlBundle\Event\UserModifiedEvent; +use Hslavich\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken; use Hslavich\OneloginSamlBundle\Security\User\SamlUserFactoryInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; -class SamlUserFactory implements SamlUserFactoryInterface +class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterface { public const SAML_PASSWORD_PLACEHOLDER = '!!SAML!!'; + private Security $security; + private EntityManagerInterface $entityManager; + + public function __construct(Security $security, EntityManagerInterface $entityManager) + { + $this->entityManager = $entityManager; + $this->security = $security; + } + public function createUser($username, array $attributes = []): UserInterface { $user = new User(); @@ -37,8 +53,76 @@ class SamlUserFactory implements SamlUserFactoryInterface //This is a SAML user now! $user->setSamlUser(true); - $user->setSamlAttributes($attributes); + $this->updateUserInfoFromSAMLAttributes($user, $attributes); return $user; } + + public function updateAndPersistUser(AbstractUserEvent $event): void + { + $user = $event->getUser(); + $token = $this->security->getToken(); + + if (!$user instanceof User) { + throw new \RuntimeException('User must be an instance of '.User::class); + } + if (!$token instanceof SamlToken) { + throw new \RuntimeException('Token must be an instance of '.SamlToken::class); + } + + $attributes = $token->getAttributes(); + + //Update the user info based on the SAML attributes + $this->updateUserInfoFromSAMLAttributes($user, $attributes); + + //Persist the user + $this->entityManager->persist($user); + + //Flush the entity manager + $this->entityManager->flush(); + } + + public static function getSubscribedEvents() + { + return [ + UserCreatedEvent::class => 'updateAndPersistUser', + UserModifiedEvent::class => 'updateAndPersistUser', + ]; + } + + /** + * Sets the SAML attributes to the user. + * @param User $user + * @param array $attributes + * @return void + */ + public function updateUserInfoFromSAMLAttributes(User $user, array $attributes): void + { + //When mail attribute exists, set it + if (isset($attributes['email'])) { + $user->setEmail($attributes['email'][0]); + } + //When first name attribute exists, set it + if (isset($attributes['firstName'])) { + $user->setFirstName($attributes['firstName'][0]); + } + //When last name attribute exists, set it + if (isset($attributes['lastName'])) { + $user->setLastName($attributes['lastName'][0]); + } + if (isset($attributes['department'])) { + $user->setDepartment($attributes['department'][0]); + } + + //Use X500 attributes as userinfo + if (isset($attributes['urn:oid:2.5.4.42'])) { + $user->setFirstName($attributes['urn:oid:2.5.4.42'][0]); + } + if (isset($attributes['urn:oid:2.5.4.4'])) { + $user->setLastName($attributes['urn:oid:2.5.4.4'][0]); + } + if (isset($attributes['urn:oid:1.2.840.113549.1.9.1'])) { + $user->setEmail($attributes['urn:oid:1.2.840.113549.1.9.1'][0]); + } + } } \ No newline at end of file diff --git a/tests/Entity/UserSystem/UserTest.php b/tests/Entity/UserSystem/UserTest.php index 456457a4..89aab673 100644 --- a/tests/Entity/UserSystem/UserTest.php +++ b/tests/Entity/UserSystem/UserTest.php @@ -148,40 +148,4 @@ class UserTest extends TestCase } $this->assertFalse($user->isWebAuthnAuthenticatorEnabled()); } - - public function testSetSAMLAttributes(): void - { - $data = [ - 'firstName' => ['John'], - 'lastName' => ['Doe'], - 'email' => ['j.doe@invalid.invalid'], - 'department' => ['Test Department'], - ]; - - $user = new User(); - $user->setSAMLAttributes($data); - - //Test if the data was set correctly - $this->assertSame('John', $user->getFirstName()); - $this->assertSame('Doe', $user->getLastName()); - $this->assertSame('j.doe@invalid.invalid', $user->getEmail()); - $this->assertSame('Test Department', $user->getDepartment()); - - //Test that it works for X500 attributes - $data = [ - 'urn:oid:2.5.4.42' => ['Jane'], - 'urn:oid:2.5.4.4' => ['Dane'], - 'urn:oid:1.2.840.113549.1.9.1' => ['mail@invalid.invalid'], - ]; - - $user->setSAMLAttributes($data); - - //Data must be changed - $this->assertSame('Jane', $user->getFirstName()); - $this->assertSame('Dane', $user->getLastName()); - $this->assertSame('mail@invalid.invalid', $user->getEmail()); - - //Department must not be changed - $this->assertSame('Test Department', $user->getDepartment()); - } } diff --git a/tests/Security/SamlUserFactoryTest.php b/tests/Security/SamlUserFactoryTest.php index 6127237a..77fd6ab9 100644 --- a/tests/Security/SamlUserFactoryTest.php +++ b/tests/Security/SamlUserFactoryTest.php @@ -62,4 +62,40 @@ class SamlUserFactoryTest extends WebTestCase $this->assertEquals('IT', $user->getDepartment()); $this->assertEquals('j.doe@invalid.invalid', $user->getEmail()); } + + public function testUpdateUserInfoFromSAMLAttributes(): void + { + $data = [ + 'firstName' => ['John'], + 'lastName' => ['Doe'], + 'email' => ['j.doe@invalid.invalid'], + 'department' => ['Test Department'], + ]; + + $user = new User(); + $this->service->updateUserInfoFromSAMLAttributes($user, $data); + + //Test if the data was set correctly + $this->assertSame('John', $user->getFirstName()); + $this->assertSame('Doe', $user->getLastName()); + $this->assertSame('j.doe@invalid.invalid', $user->getEmail()); + $this->assertSame('Test Department', $user->getDepartment()); + + //Test that it works for X500 attributes + $data = [ + 'urn:oid:2.5.4.42' => ['Jane'], + 'urn:oid:2.5.4.4' => ['Dane'], + 'urn:oid:1.2.840.113549.1.9.1' => ['mail@invalid.invalid'], + ]; + + $this->service->updateUserInfoFromSAMLAttributes($user, $data); + + //Data must be changed + $this->assertSame('Jane', $user->getFirstName()); + $this->assertSame('Dane', $user->getLastName()); + $this->assertSame('mail@invalid.invalid', $user->getEmail()); + + //Department must not be changed + $this->assertSame('Test Department', $user->getDepartment()); + } } diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 34be1ce5..ae66d98c 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -10990,7 +10990,7 @@ Element 3 login.local_login_hint - The form below is only for log in for a local user. If you want to log in via single sign-on, press the button above. + The form below is only for log in with a local user. If you want to log in via single sign-on, press the button above. From 99f04d71afd9eec7eb1233137c75e2c4012992ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Fri, 24 Feb 2023 00:12:44 +0100 Subject: [PATCH 14/25] Revert "Moved all user info updating logic into SAMLUserFactory" This reverts commit 960ee342e410bcb1672d2cefae02eaeb10ff5059. --- config/packages/security.yaml | 2 +- config/services_test.yaml | 9 +++ psalm.xml | 14 ++-- src/Entity/UserSystem/User.php | 34 +++++++++- src/Security/SamlUserFactory.php | 88 +------------------------- tests/Entity/UserSystem/UserTest.php | 36 +++++++++++ tests/Security/SamlUserFactoryTest.php | 36 ----------- translations/messages.en.xlf | 2 +- 8 files changed, 88 insertions(+), 133 deletions(-) create mode 100644 config/services_test.yaml diff --git a/config/packages/security.yaml b/config/packages/security.yaml index f03e0feb..7e0ded3c 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -33,7 +33,7 @@ security: saml: use_referer: true user_factory: saml_user_factory - persist_user: false # False as we have our own persisting mechanism in SAMLUserFactory + persist_user: true check_path: saml_acs login_path: saml_login failure_path: login diff --git a/config/services_test.yaml b/config/services_test.yaml new file mode 100644 index 00000000..e29da6fc --- /dev/null +++ b/config/services_test.yaml @@ -0,0 +1,9 @@ +# Service overrides for the test environment + +services: + saml_user_factory: + class: App\Security\SamlUserFactory + public: true + + App\Security\SamlUserFactory: + public: true \ No newline at end of file diff --git a/psalm.xml b/psalm.xml index 71c720ab..872169ac 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,13 +1,11 @@ @@ -54,4 +52,4 @@ - + diff --git a/src/Entity/UserSystem/User.php b/src/Entity/UserSystem/User.php index 4289aa0d..eddf9179 100644 --- a/src/Entity/UserSystem/User.php +++ b/src/Entity/UserSystem/User.php @@ -62,7 +62,7 @@ use Jbtronics\TFAWebauthn\Model\TwoFactorInterface as WebauthnTwoFactorInterface * @UniqueEntity("name", message="validator.user.username_already_used") */ class User extends AttachmentContainingDBElement implements UserInterface, HasPermissionsInterface, TwoFactorInterface, - BackupCodeInterface, TrustedDeviceInterface, WebauthnTwoFactorInterface, PreferredProviderInterface, PasswordAuthenticatedUserInterface + BackupCodeInterface, TrustedDeviceInterface, WebauthnTwoFactorInterface, PreferredProviderInterface, PasswordAuthenticatedUserInterface, SamlUserInterface { //use MasterAttachmentTrait; @@ -892,4 +892,36 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe $this->saml_user = $saml_user; return $this; } + + + + public function setSamlAttributes(array $attributes) + { + //When mail attribute exists, set it + if (isset($attributes['email'])) { + $this->setEmail($attributes['email'][0]); + } + //When first name attribute exists, set it + if (isset($attributes['firstName'])) { + $this->setFirstName($attributes['firstName'][0]); + } + //When last name attribute exists, set it + if (isset($attributes['lastName'])) { + $this->setLastName($attributes['lastName'][0]); + } + if (isset($attributes['department'])) { + $this->setDepartment($attributes['department'][0]); + } + + //Use X500 attributes as userinfo + if (isset($attributes['urn:oid:2.5.4.42'])) { + $this->setFirstName($attributes['urn:oid:2.5.4.42'][0]); + } + if (isset($attributes['urn:oid:2.5.4.4'])) { + $this->setLastName($attributes['urn:oid:2.5.4.4'][0]); + } + if (isset($attributes['urn:oid:1.2.840.113549.1.9.1'])) { + $this->setEmail($attributes['urn:oid:1.2.840.113549.1.9.1'][0]); + } + } } diff --git a/src/Security/SamlUserFactory.php b/src/Security/SamlUserFactory.php index 168c4269..fd181133 100644 --- a/src/Security/SamlUserFactory.php +++ b/src/Security/SamlUserFactory.php @@ -21,29 +21,13 @@ namespace App\Security; use App\Entity\UserSystem\User; -use Doctrine\ORM\EntityManagerInterface; -use Hslavich\OneloginSamlBundle\Event\AbstractUserEvent; -use Hslavich\OneloginSamlBundle\Event\UserCreatedEvent; -use Hslavich\OneloginSamlBundle\Event\UserModifiedEvent; -use Hslavich\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken; use Hslavich\OneloginSamlBundle\Security\User\SamlUserFactoryInterface; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; -class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterface +class SamlUserFactory implements SamlUserFactoryInterface { public const SAML_PASSWORD_PLACEHOLDER = '!!SAML!!'; - private Security $security; - private EntityManagerInterface $entityManager; - - public function __construct(Security $security, EntityManagerInterface $entityManager) - { - $this->entityManager = $entityManager; - $this->security = $security; - } - public function createUser($username, array $attributes = []): UserInterface { $user = new User(); @@ -53,76 +37,8 @@ class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterf //This is a SAML user now! $user->setSamlUser(true); - $this->updateUserInfoFromSAMLAttributes($user, $attributes); + $user->setSamlAttributes($attributes); return $user; } - - public function updateAndPersistUser(AbstractUserEvent $event): void - { - $user = $event->getUser(); - $token = $this->security->getToken(); - - if (!$user instanceof User) { - throw new \RuntimeException('User must be an instance of '.User::class); - } - if (!$token instanceof SamlToken) { - throw new \RuntimeException('Token must be an instance of '.SamlToken::class); - } - - $attributes = $token->getAttributes(); - - //Update the user info based on the SAML attributes - $this->updateUserInfoFromSAMLAttributes($user, $attributes); - - //Persist the user - $this->entityManager->persist($user); - - //Flush the entity manager - $this->entityManager->flush(); - } - - public static function getSubscribedEvents() - { - return [ - UserCreatedEvent::class => 'updateAndPersistUser', - UserModifiedEvent::class => 'updateAndPersistUser', - ]; - } - - /** - * Sets the SAML attributes to the user. - * @param User $user - * @param array $attributes - * @return void - */ - public function updateUserInfoFromSAMLAttributes(User $user, array $attributes): void - { - //When mail attribute exists, set it - if (isset($attributes['email'])) { - $user->setEmail($attributes['email'][0]); - } - //When first name attribute exists, set it - if (isset($attributes['firstName'])) { - $user->setFirstName($attributes['firstName'][0]); - } - //When last name attribute exists, set it - if (isset($attributes['lastName'])) { - $user->setLastName($attributes['lastName'][0]); - } - if (isset($attributes['department'])) { - $user->setDepartment($attributes['department'][0]); - } - - //Use X500 attributes as userinfo - if (isset($attributes['urn:oid:2.5.4.42'])) { - $user->setFirstName($attributes['urn:oid:2.5.4.42'][0]); - } - if (isset($attributes['urn:oid:2.5.4.4'])) { - $user->setLastName($attributes['urn:oid:2.5.4.4'][0]); - } - if (isset($attributes['urn:oid:1.2.840.113549.1.9.1'])) { - $user->setEmail($attributes['urn:oid:1.2.840.113549.1.9.1'][0]); - } - } } \ No newline at end of file diff --git a/tests/Entity/UserSystem/UserTest.php b/tests/Entity/UserSystem/UserTest.php index 89aab673..456457a4 100644 --- a/tests/Entity/UserSystem/UserTest.php +++ b/tests/Entity/UserSystem/UserTest.php @@ -148,4 +148,40 @@ class UserTest extends TestCase } $this->assertFalse($user->isWebAuthnAuthenticatorEnabled()); } + + public function testSetSAMLAttributes(): void + { + $data = [ + 'firstName' => ['John'], + 'lastName' => ['Doe'], + 'email' => ['j.doe@invalid.invalid'], + 'department' => ['Test Department'], + ]; + + $user = new User(); + $user->setSAMLAttributes($data); + + //Test if the data was set correctly + $this->assertSame('John', $user->getFirstName()); + $this->assertSame('Doe', $user->getLastName()); + $this->assertSame('j.doe@invalid.invalid', $user->getEmail()); + $this->assertSame('Test Department', $user->getDepartment()); + + //Test that it works for X500 attributes + $data = [ + 'urn:oid:2.5.4.42' => ['Jane'], + 'urn:oid:2.5.4.4' => ['Dane'], + 'urn:oid:1.2.840.113549.1.9.1' => ['mail@invalid.invalid'], + ]; + + $user->setSAMLAttributes($data); + + //Data must be changed + $this->assertSame('Jane', $user->getFirstName()); + $this->assertSame('Dane', $user->getLastName()); + $this->assertSame('mail@invalid.invalid', $user->getEmail()); + + //Department must not be changed + $this->assertSame('Test Department', $user->getDepartment()); + } } diff --git a/tests/Security/SamlUserFactoryTest.php b/tests/Security/SamlUserFactoryTest.php index 77fd6ab9..6127237a 100644 --- a/tests/Security/SamlUserFactoryTest.php +++ b/tests/Security/SamlUserFactoryTest.php @@ -62,40 +62,4 @@ class SamlUserFactoryTest extends WebTestCase $this->assertEquals('IT', $user->getDepartment()); $this->assertEquals('j.doe@invalid.invalid', $user->getEmail()); } - - public function testUpdateUserInfoFromSAMLAttributes(): void - { - $data = [ - 'firstName' => ['John'], - 'lastName' => ['Doe'], - 'email' => ['j.doe@invalid.invalid'], - 'department' => ['Test Department'], - ]; - - $user = new User(); - $this->service->updateUserInfoFromSAMLAttributes($user, $data); - - //Test if the data was set correctly - $this->assertSame('John', $user->getFirstName()); - $this->assertSame('Doe', $user->getLastName()); - $this->assertSame('j.doe@invalid.invalid', $user->getEmail()); - $this->assertSame('Test Department', $user->getDepartment()); - - //Test that it works for X500 attributes - $data = [ - 'urn:oid:2.5.4.42' => ['Jane'], - 'urn:oid:2.5.4.4' => ['Dane'], - 'urn:oid:1.2.840.113549.1.9.1' => ['mail@invalid.invalid'], - ]; - - $this->service->updateUserInfoFromSAMLAttributes($user, $data); - - //Data must be changed - $this->assertSame('Jane', $user->getFirstName()); - $this->assertSame('Dane', $user->getLastName()); - $this->assertSame('mail@invalid.invalid', $user->getEmail()); - - //Department must not be changed - $this->assertSame('Test Department', $user->getDepartment()); - } } diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index ae66d98c..34be1ce5 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -10990,7 +10990,7 @@ Element 3 login.local_login_hint - The form below is only for log in with a local user. If you want to log in via single sign-on, press the button above. + The form below is only for log in for a local user. If you want to log in via single sign-on, press the button above. From 6a06a24296a0490417b0d3fc15294cf014a2e61f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Mon, 27 Feb 2023 22:29:19 +0100 Subject: [PATCH 15/25] Improved translations --- translations/messages.en.xlf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 34be1ce5..ae66d98c 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -10990,7 +10990,7 @@ Element 3 login.local_login_hint - The form below is only for log in for a local user. If you want to log in via single sign-on, press the button above. + The form below is only for log in with a local user. If you want to log in via single sign-on, press the button above. From 5e85c52a57c2d14804bc401e4a573df9168ba68d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Mon, 27 Feb 2023 23:47:42 +0100 Subject: [PATCH 16/25] Allow to automatically assign SAML users to a group based on SAML attributes --- .env | 8 ++ config/parameters.yaml | 2 + config/services.yaml | 8 +- config/services_test.yaml | 9 -- src/Security/SamlUserFactory.php | 112 ++++++++++++++++++++++++- tests/Security/SamlUserFactoryTest.php | 23 +++++ 6 files changed, 151 insertions(+), 11 deletions(-) delete mode 100644 config/services_test.yaml diff --git a/.env b/.env index 5906397b..352d553a 100644 --- a/.env +++ b/.env @@ -79,6 +79,14 @@ ERROR_PAGE_SHOW_HELP=1 # Set this to 1 to enable SAML single sign on SAML_ENABLED=0 +# A JSON encoded array of role mappings in the form { "saml_role": PARTDB_GROUP_ID, "*": PARTDB_GROUP_ID } +SAML_ROLE_MAPPING="{}" +# A mapping could look like the following +#SAML_ROLE_MAPPING='{ "*": 2, "editor": 3, "admin": 1 }' + +# When this is set to 1, the group of SAML users will be updated everytime they login based on their SAML roles +SAML_UPDATE_GROUP_ON_LOGIN=1 + # The entity ID of your SAML IDP (e.g. the realm name of your Keycloak server) SAML_IDP_ENTITY_ID="https://idp.changeme.invalid/realms/master" # The URL of your SAML IDP SingleSignOnService (e.g. the endpoint of your Keycloak server) diff --git a/config/parameters.yaml b/config/parameters.yaml index e7b10354..cd372483 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -119,3 +119,5 @@ parameters: env(TRUSTED_HOSTS): '' # Trust all host names by default env(DEFAULT_URI): 'https://partdb.changeme.invalid/' + + env(SAML_ROLE_MAPPING): '{}' diff --git a/config/services.yaml b/config/services.yaml index a5914ee2..925b1c83 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -128,7 +128,13 @@ services: #################################################################################################################### saml_user_factory: - class: App\Security\SamlUserFactory + alias: App\Security\SamlUserFactory + public: true + + App\Security\SamlUserFactory: + arguments: + $saml_role_mapping: '%env(json:SAML_ROLE_MAPPING)%' + $update_group_on_login: '%env(bool:SAML_UPDATE_GROUP_ON_LOGIN)%' #################################################################################################################### # Cache diff --git a/config/services_test.yaml b/config/services_test.yaml deleted file mode 100644 index e29da6fc..00000000 --- a/config/services_test.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# Service overrides for the test environment - -services: - saml_user_factory: - class: App\Security\SamlUserFactory - public: true - - App\Security\SamlUserFactory: - public: true \ No newline at end of file diff --git a/src/Security/SamlUserFactory.php b/src/Security/SamlUserFactory.php index fd181133..05394306 100644 --- a/src/Security/SamlUserFactory.php +++ b/src/Security/SamlUserFactory.php @@ -20,12 +20,32 @@ namespace App\Security; +use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; +use Doctrine\ORM\EntityManagerInterface; +use Hslavich\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken; use Hslavich\OneloginSamlBundle\Security\User\SamlUserFactoryInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; use Symfony\Component\Security\Core\User\UserInterface; -class SamlUserFactory implements SamlUserFactoryInterface +class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterface { + private EntityManagerInterface $em; + private array $saml_role_mapping; + private bool $update_group_on_login; + + public function __construct(EntityManagerInterface $entityManager, ?array $saml_role_mapping, bool $update_group_on_login) + { + $this->em = $entityManager; + if ($saml_role_mapping) { + $this->saml_role_mapping = $saml_role_mapping; + } else { + $this->saml_role_mapping = []; + } + $this->update_group_on_login = $update_group_on_login; + } + public const SAML_PASSWORD_PLACEHOLDER = '!!SAML!!'; public function createUser($username, array $attributes = []): UserInterface @@ -37,8 +57,98 @@ class SamlUserFactory implements SamlUserFactoryInterface //This is a SAML user now! $user->setSamlUser(true); + //Update basic user information $user->setSamlAttributes($attributes); + //Check if we can find a group for this user based on the SAML attributes + $group = $this->mapSAMLAttributesToLocalGroup($attributes); + $user->setGroup($group); + return $user; } + + /** + * This method is called after a successful authentication. It is used to update the group of the user, + * based on the new SAML attributes. + * @param AuthenticationSuccessEvent $event + * @return void + */ + public function onAuthenticationSuccess(AuthenticationSuccessEvent $event): void + { + if (! $this->update_group_on_login) { + return; + } + + $token = $event->getAuthenticationToken(); + $user = $token->getUser(); + //Only update the group if the user is a SAML user + if (! $token instanceof SamlToken || ! $user instanceof User) { + return; + } + + //Check if we can find a group for this user based on the SAML attributes + $group = $this->mapSAMLAttributesToLocalGroup($token->getAttributes()); + //If needed update the group of the user and save it to DB + if ($group !== $user->getGroup()) { + $user->setGroup($group); + $this->em->flush($user); + } + } + + /** + * Maps the given SAML attributes to a local group. + * @param array $attributes The SAML attributes + * @return Group|null + */ + public function mapSAMLAttributesToLocalGroup(array $attributes): ?Group + { + //Extract the roles from the SAML attributes + $roles = $attributes['group'] ?? []; + $group_id = $this->mapSAMLRolesToLocalGroupID($roles); + + //Check if we can find a group with the given ID + if ($group_id !== null) { + $group = $this->em->find(Group::class, $group_id); + if ($group !== null) { + return $group; + } + } + + //If no group was found, return null + return null; + } + + /** + * Maps a list of SAML roles to a local group ID. + * @param array $roles The list of SAML roles + * @param array $map|null The mapping from SAML roles. If null, the global mapping will be used. + * @return int|null The ID of the local group or null if no mapping was found. + */ + public function mapSAMLRolesToLocalGroupID(array $roles, array $map = null): ?int + { + $map = $map ?? $this->saml_role_mapping; + + //Iterate over all roles and check if we have a mapping for it. + foreach ($roles as $role) { + if (array_key_exists($role, $map)) { + //We use the first available mapping + return (int) $map[$role]; + } + } + + //If no applicable mapping was found, check if we have a default mapping + if (array_key_exists('*', $map)) { + return (int) $map['*']; + } + + //If no mapping was found, return null + return null; + } + + public static function getSubscribedEvents(): array + { + return [ + AuthenticationSuccessEvent::class => 'onAuthenticationSuccess', + ]; + } } \ No newline at end of file diff --git a/tests/Security/SamlUserFactoryTest.php b/tests/Security/SamlUserFactoryTest.php index 6127237a..ab349f68 100644 --- a/tests/Security/SamlUserFactoryTest.php +++ b/tests/Security/SamlUserFactoryTest.php @@ -62,4 +62,27 @@ class SamlUserFactoryTest extends WebTestCase $this->assertEquals('IT', $user->getDepartment()); $this->assertEquals('j.doe@invalid.invalid', $user->getEmail()); } + + public function testMapSAMLRolesToLocalGroupID() + { + $mapping = [ + 'employee' => 1, + 'admin' => 2, + 'manager' => 3, + 'administrator' => 2, + '*' => 4, + ]; + + //Test if mapping works + $this->assertEquals(1, $this->service->mapSAMLRolesToLocalGroupID(['employee'], $mapping)); + //Only the first valid mapping should be used + $this->assertEquals(1, $this->service->mapSAMLRolesToLocalGroupID(['employee', 'admin'], $mapping)); + $this->assertSame(2, $this->service->mapSAMLRolesToLocalGroupID(['does_not_matter', 'admin', 'employee'], $mapping)); + //Test if mapping is case sensitive + $this->assertEquals(4, $this->service->mapSAMLRolesToLocalGroupID(['ADMIN'], $mapping)); + + //Test that wildcard mapping works + $this->assertEquals(4, $this->service->mapSAMLRolesToLocalGroupID(['entry1', 'entry2'], $mapping)); + $this->assertEquals(4, $this->service->mapSAMLRolesToLocalGroupID([], $mapping)); + } } From 472e1ce0a346cd4b035f580193ca18438f130eeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 28 Feb 2023 00:28:31 +0100 Subject: [PATCH 17/25] Added documentation on how to setup SAML. --- docs/installation/saml_sso.md | 89 +++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 docs/installation/saml_sso.md diff --git a/docs/installation/saml_sso.md b/docs/installation/saml_sso.md new file mode 100644 index 00000000..13677f7d --- /dev/null +++ b/docs/installation/saml_sso.md @@ -0,0 +1,89 @@ +--- +title: Single Sign-On via SAML +layout: default +parent: Installation +nav_order: 12 +--- + +# Single Sign-On via SAML + +Part-DB supports Single Sign-On via SAML. This means that you can use your existing SAML identity provider to log in to Part-DB. +Using an intermediate SAML server like [Keycloak](https://www.keycloak.org/), also allows you to connect Part-DB to a LDAP or Active Directory server. + +{: .important } +> This feature is for advanced users only. Single Sign-On is useful for large organizations with many users, which are already using SAML for other services. +> If you have only one or a few users, you should use the built-in authentication system of Part-DB. +> This guide assumes that you already have an SAML identity provider set up and working, and have a basic understanding of how SAML works. + +{: .warning } +> This feature is currently in beta. Please report any bugs you find. +> So far it has only tested with Keycloak, but it should work with any SAML 2.0 compatible identity provider. + +This guide will show you how to configure Part-DB with [Keycloak](https://www.keycloak.org/) as the SAML identity provider, +but it should work with any SAML 2.0 compatible identity provider. + +This guide assumes that you have a working Keycloak installation with some users. If you don't, you can follow the [Keycloak Getting Started Guide](https://www.keycloak.org/docs/latest/getting_started/index.html). + +## Configure basic SAML connection + +### Create a new SAML client +1. First, you need to configure a new SAML client in Keycloak. Login in to your Keycloak admin console and go to the `Clients` page. +2. Click on `Create client` and select `SAML` as type from the dropdown menu. For the client ID, you can use anything you want, but it should be unique. +*It is recommended to set this value to the domain name of your Part-DB installation, with an attached `/sp` (e.g. `https://partdb.yourdomain.invalid/sp`)*. +The name field should be set to something human-readable, like `Part-DB`. +3. Click on `Save` to create the new client. + +### Configure the SAML client + +1. Now you need to configure the SAML client. Go to the `Settings` tab and set the following values: + * Set `Home URL` to the homepage of your Part-DB installation (e.g. `https://partdb.yourdomain.invalid/`). + * Set `Valid redirect URIs` to your homepage with a wildcard at the end (e.g. `https://partdb.yourdomain.invalid/*`). + * Set `Valid post logout redirect URIs` to `+` to allow all urls from the `Valid redirect URIs`. + * Set `Name ID format` to `username` + * Ensure `Force POST binding` is enabled. + * Ensure `Sign documents` is enabled. + * Ensure `Front channel logout` is enabled. + * Ensure `Signature Algorithm` is set to `RSA_SHA256`. + + Click on `Save` to save the changes. +2. Go to the `Advanced` tab and set the following values: + * Assertion Consumer Service POST Binding URL to your homepage with `/saml/acs` at the end (e.g. `https://partdb.yourdomain.invalid/saml/acs`). + * Logout Service POST Binding URL to your homepage with `/logout` at the end (e.g. `https://partdb.yourdomain.invalid/logout`). +3. Go to Keys tab and ensure `Client Signature Required` is enabled. +4. In the Keys tab click on `Generate new keys`. This will generate a new key pair for the SAML client. The private key will be downloaded to your computer. + +### Configure Part-DB to use SAML +1. Open the `.env.local` file of Part-DB (or the docker-compose.yaml) for edit +2. Set the `SAMLP_SP_PRIVATE_KEY` environment variable to the content of the private key file you downloaded in the previous step. It should start with `MIEE` and end with `=`. +3. Set the `SAML_SP_X509_CERT` environment variable to the content of the Certificate field shown in the `Keys` tab of the SAML client in Keycloak. It should start with `MIIC` and end with `=`. +4. Set the `SAML_SP_ENTITY_ID` environment variable to the entityID of the SAML client in Keycloak (e.g. `https://partdb.yourdomain.invalid/sp`). +5. In Keycloak navigate to `Realm Settings` -> `SAML 2.0 Identity Provider` (by default something like `https://idp.yourdomain.invalid/realms/master/protocol/saml/descriptor) to show the SAML metadata. +6. Copy the `entityID` value from the metadata to the `SAML_IDP_ENTITY_ID` configuration variable of Part-DB (by default something like `https://idp.yourdomain.invalid/realms/master`). +7. Copy the `Single Sign-On Service` value from the metadata to the `SAML_IDP_SINGLE_SIGN_ON_SERVICE` configuration variable of Part-DB (by default something like `https://idp.yourdomain.invalid/realms/master/protocol/saml`). +8. Copy the `Single Logout Service` value from the metadata to the `SAML_IDP_SINGLE_LOGOUT_SERVICE` configuration variable of Part-DB (by default something like `https://idp.yourdomain.invalid/realms/master/protocol/saml/logout`). +9. Copy the `X.509 Certificate` value from the metadata to the `SAML_IDP_X509_CERT` configuration variable of Part-DB (it should start with `MIIC` and should be pretty long). +10. Set the `SAML_ENABLED` configuration in Part-DB to 1 to enable SAML authentication. + +When you access the Part-DB login form now, you should see a new button to log in via SSO. Click on it to be redirected to the SAML identity provider and log in. + +In the following sections, you will learn how to configure that Part-DB uses the data provided by the SAML identity provider to fill out user informations. + +### Set user information based on SAML attributes +Part-DB can set basic user information like the username, the real name and the email address based on the SAML attributes provided by the SAML identity provider. +To do this, you need to configure your SAML identity provider to provide the following attributes: + +* `email` or `urn:oid:1.2.840.113549.1.9.1` for the email address +* `firstName` or `urn:oid:2.5.4.42` for the first name +* `lastName` or `urn:oid:2.5.4.4` for the last name +* `department` for the department field of the user + +You can omit any of these attributes, but then the corresponding field will be empty (but can be overriden by an administrator). +These values are written to Part-DB database, whenever the user logs in via SAML (the user is created on the first login, and updated on every login). + +To configure Keycloak to provide these attributes, you need to go to the `Client scopes` page and select the `sp-dedicatd` client scope (or create a new one). +In the scope configuration page, click on `Add mappers` and `From predefined mappers`. Select the following mappers: +* `X500 email` +* `X500 givenName` +* `X500 surname` + +and click `Add`. Now Part-DB will be provided with the email, first name and last name of the user based on the Keycloak user database. \ No newline at end of file From cb9433902c372e1022206b425be74aa92b0c5b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 28 Feb 2023 16:34:51 +0100 Subject: [PATCH 18/25] Added SAML configuration options to docs --- .env | 3 ++- docs/configuration.md | 17 +++++++++++++++++ docs/installation/reverse-proxy.md | 2 ++ docs/installation/saml_sso.md | 3 ++- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.env b/.env index 352d553a..efb11189 100644 --- a/.env +++ b/.env @@ -80,7 +80,8 @@ ERROR_PAGE_SHOW_HELP=1 SAML_ENABLED=0 # A JSON encoded array of role mappings in the form { "saml_role": PARTDB_GROUP_ID, "*": PARTDB_GROUP_ID } -SAML_ROLE_MAPPING="{}" +# Please not to only use single quotes to enclose the JSON string +SAML_ROLE_MAPPING='{}' # A mapping could look like the following #SAML_ROLE_MAPPING='{ "*": 2, "editor": 3, "admin": 1 }' diff --git a/docs/configuration.md b/docs/configuration.md index 3c947f48..ceacf2ea 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -26,6 +26,7 @@ The following configuration options can only be changed by the server administra * `INSTANCE_NAME`: The name of your installation. It will be shown as a title in the navbar and other places. By default `Part-DB`, but you can customize it to something likes `ExampleCorp. Inventory`. * `ALLOW_ATTACHMENT_DOWNLOADS` (allowed values `0` or `1`): By setting this option to 1, users can make Part-DB directly download a file specified as an URL and create it as local file. Please not that this allows users access to all ressources publicly available to the server (so full access to other servers in the same local network), which could be a security risk. * `USE_GRAVATAR`: Set to `1` to use [gravatar.com](gravatar.com) images for user avatars (as long as they have not set their own picture). The users browsers have to download the pictures from a third-party (gravatars) server, so this might be a privacy risk. +* `DEFAULT_URI`: The default URI base to use for the Part-DB, when no URL can be determined from the browser request. This should be the primary URL/Domain, which is used to access Part-DB. This value is used to create correct links in emails and other places, where the URL is needed. It is also used, when SAML is enabled.s If you are using a reverse proxy, you should set this to the URL of the reverse proxy (e.g. `https://part-db.example.com`). **This value must end with a slash**. ### E-Mail settings @@ -46,6 +47,22 @@ If you wanna use want to revert changes or view older revisions of entities, the * `ERROR_PAGE_ADMIN_EMAIL`: You can set an email-address here, which is shown on the error page, who should be contacted about the issue (e.g. an IT support email of your company) * `ERROR_PAGE_SHOW_HELP`: Set this 0, to disable the solution hints shown on an error page. These hints should not contain senstive informations, but could confuse end-users. +### SAML SSO settings +The following settings can be used to enable and configure Single-Sign on via SAML. This allows users to login to Part-DB without entering a username and password, but instead they are redirected to a SAML Identity Provider (IdP) and are logged in automatically. This is especially useful, when you want to use Part-DB in a company, where all users have a SAML account (e.g. via Active Directory or LDAP). +You can find more advanced settings in the `config/packages/hslavich_onelogin_saml.yaml` file. Please note that this file is not backuped by the backup script, so you have to backup it manually, if you want to keep your changes. If you want to edit it on docker, you have to map the file to a volume. + +* `SAML_ENABLED`: When this is set to 1, SAML SSO is enabled and the SSO Login button is shown in the login form. You have to configure the SAML settings below, before you can use this feature. +* `SAML_ROLE_MAPPING`: A [JSON](https://en.wikipedia.org/wiki/JSON) encoded map which specifies how Part-DB should convert the user roles given by SAML attribute `group` should be converted to a Part-DB group (specified by ID). You can use a wildcard `*` to map all otherwise unmapped roles to a certain group. Example: `{"*": 1, "admin": 2, "editor": 3}`. This would map all roles to the group with ID 1, except the role `admin`, which is mapped to the group with ID 2 and the role `editor`, which is mapped to the group with ID 3. +* `SAML_UPDATE_GROUP_ON_LOGIN`: When this is enabled the group of the user is updated on every login of the user based on the SAML role attributes. When this is disabled, the group is only assigned on the first login of the user, and a Part-DB administrator can change the group afterwards by editing the user. +* `SAML_IDP_ENTITY_ID`: The entity ID of your SAML Identity Provider (IdP). You can find this value in the metadata XML file or configuration UI of your IdP. +* `SAML_IDP_SINGLE_SIGN_ON_SERVICE`: The URL of the SAML IdP Single Sign-On Service (SSO). You can find this value in the metadata XML file or configuration UI of your IdP. +* `SAML_IDP_SINGLE_LOGOUT_SERVICE`: The URL of the SAML IdP Single Logout Service (SLO). You can find this value in the metadata XML file or configuration UI of your IdP. +* `SAML_IDP_X509_CERT`: The base64 encoded X.509 public certificate of your SAML IdP. You can find this value in the metadata XML file or configuration UI of your IdP. It should start with `MIIC` and end with `=`. +* `SAML_SP_ENTITY_ID`: The entity ID of your SAML Service Provider (SP). This is the value you have configured for the Part-DB client in your IdP. +* `SAML_SP_X509_CERT`: The public X.509 certificate of your SAML SP (here Part-DB). This is the value you have configured for the Part-DB client in your IdP. It should start with `MIIC` and end with `=`. IdPs like keycloak allows you to generate a public/private key pair for the client which you can setup here and in the `SAML_SP_PRIVATE_KEY` setting. +* `SAML_SP_PRIVATE_KEY`: The private key of your SAML SP (here Part-DB), corresponding the public key specified in `SAML_SP_X509_CERT`. This is the value you have configured for the Part-DB client in your IdP. It should start with `MIIE` and end with `=`. IdPs like keycloak allows you to generate a public/private key pair for the client which you can setup here and in the `SAML_SP_X509_CERT` setting. + + ### Other / less used options * `TRUSTED_PROXIES`: Set the IP addresses (or IP blocks) of trusted reverse proxies here. This is needed to get correct IP informations (see [here](https://symfony.com/doc/current/deployment/proxies.html) for more info). * `TRUSTED_HOSTS`: To prevent `HTTP Host header attacks` you can set a regex containing all host names via which Part-DB should be accessible. If accessed via the wrong hostname, an error will be shown. diff --git a/docs/installation/reverse-proxy.md b/docs/installation/reverse-proxy.md index 5fdfef01..d42e5a2b 100644 --- a/docs/installation/reverse-proxy.md +++ b/docs/installation/reverse-proxy.md @@ -17,3 +17,5 @@ For example, if your reverse proxy has the IP address `192.168.2.10`, your value ``` TRUSTED_PROXIES=192.168.2.10 ``` + +Set the `DEFAULT_URI` environment variable to the URL of your Part-DB installation, available from the outside (so via the reverse proxy). \ No newline at end of file diff --git a/docs/installation/saml_sso.md b/docs/installation/saml_sso.md index 13677f7d..56a9a8e7 100644 --- a/docs/installation/saml_sso.md +++ b/docs/installation/saml_sso.md @@ -62,7 +62,8 @@ The name field should be set to something human-readable, like `Part-DB`. 7. Copy the `Single Sign-On Service` value from the metadata to the `SAML_IDP_SINGLE_SIGN_ON_SERVICE` configuration variable of Part-DB (by default something like `https://idp.yourdomain.invalid/realms/master/protocol/saml`). 8. Copy the `Single Logout Service` value from the metadata to the `SAML_IDP_SINGLE_LOGOUT_SERVICE` configuration variable of Part-DB (by default something like `https://idp.yourdomain.invalid/realms/master/protocol/saml/logout`). 9. Copy the `X.509 Certificate` value from the metadata to the `SAML_IDP_X509_CERT` configuration variable of Part-DB (it should start with `MIIC` and should be pretty long). -10. Set the `SAML_ENABLED` configuration in Part-DB to 1 to enable SAML authentication. +10. Set the `DEFAULT_URI` to the homepage (on the publicly available domain) of your Part-DB installation (e.g. `https://partdb.yourdomain.invalid/`). It must end with a slash. +11. Set the `SAML_ENABLED` configuration in Part-DB to 1 to enable SAML authentication. When you access the Part-DB login form now, you should see a new button to log in via SSO. Click on it to be redirected to the SAML identity provider and log in. From 8a189515621ea1c7ce057c5761dda4226809bb2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 28 Feb 2023 17:03:57 +0100 Subject: [PATCH 19/25] Fixed static analysis issue. --- src/Security/SamlUserFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Security/SamlUserFactory.php b/src/Security/SamlUserFactory.php index 05394306..66c4b0a5 100644 --- a/src/Security/SamlUserFactory.php +++ b/src/Security/SamlUserFactory.php @@ -91,7 +91,7 @@ class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterf //If needed update the group of the user and save it to DB if ($group !== $user->getGroup()) { $user->setGroup($group); - $this->em->flush($user); + $this->em->flush(); } } From d845f8b7e303eeb01927a9f3fb1af53717541cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Wed, 1 Mar 2023 14:36:46 +0100 Subject: [PATCH 20/25] Added documentation about the convert-to-saml-user command --- docs/installation/saml_sso.md | 15 ++++++++++++++- docs/usage/console_commands.md | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/installation/saml_sso.md b/docs/installation/saml_sso.md index 56a9a8e7..099b082e 100644 --- a/docs/installation/saml_sso.md +++ b/docs/installation/saml_sso.md @@ -87,4 +87,17 @@ In the scope configuration page, click on `Add mappers` and `From predefined map * `X500 givenName` * `X500 surname` -and click `Add`. Now Part-DB will be provided with the email, first name and last name of the user based on the Keycloak user database. \ No newline at end of file +and click `Add`. Now Part-DB will be provided with the email, first name and last name of the user based on the Keycloak user database. + +### Configure user permissions + + +### Use SAML Login for existing users +Part-DB distinguishes between local users and SAML users. Local users are users, which can login via Part-DB login form and which use the password (hash) saved in the Part-DB database. SAML users are stored in the database too (they are created on the first login of the user via SAML), but they use the SAML identity provider to authenticate the user and have no password stored in the database. When you try you will get an error message. + +For security reasons it is not possible to authenticate via SAML as a local user (and vice versa). So if you have existing users in your Part-DB database and want them to be able to login via SAML in the future, you can use the `php bin/console partdb:user:convert-to-saml-user username` command to convert them to SAML users. This will remove the password hash from the database and mark them as SAML users, so they can login via SAML in the future. + +The reverse is also possible: If you have existing SAML users and want them to be able to login via the Part-DB login form, you can use the `php bin/console partdb:user:convert-to-saml-user --to-local username` command to convert them to local users. You have to set an password for the user afterwards. + +{: .important } +> It is recommended that you let the original admin user (ID: 2) be a local user, so you can still login to Part-DB if the SAML identity provider is not available. \ No newline at end of file diff --git a/docs/usage/console_commands.md b/docs/usage/console_commands.md index ccc9a64d..f762f614 100644 --- a/docs/usage/console_commands.md +++ b/docs/usage/console_commands.md @@ -19,6 +19,7 @@ You can get help for every command with the parameter `--help`. See `php bin/con * `php bin/console partdb:users:permissions`: View/Change the permissions of the user with the given username * `php bin/console partdb:users:upgrade-permissions-schema`: Upgrade the permissions schema of users to the latest version (this is normally automatically done when the user visits a page) * `php bin/console partdb:logs:show`: Show the most recent entries of the Part-DB event log / recent activity +* `php bin/console partdb:user:convert-to-saml-user`: Convert a local user to a SAML/SSO user. This is needed, if you want to use SAML/SSO authentication for a user, which was created before you enabled SAML/SSO authentication. ## Currency commands * `php bin/console partdb:currencies:update-exchange-rates`: Update the exchange rates of all currencies from the internet) From 7030e752fc3a8e1e2cf7053563c933284c94b550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Wed, 1 Mar 2023 14:56:05 +0100 Subject: [PATCH 21/25] Added documentation about permission mapping. --- docs/installation/saml_sso.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/installation/saml_sso.md b/docs/installation/saml_sso.md index 099b082e..cbf33dc1 100644 --- a/docs/installation/saml_sso.md +++ b/docs/installation/saml_sso.md @@ -89,8 +89,27 @@ In the scope configuration page, click on `Add mappers` and `From predefined map and click `Add`. Now Part-DB will be provided with the email, first name and last name of the user based on the Keycloak user database. -### Configure user permissions +### Configure permissions for SAML users +On the first login of a SAML user, Part-DB will create a new user in the database. This user will have the same username as the SAML user, but no password set. The user will be marked as a SAML user, so he can only login via SAML in the future. However in other aspects the user is a normal user, so Part-DB admins can set permissions for SAML users like for any other user and override permissions assigned via groups. +However for large organizations you maybe want to automatically assign permissions to SAML users based on the roles or groups configured in the identity provider. For this purpose Part-DB allows you to map SAML roles or groups to Part-DB groups. See the next section for details. + +### Map SAML roles to Part-DB groups +Part-DB allows you to configure a mapping between SAML roles or groups and Part-DB groups. This allows you to automatically assign permissions to SAML users based on the roles or groups configured in the identity provider. For example if a user at your SAML provider has the role `admin`, you can configure Part-DB to assign the `admin` group to this user. This will give the user all permissions of the `admin` group. + +For this you need first have to create the groups in Part-DB, to which you want to assign the users and configure their permissions. You will need the IDs of the groups, which you can find in the `System->Group` page of Part-DB in the Info tab. + +The map is provided as [JSON](https://en.wikipedia.org/wiki/JSON) encoded map between the SAML role and the group ID, which has the form `{"saml_role": group_id, "*": group_id, ...}`. You can use the `*` key to assign a group to all users, which are not in any other group. The map is configured via the `SAML_ROLE_MAPPING` environment variable, which you can configure via the `.env.local` or `docker-compose.yml` file. Please note that you have to enclose the JSON string in single quotes here, as JSON itself uses double quotes (e.g. `SAML_ROLE_MAPPING='{ "*": 2, "editor": 3, "admin": 1 }'). + +For example if you want to assign the group with ID 1 (by default admin) to every SAML user which has the role `admin`, the role with ID 3 (by default editor) to every SAML user with the role `editor` and everybody else to the group with ID 2 (by default readonly), you can configure the following map: + +``` +SAML_ROLE_MAPPING='{"admin": 1, "editor": 3, "*": 2}' +``` + +If you want to assign users with a certain role to a empty group, provide the group ID -1 as the value. This is not a valid group ID, so the user will not be assigned to any group. + +The SAML roles (or groups depending on your configuration), have to be supplied via a SAML attribute `group`. You have to configure your SAML identity provider to provide this attribute. For example in Keycloak you can configure this attribute in the `Client scopes` page. Select the `sp-dedicatd` client scope (or create a new one) and click on `Add mappers`. Select `Role mapping` or `Group membership`, change the field name and click `Add`. Now Part-DB will be provided with the groups of the user based on the Keycloak user database. ### Use SAML Login for existing users Part-DB distinguishes between local users and SAML users. Local users are users, which can login via Part-DB login form and which use the password (hash) saved in the Part-DB database. SAML users are stored in the database too (they are created on the first login of the user via SAML), but they use the SAML identity provider to authenticate the user and have no password stored in the database. When you try you will get an error message. From bbe4de996ac967736974745a0a480d94ae6be174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Wed, 1 Mar 2023 15:24:47 +0100 Subject: [PATCH 22/25] Added documentation about the SAML_UPDATE_GROUP_ON_LOGIN env --- docs/installation/saml_sso.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/installation/saml_sso.md b/docs/installation/saml_sso.md index cbf33dc1..995a8858 100644 --- a/docs/installation/saml_sso.md +++ b/docs/installation/saml_sso.md @@ -111,6 +111,8 @@ If you want to assign users with a certain role to a empty group, provide the gr The SAML roles (or groups depending on your configuration), have to be supplied via a SAML attribute `group`. You have to configure your SAML identity provider to provide this attribute. For example in Keycloak you can configure this attribute in the `Client scopes` page. Select the `sp-dedicatd` client scope (or create a new one) and click on `Add mappers`. Select `Role mapping` or `Group membership`, change the field name and click `Add`. Now Part-DB will be provided with the groups of the user based on the Keycloak user database. +By default the group is assigned to the user on the first login and updated on every login based on the SAML attributes. This allows you to configure the groups in the SAML identity provider and the users will automatically stay up to date with their permissions. However if you want to disable this behavior (and let the Part-DB admins configure the groups manually, after the first login), you can set the `SAML_UPDATE_GROUP_ON_LOGIN` environment variable to `false`. If you want to disable the automatic group assignment completly (so not even on the first login of a user), set the `SAML_ROLE_MAPPING` to `{}` (empty JSON object). + ### Use SAML Login for existing users Part-DB distinguishes between local users and SAML users. Local users are users, which can login via Part-DB login form and which use the password (hash) saved in the Part-DB database. SAML users are stored in the database too (they are created on the first login of the user via SAML), but they use the SAML identity provider to authenticate the user and have no password stored in the database. When you try you will get an error message. From 8fad743e85bc8c881b36049dc9013731cc73836f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 4 Mar 2023 16:52:17 +0100 Subject: [PATCH 23/25] Allow to select the priority of SAML role mapping based on the order in the configuration option --- .env | 3 ++- docs/installation/saml_sso.md | 6 +++++- src/Security/SamlUserFactory.php | 15 ++++++++++----- tests/Security/SamlUserFactoryTest.php | 8 +++++--- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/.env b/.env index efb11189..bac6a95c 100644 --- a/.env +++ b/.env @@ -80,10 +80,11 @@ ERROR_PAGE_SHOW_HELP=1 SAML_ENABLED=0 # A JSON encoded array of role mappings in the form { "saml_role": PARTDB_GROUP_ID, "*": PARTDB_GROUP_ID } +# The first match is used, so the order is important! Put the group mapping with the most privileges first. # Please not to only use single quotes to enclose the JSON string SAML_ROLE_MAPPING='{}' # A mapping could look like the following -#SAML_ROLE_MAPPING='{ "*": 2, "editor": 3, "admin": 1 }' +#SAML_ROLE_MAPPING='{ "*": 2, "admin": 1, "editor": 3}' # When this is set to 1, the group of SAML users will be updated everytime they login based on their SAML roles SAML_UPDATE_GROUP_ON_LOGIN=1 diff --git a/docs/installation/saml_sso.md b/docs/installation/saml_sso.md index 995a8858..16c22be8 100644 --- a/docs/installation/saml_sso.md +++ b/docs/installation/saml_sso.md @@ -107,11 +107,15 @@ For example if you want to assign the group with ID 1 (by default admin) to ever SAML_ROLE_MAPPING='{"admin": 1, "editor": 3, "*": 2}' ``` +Please not that the order of the mapping is important. The first matching role will be assigned to the user. So if you have a user with the roles `admin` and `editor`, he will be assigned to the group with ID 1 (admin) and not to the group with ID 3 (editor), as the `admin` role comes first in the JSON map. +This mean that you should always put the most specific roles (e.g. admins) first of the map and the most general roles (e.g. normal users) later. + If you want to assign users with a certain role to a empty group, provide the group ID -1 as the value. This is not a valid group ID, so the user will not be assigned to any group. The SAML roles (or groups depending on your configuration), have to be supplied via a SAML attribute `group`. You have to configure your SAML identity provider to provide this attribute. For example in Keycloak you can configure this attribute in the `Client scopes` page. Select the `sp-dedicatd` client scope (or create a new one) and click on `Add mappers`. Select `Role mapping` or `Group membership`, change the field name and click `Add`. Now Part-DB will be provided with the groups of the user based on the Keycloak user database. -By default the group is assigned to the user on the first login and updated on every login based on the SAML attributes. This allows you to configure the groups in the SAML identity provider and the users will automatically stay up to date with their permissions. However if you want to disable this behavior (and let the Part-DB admins configure the groups manually, after the first login), you can set the `SAML_UPDATE_GROUP_ON_LOGIN` environment variable to `false`. If you want to disable the automatic group assignment completly (so not even on the first login of a user), set the `SAML_ROLE_MAPPING` to `{}` (empty JSON object). +By default, the group is assigned to the user on the first login and updated on every login based on the SAML attributes. This allows you to configure the groups in the SAML identity provider and the users will automatically stay up to date with their permissions. However if you want to disable this behavior (and let the Part-DB admins configure the groups manually, after the first login), you can set the `SAML_UPDATE_GROUP_ON_LOGIN` environment variable to `false`. If you want to disable the automatic group assignment completly (so not even on the first login of a user), set the `SAML_ROLE_MAPPING` to `{}` (empty JSON object). + ### Use SAML Login for existing users Part-DB distinguishes between local users and SAML users. Local users are users, which can login via Part-DB login form and which use the password (hash) saved in the Part-DB database. SAML users are stored in the database too (they are created on the first login of the user via SAML), but they use the SAML identity provider to authenticate the user and have no password stored in the database. When you try you will get an error message. diff --git a/src/Security/SamlUserFactory.php b/src/Security/SamlUserFactory.php index 66c4b0a5..39e67c0c 100644 --- a/src/Security/SamlUserFactory.php +++ b/src/Security/SamlUserFactory.php @@ -120,6 +120,7 @@ class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterf /** * Maps a list of SAML roles to a local group ID. + * The first available mapping will be used (so the order of the $map is important, first match wins). * @param array $roles The list of SAML roles * @param array $map|null The mapping from SAML roles. If null, the global mapping will be used. * @return int|null The ID of the local group or null if no mapping was found. @@ -128,14 +129,18 @@ class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterf { $map = $map ?? $this->saml_role_mapping; - //Iterate over all roles and check if we have a mapping for it. - foreach ($roles as $role) { - if (array_key_exists($role, $map)) { - //We use the first available mapping - return (int) $map[$role]; + //Iterate over the mapping (from first to last) and check if we have a match + foreach ($map as $saml_role => $group_id) { + //Skip wildcard + if ($saml_role === '*') { + continue; + } + if (in_array($saml_role, $roles, true)) { + return (int) $group_id; } } + //If no applicable mapping was found, check if we have a default mapping if (array_key_exists('*', $map)) { return (int) $map['*']; diff --git a/tests/Security/SamlUserFactoryTest.php b/tests/Security/SamlUserFactoryTest.php index ab349f68..f7dd15d7 100644 --- a/tests/Security/SamlUserFactoryTest.php +++ b/tests/Security/SamlUserFactoryTest.php @@ -66,18 +66,20 @@ class SamlUserFactoryTest extends WebTestCase public function testMapSAMLRolesToLocalGroupID() { $mapping = [ + 'admin' => 2, //This comes first, as this should have higher priority 'employee' => 1, - 'admin' => 2, 'manager' => 3, 'administrator' => 2, '*' => 4, ]; //Test if mapping works - $this->assertEquals(1, $this->service->mapSAMLRolesToLocalGroupID(['employee'], $mapping)); + $this->assertSame(1, $this->service->mapSAMLRolesToLocalGroupID(['employee'], $mapping)); //Only the first valid mapping should be used - $this->assertEquals(1, $this->service->mapSAMLRolesToLocalGroupID(['employee', 'admin'], $mapping)); + $this->assertSame(2, $this->service->mapSAMLRolesToLocalGroupID(['employee', 'admin'], $mapping)); $this->assertSame(2, $this->service->mapSAMLRolesToLocalGroupID(['does_not_matter', 'admin', 'employee'], $mapping)); + $this->assertSame(1, $this->service->mapSAMLRolesToLocalGroupID(['employee', 'does_not_matter', 'manager'], $mapping)); + $this->assertSame(3, $this->service->mapSAMLRolesToLocalGroupID(['administrator', 'does_not_matter', 'manager'], $mapping)); //Test if mapping is case sensitive $this->assertEquals(4, $this->service->mapSAMLRolesToLocalGroupID(['ADMIN'], $mapping)); From eabdd3b11f4a55d3b744fe8c6a4a8f10e18c7d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 4 Mar 2023 16:56:41 +0100 Subject: [PATCH 24/25] Improved documentation for SAML SSO --- docs/installation/saml_sso.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/installation/saml_sso.md b/docs/installation/saml_sso.md index 16c22be8..09349047 100644 --- a/docs/installation/saml_sso.md +++ b/docs/installation/saml_sso.md @@ -112,10 +112,20 @@ This mean that you should always put the most specific roles (e.g. admins) first If you want to assign users with a certain role to a empty group, provide the group ID -1 as the value. This is not a valid group ID, so the user will not be assigned to any group. -The SAML roles (or groups depending on your configuration), have to be supplied via a SAML attribute `group`. You have to configure your SAML identity provider to provide this attribute. For example in Keycloak you can configure this attribute in the `Client scopes` page. Select the `sp-dedicatd` client scope (or create a new one) and click on `Add mappers`. Select `Role mapping` or `Group membership`, change the field name and click `Add`. Now Part-DB will be provided with the groups of the user based on the Keycloak user database. +The SAML roles (or groups depending on your configuration), have to be supplied via a SAML attribute `group`. You have to configure your SAML identity provider to provide this attribute. For example in Keycloak you can configure this attribute in the `Client scopes` page. Select the `sp-dedicated` client scope (or create a new one) and click on `Add mappers`. Select `Role mapping` or `Group membership`, change the field name and click `Add`. Now Part-DB will be provided with the groups of the user based on the Keycloak user database. -By default, the group is assigned to the user on the first login and updated on every login based on the SAML attributes. This allows you to configure the groups in the SAML identity provider and the users will automatically stay up to date with their permissions. However if you want to disable this behavior (and let the Part-DB admins configure the groups manually, after the first login), you can set the `SAML_UPDATE_GROUP_ON_LOGIN` environment variable to `false`. If you want to disable the automatic group assignment completly (so not even on the first login of a user), set the `SAML_ROLE_MAPPING` to `{}` (empty JSON object). +By default, the group is assigned to the user on the first login and updated on every login based on the SAML attributes. This allows you to configure the groups in the SAML identity provider and the users will automatically stay up to date with their permissions. However, if you want to disable this behavior (and let the Part-DB admins configure the groups manually, after the first login), you can set the `SAML_UPDATE_GROUP_ON_LOGIN` environment variable to `false`. If you want to disable the automatic group assignment completly (so not even on the first login of a user), set the `SAML_ROLE_MAPPING` to `{}` (empty JSON object). +### Overview of possible SAML attributes used by Part-DB +The following table shows all SAML attributes, which can be usedby Part-DB. If your identity provider is configured to provide these attributes, you can use to automatically fill the corresponding fields of the user in Part-DB. + +| SAML attribute | Part-DB user field | Description | +|-------------------------------------------|-------------------|-------------------------------------------------------------------| +| `urn:oid:1.2.840.113549.1.9.1` or `email` | email | The email address of the user. | +| `urn:oid:2.5.4.42` or `firstName` | firstName | The first name of the user. | +| `urn:oid:2.5.4.4` or `lastName` | lastName | The last name of the user. | +| `department` | department | The department of the user. | +| `group` | group | The group of the user (determined by `SAML_ROLE_MAPPING` option). | ### Use SAML Login for existing users Part-DB distinguishes between local users and SAML users. Local users are users, which can login via Part-DB login form and which use the password (hash) saved in the Part-DB database. SAML users are stored in the database too (they are created on the first login of the user via SAML), but they use the SAML identity provider to authenticate the user and have no password stored in the database. When you try you will get an error message. From 20caad24ed0a0102678a16e97a2984506d48da32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 4 Mar 2023 17:15:17 +0100 Subject: [PATCH 25/25] Improved documentation --- README.md | 1 + docs/index.md | 1 + docs/installation/saml_sso.md | 7 ++++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 254e0cbe..2ab62eba 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ and multiple store locations and price information. Parts can be grouped using t * Barcodes/Labels generator for parts and storage locations, scan barcodes via webcam using the builtin barcode scanner * User system with groups and detailed (fine granular) permissions. Two-factor authentication is supported (Google Authenticator and Webauthn/U2F keys) and can be enforced for groups. Password reset via email can be setuped. +* Optional support for single sign-on (SSO) via SAML (using an intermediate service like [Keycloak](https://www.keycloak.org/) you can connect Part-DB to an existing LDAP or Active Directory server) * Import/Export system (partial working) * Project management: Create projects and assign parts to the bill of material (BOM), to show how often you could build this project and directly withdraw all components needed from DB * Event log: Track what changes happens to your inventory, track which user does what. Revert your parts to older versions. diff --git a/docs/index.md b/docs/index.md index c2f561cf..9bc5b1a4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,6 +27,7 @@ It is installed on a web server and so can be accessed with any browser without * Barcodes/Labels generator for parts and storage locations, scan barcodes via webcam using the builtin barcode scanner * User system with groups and detailed (fine granular) permissions. Two-factor authentication is supported (Google Authenticator and Webauthn/U2F keys) and can be enforced for groups. Password reset via email can be setuped. +* Optional support for single sign-on (SSO) via SAML (using an intermediate service like [Keycloak](https://www.keycloak.org/) you can connect Part-DB to an existing LDAP or Active Directory server) * Import/Export system (partial working) * Project management: Create projects and assign parts to the bill of material (BOM), to show how often you could build this project and directly withdraw all components needed from DB * Event log: Track what changes happens to your inventory, track which user does what. Revert your parts to older versions. diff --git a/docs/installation/saml_sso.md b/docs/installation/saml_sso.md index 09349047..4922d042 100644 --- a/docs/installation/saml_sso.md +++ b/docs/installation/saml_sso.md @@ -24,6 +24,11 @@ but it should work with any SAML 2.0 compatible identity provider. This guide assumes that you have a working Keycloak installation with some users. If you don't, you can follow the [Keycloak Getting Started Guide](https://www.keycloak.org/docs/latest/getting_started/index.html). +{: .important } +> Part-DB associates local users with SAML users by their username. That means if the username of a SAML user changes, a new local user will be created (and the old account can not be accessed). +> You should make sure that the username of a SAML user does not change. If you use Keycloak make sure that the possibility to change the username is disabled (which is by default). +> If you really have to rename a SAML user, a Part-DB admin can rename the local user in the Part-DB in the admin panel, to match the new username of the SAML user. + ## Configure basic SAML connection ### Create a new SAML client @@ -99,7 +104,7 @@ Part-DB allows you to configure a mapping between SAML roles or groups and Part- For this you need first have to create the groups in Part-DB, to which you want to assign the users and configure their permissions. You will need the IDs of the groups, which you can find in the `System->Group` page of Part-DB in the Info tab. -The map is provided as [JSON](https://en.wikipedia.org/wiki/JSON) encoded map between the SAML role and the group ID, which has the form `{"saml_role": group_id, "*": group_id, ...}`. You can use the `*` key to assign a group to all users, which are not in any other group. The map is configured via the `SAML_ROLE_MAPPING` environment variable, which you can configure via the `.env.local` or `docker-compose.yml` file. Please note that you have to enclose the JSON string in single quotes here, as JSON itself uses double quotes (e.g. `SAML_ROLE_MAPPING='{ "*": 2, "editor": 3, "admin": 1 }'). +The map is provided as [JSON](https://en.wikipedia.org/wiki/JSON) encoded map between the SAML role and the group ID, which has the form `{"saml_role": group_id, "*": group_id, ...}`. You can use the `*` key to assign a group to all users, which are not in any other group. The map is configured via the `SAML_ROLE_MAPPING` environment variable, which you can configure via the `.env.local` or `docker-compose.yml` file. Please note that you have to enclose the JSON string in single quotes here, as JSON itself uses double quotes (e.g. `SAML_ROLE_MAPPING='{ "*": 2, "editor": 3, "admin": 1 }`). For example if you want to assign the group with ID 1 (by default admin) to every SAML user which has the role `admin`, the role with ID 3 (by default editor) to every SAML user with the role `editor` and everybody else to the group with ID 2 (by default readonly), you can configure the following map: