From b92893e075d679adc9c90b3477da602c0bc1bbff Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Sun, 18 Dec 2016 01:32:17 -0600 Subject: [PATCH] Added support to save xml and certificate data along with defining user attributes for SAML --- .../views/discovery.html.twig | 16 ++ app/bundles/ConfigBundle/Config/config.php | 4 +- .../Controller/ConfigController.php | 173 ++++++++++++------ .../ConfigBundle/Event/ConfigBuilderEvent.php | 75 ++++++-- .../ConfigBundle/Event/ConfigEvent.php | 55 +++++- .../ConfigBundle/Form/Type/ConfigType.php | 97 ++++++---- .../ConfigBundle/Views/Config/form.html.php | 2 +- .../Translations/en_US/validators.ini | 3 +- .../LeadBundle/Form/Type/LeadImportType.php | 130 ++++++++----- .../Translations/en_US/validators.ini | 2 +- app/bundles/UserBundle/Config/config.php | 29 +-- .../DependencyInjection/Compiler/SamlPass.php | 71 +++++++ .../EventListener/ConfigSubscriber.php | 74 ++++++-- .../EventListener/RouteSubscriber.php | 39 ++++ .../UserBundle/Form/Type/ConfigType.php | 119 ++++++++++-- app/bundles/UserBundle/MauticUserBundle.php | 3 + .../Authenticator/FormAuthenticator.php | 2 +- .../UserBundle/Security/Store/IdStore.php | 24 +-- .../UserBundle/Security/User/UserCreator.php | 83 ++++++--- .../UserBundle/Security/User/UserMapper.php | 88 +++++++++ .../Translations/en_US/messages.ini | 18 +- .../Translations/en_US/validators.ini | 2 + .../Config/_config_userconfig_widget.html.php | 82 +++++++++ app/config/security.php | 19 +- composer.json | 3 +- composer.lock | 53 +++++- 26 files changed, 1009 insertions(+), 257 deletions(-) create mode 100644 app/Resources/LightSamlSpBundle/views/discovery.html.twig create mode 100644 app/bundles/UserBundle/DependencyInjection/Compiler/SamlPass.php create mode 100644 app/bundles/UserBundle/EventListener/RouteSubscriber.php create mode 100644 app/bundles/UserBundle/Security/User/UserMapper.php create mode 100644 app/bundles/UserBundle/Views/FormTheme/Config/_config_userconfig_widget.html.php diff --git a/app/Resources/LightSamlSpBundle/views/discovery.html.twig b/app/Resources/LightSamlSpBundle/views/discovery.html.twig new file mode 100644 index 00000000000..fb0b3f2b018 --- /dev/null +++ b/app/Resources/LightSamlSpBundle/views/discovery.html.twig @@ -0,0 +1,16 @@ + + + + + + + + +
+
+

SAML not configured or configured incorrectly.

+
+
+ + diff --git a/app/bundles/ConfigBundle/Config/config.php b/app/bundles/ConfigBundle/Config/config.php index fb6b215adc1..61638689338 100644 --- a/app/bundles/ConfigBundle/Config/config.php +++ b/app/bundles/ConfigBundle/Config/config.php @@ -13,7 +13,7 @@ 'routes' => [ 'main' => [ 'mautic_config_action' => [ - 'path' => '/config/{objectAction}', + 'path' => '/config/{objectAction}/{objectId}', 'controller' => 'MauticConfigBundle:Config:execute', ], 'mautic_sysinfo_index' => [ @@ -56,7 +56,7 @@ 'forms' => [ 'mautic.form.type.config' => [ 'class' => 'Mautic\ConfigBundle\Form\Type\ConfigType', - 'arguments' => 'mautic.factory', + 'arguments' => 'translator', 'alias' => 'config', ], ], diff --git a/app/bundles/ConfigBundle/Controller/ConfigController.php b/app/bundles/ConfigBundle/Controller/ConfigController.php index 06797da9329..5c90a6add31 100644 --- a/app/bundles/ConfigBundle/Controller/ConfigController.php +++ b/app/bundles/ConfigBundle/Controller/ConfigController.php @@ -16,7 +16,9 @@ use Mautic\ConfigBundle\Event\ConfigEvent; use Mautic\CoreBundle\Controller\FormController; use Mautic\CoreBundle\Helper\EncryptionHelper; +use Symfony\Component\Form\Form; use Symfony\Component\Form\FormError; +use Symfony\Component\HttpFoundation\Response; /** * Class ConfigController. @@ -35,9 +37,11 @@ public function editAction() return $this->accessDenied(); } - $event = new ConfigBuilderEvent($this->factory); + $event = new ConfigBuilderEvent($this->get('mautic.helper.paths'), $this->get('mautic.helper.bundle')); $dispatcher = $this->get('event_dispatcher'); $dispatcher->dispatch(ConfigEvents::CONFIG_ON_GENERATE, $event); + // Extract and base64 encode file contents + $fileFields = $event->getFileFields(); $formConfigs = $event->getForms(); $formThemes = $event->getFormThemes(); $doNotChange = $this->coreParametersHelper->getParameter('security.restrictedConfigFields'); @@ -50,11 +54,16 @@ public function editAction() // Create the form $action = $this->generateUrl('mautic_config_action', ['objectAction' => 'edit']); - $form = $model->createForm($formConfigs, $this->get('form.factory'), [ - 'action' => $action, - 'doNotChange' => $doNotChange, - 'doNotChangeDisplayMode' => $doNotChangeDisplayMode, - ]); + $form = $model->createForm( + $formConfigs, + $this->get('form.factory'), + [ + 'action' => $action, + 'doNotChange' => $doNotChange, + 'doNotChangeDisplayMode' => $doNotChangeDisplayMode, + 'fileFields' => $fileFields, + ] + ); /** @var \Mautic\CoreBundle\Configurator\Configurator $configurator */ $configurator = $this->get('mautic.configurator'); @@ -75,47 +84,66 @@ public function editAction() $dispatcher->dispatch(ConfigEvents::CONFIG_PRE_SAVE, $configEvent); $formValues = $configEvent->getConfig(); - foreach ($configEvent->getErrors() as $message => $messageVars) { - $this->addFlash($message, $messageVars); - } + $errors = $configEvent->getErrors(); + $fieldErrors = $configEvent->getFieldErrors(); - // Prevent these from getting overwritten with empty values - $unsetIfEmpty = $configEvent->getPreservedFields(); + if ($errors || $fieldErrors) { + foreach ($errors as $message => $messageVars) { + $form->addError( + new FormError($this->translator->trans($message, $messageVars, 'validators')) + ); + } - // Merge each bundle's updated configuration into the local configuration - foreach ($formValues as $object) { - $checkThese = array_intersect(array_keys($object), $unsetIfEmpty); - foreach ($checkThese as $checkMe) { - if (empty($object[$checkMe])) { - unset($object[$checkMe]); + foreach ($fieldErrors as $key => $fields) { + foreach ($fields as $field => $fieldError) { + $form[$key][$field]->addError( + new FormError($this->translator->trans($fieldError[0], $fieldError[1], 'validators')) + ); } } + $isValid = false; + } else { + // Prevent these from getting overwritten with empty values + $unsetIfEmpty = $configEvent->getPreservedFields(); + $unsetIfEmpty = array_merge($unsetIfEmpty, $fileFields); + + // Merge each bundle's updated configuration into the local configuration + foreach ($formValues as $key => $object) { + $checkThese = array_intersect(array_keys($object), $unsetIfEmpty); + foreach ($checkThese as $checkMe) { + if (empty($object[$checkMe])) { + unset($object[$checkMe]); + } + } - $configurator->mergeParameters($object); - } - - try { - // Ensure the config has a secret key - $params = $configurator->getParameters(); - if (empty($params['secret_key'])) { - $configurator->mergeParameters(['secret_key' => EncryptionHelper::generateKey()]); + $configurator->mergeParameters($object); } - $configurator->write(); + try { + // Ensure the config has a secret key + $params = $configurator->getParameters(); + if (empty($params['secret_key'])) { + $configurator->mergeParameters(['secret_key' => EncryptionHelper::generateKey()]); + } + + $configurator->write(); - $this->addFlash('mautic.config.config.notice.updated'); + $this->addFlash('mautic.config.config.notice.updated'); - // We must clear the application cache for the updated values to take effect - /** @var \Mautic\CoreBundle\Helper\CacheHelper $cacheHelper */ - $cacheHelper = $this->factory->getHelper('cache'); - $cacheHelper->clearContainerFile(); - } catch (\RuntimeException $exception) { - $this->addFlash('mautic.config.config.error.not.updated', ['%exception%' => $exception->getMessage()], 'error'); + // We must clear the application cache for the updated values to take effect + /** @var \Mautic\CoreBundle\Helper\CacheHelper $cacheHelper */ + $cacheHelper = $this->get('mautic.helper.cache'); + $cacheHelper->clearContainerFile(); + } catch (\RuntimeException $exception) { + $this->addFlash('mautic.config.config.error.not.updated', ['%exception%' => $exception->getMessage()], 'error'); + } } } elseif (!$isWritabale) { - $form->addError(new FormError( - $this->translator->trans('mautic.config.notwritable') - )); + $form->addError( + new FormError( + $this->translator->trans('mautic.config.notwritable') + ) + ); } } @@ -131,21 +159,64 @@ public function editAction() $tmpl = $this->request->isXmlHttpRequest() ? $this->request->get('tmpl', 'index') : 'index'; - return $this->delegateView([ - 'viewParameters' => [ - 'tmpl' => $tmpl, - 'security' => $this->get('mautic.security'), - 'form' => $this->setFormTheme($form, 'MauticConfigBundle:Config:form.html.php', $formThemes), - 'formConfigs' => $formConfigs, - 'isWritable' => $isWritabale, - ], - 'contentTemplate' => 'MauticConfigBundle:Config:form.html.php', - 'passthroughVars' => [ - 'activeLink' => '#mautic_config_index', - 'mauticContent' => 'config', - 'route' => $this->generateUrl('mautic_config_action', ['objectAction' => 'edit']), - ], - ]); + return $this->delegateView( + [ + 'viewParameters' => [ + 'tmpl' => $tmpl, + 'security' => $this->get('mautic.security'), + 'form' => $this->setFormTheme($form, 'MauticConfigBundle:Config:form.html.php', $formThemes), + 'formConfigs' => $formConfigs, + 'isWritable' => $isWritabale, + ], + 'contentTemplate' => 'MauticConfigBundle:Config:form.html.php', + 'passthroughVars' => [ + 'activeLink' => '#mautic_config_index', + 'mauticContent' => 'config', + 'route' => $this->generateUrl('mautic_config_action', ['objectAction' => 'edit']), + ], + ] + ); + } + + /** + * @param $objectId + * + * @return array|\Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response + */ + public function downloadAction($objectId) + { + //admin only allowed + if (!$this->user->isAdmin()) { + return $this->accessDenied(); + } + + $event = new ConfigBuilderEvent($this->get('mautic.helper.paths'), $this->get('mautic.helper.bundle')); + $dispatcher = $this->get('event_dispatcher'); + $dispatcher->dispatch(ConfigEvents::CONFIG_ON_GENERATE, $event); + + // Extract and base64 encode file contents + $fileFields = $event->getFileFields(); + + if (!in_array($objectId, $fileFields)) { + return $this->accessDenied(); + } + + $content = $this->get('mautic.helper.core_parameters')->getParameter($objectId); + $filename = $this->request->get('filename', $objectId); + + if ($decoded = base64_decode($content)) { + $response = new Response($decoded); + $response->headers->set('Content-Type', 'application/force-download'); + $response->headers->set('Content-Type', 'application/octet-stream'); + $response->headers->set('Content-Disposition', 'attachment; filename="'.$filename); + $response->headers->set('Expires', 0); + $response->headers->set('Cache-Control', 'must-revalidate'); + $response->headers->set('Pragma', 'public'); + + return $response; + } + + return $this->notFound(); } /** diff --git a/app/bundles/ConfigBundle/Event/ConfigBuilderEvent.php b/app/bundles/ConfigBundle/Event/ConfigBuilderEvent.php index 16191c13453..733a64f64e9 100644 --- a/app/bundles/ConfigBundle/Event/ConfigBuilderEvent.php +++ b/app/bundles/ConfigBundle/Event/ConfigBuilderEvent.php @@ -11,7 +11,8 @@ namespace Mautic\ConfigBundle\Event; -use Mautic\CoreBundle\Factory\MauticFactory; +use Mautic\CoreBundle\Helper\BundleHelper; +use Mautic\CoreBundle\Helper\PathsHelper; use Symfony\Component\EventDispatcher\Event; /** @@ -30,22 +31,38 @@ class ConfigBuilderEvent extends Event private $formThemes = []; /** - * @var MauticFactory + * @var PathsHelper */ - private $factory; + private $pathsHelper; /** - * @param MauticFactory $factory + * @var BundleHelper */ - public function __construct(MauticFactory $factory) + private $bundleHelper; + + /** + * @var array + */ + protected $encodedFields = []; + + /** + * ConfigBuilderEvent constructor. + * + * @param PathsHelper $pathsHelper + * @param BundleHelper $bundleHelper + */ + public function __construct(PathsHelper $pathsHelper, BundleHelper $bundleHelper) { - $this->factory = $factory; + $this->pathsHelper = $pathsHelper; + $this->bundleHelper = $bundleHelper; } /** * Set new form to the forms array. * - * @param array $form + * @param $form + * + * @return $this */ public function addForm($form) { @@ -54,6 +71,8 @@ public function addForm($form) } $this->forms[$form['formAlias']] = $form; + + return $this; } /** @@ -76,16 +95,6 @@ public function getFormThemes() return $this->formThemes; } - /** - * Returns the factory. - * - * @return MauticFactory - */ - public function getFactory() - { - return $this->factory; - } - /** * Helper method can load $parameters array from a config file. * @@ -95,7 +104,7 @@ public function getFactory() */ public function getParameters($path = null) { - $paramsFile = $this->factory->getSystemPath('app').$path; + $paramsFile = $this->pathsHelper->getSystemPath('app').$path; if (file_exists($paramsFile)) { // Import the bundle configuration, $parameters is defined in this file @@ -106,6 +115,14 @@ public function getParameters($path = null) $parameters = []; } + $fields = $this->getBase64EncodedFields(); + $checkThese = array_intersect(array_keys($parameters), $fields); + foreach ($checkThese as $checkMe) { + if (!empty($parameters[$checkMe])) { + $parameters[$checkMe] = base64_decode($parameters[$checkMe]); + } + } + return $parameters; } @@ -119,7 +136,7 @@ public function getParametersFromConfig($bundle) static $allBundles; if (empty($allBundles)) { - $allBundles = $this->factory->getMauticBundles(true); + $allBundles = $this->bundleHelper->getMauticBundles(true); } if (isset($allBundles[$bundle]) && $allBundles[$bundle]['config']['parameters']) { @@ -128,4 +145,24 @@ public function getParametersFromConfig($bundle) return []; } } + + /** + * @param $fields + * + * @return $this + */ + public function addFileFields($fields) + { + $this->encodedFields = array_merge($this->encodedFields, (array) $fields); + + return $this; + } + + /** + * @return array + */ + public function getFileFields() + { + return $this->encodedFields; + } } diff --git a/app/bundles/ConfigBundle/Event/ConfigEvent.php b/app/bundles/ConfigBundle/Event/ConfigEvent.php index 970793899f2..215b13331c1 100644 --- a/app/bundles/ConfigBundle/Event/ConfigEvent.php +++ b/app/bundles/ConfigBundle/Event/ConfigEvent.php @@ -12,6 +12,7 @@ namespace Mautic\ConfigBundle\Event; use Mautic\CoreBundle\Event\CommonEvent; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\ParameterBag; /** @@ -39,6 +40,11 @@ class ConfigEvent extends CommonEvent */ private $errors = []; + /** + * @var array + */ + private $fieldErrors = []; + /** * @param array $config * @param ParameterBag $post @@ -122,9 +128,24 @@ public function getPreservedFields() * @param string $message (untranslated) * @param array $messageVars for translation */ - public function setError($message, $messageVars = []) + public function setError($message, $messageVars = [], $key = null, $field = null) { + if (!empty($key) && !empty($field)) { + if (!isset($this->errors[$key])) { + $this->fieldErrors[$key] = []; + } + + $this->fieldErrors[$key][$field] = [ + $message, + $messageVars, + ]; + + return $this; + } + $this->errors[$message] = $messageVars; + + return $this; } /** @@ -137,6 +158,14 @@ public function getErrors() return $this->errors; } + /** + * @return array + */ + public function getFieldErrors() + { + return $this->fieldErrors; + } + /** * @param $value * @@ -159,4 +188,28 @@ public function escapeString($value) return $value; } + + /** + * @param UploadedFile $file + * + * @return string + */ + public function getFileContent(UploadedFile $file) + { + $tmpFile = $file->getRealPath(); + $content = trim(file_get_contents($tmpFile)); + @unlink($tmpFile); + + return $content; + } + + /** + * @param $content + * + * @return string + */ + public function encodeFileContents($content) + { + return base64_encode($content); + } } diff --git a/app/bundles/ConfigBundle/Form/Type/ConfigType.php b/app/bundles/ConfigBundle/Form/Type/ConfigType.php index f64657517d4..978f4e54a85 100644 --- a/app/bundles/ConfigBundle/Form/Type/ConfigType.php +++ b/app/bundles/ConfigBundle/Form/Type/ConfigType.php @@ -11,12 +11,13 @@ namespace Mautic\ConfigBundle\Form\Type; -use Mautic\CoreBundle\Factory\MauticFactory; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; +use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\Translation\TranslatorInterface; /** * Class ConfigType. @@ -29,11 +30,13 @@ class ConfigType extends AbstractType private $translator; /** - * @param MauticFactory $factory + * ConfigType constructor. + * + * @param TranslatorInterface $translator */ - public function __construct(MauticFactory $factory) + public function __construct(TranslatorInterface $translator) { - $this->translator = $factory->getTranslator(); + $this->translator = $translator; } /** @@ -43,42 +46,60 @@ public function buildForm(FormBuilderInterface $builder, array $options) { foreach ($options['data'] as $config) { if (isset($config['formAlias']) && isset($config['parameters'])) { - $builder->add($config['formAlias'], $config['formAlias'], [ - 'data' => $config['parameters'], - ]); + $checkThese = array_intersect(array_keys($config['parameters']), $options['fileFields']); + foreach ($checkThese as $checkMe) { + // Unset base64 encoded values + unset($config['parameters'][$checkMe]); + } + $builder->add( + $config['formAlias'], + $config['formAlias'], + [ + 'data' => $config['parameters'], + ] + ); } } $translator = $this->translator; - $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options, $translator) { - $form = $event->getForm(); + $builder->addEventListener( + FormEvents::PRE_SET_DATA, + function (FormEvent $event) use ($options, $translator) { + $form = $event->getForm(); - foreach ($form as $config => $configForm) { - foreach ($configForm as $key => $child) { - if (in_array($key, $options['doNotChange'])) { - if ($options['doNotChangeDisplayMode'] == 'mask') { - $fieldOptions = $child->getConfig()->getOptions(); + foreach ($form as $config => $configForm) { + foreach ($configForm as $key => $child) { + if (in_array($key, $options['doNotChange'])) { + if ($options['doNotChangeDisplayMode'] == 'mask') { + $fieldOptions = $child->getConfig()->getOptions(); - $configForm->add($key, 'text', [ - 'label' => $fieldOptions['label'], - 'required' => false, - 'mapped' => false, - 'disabled' => true, - 'attr' => [ - 'placeholder' => $translator->trans('mautic.config.restricted'), - 'class' => 'form-control', - ], - 'label_attr' => ['class' => 'control-label'], - ]); - } elseif ($options['doNotChangeDisplayMode'] == 'remove') { - $configForm->remove($key); + $configForm->add( + $key, + 'text', + [ + 'label' => $fieldOptions['label'], + 'required' => false, + 'mapped' => false, + 'disabled' => true, + 'attr' => [ + 'placeholder' => $translator->trans('mautic.config.restricted'), + 'class' => 'form-control', + ], + 'label_attr' => ['class' => 'control-label'], + ] + ); + } elseif ($options['doNotChangeDisplayMode'] == 'remove') { + $configForm->remove($key); + } } } } } - }); + ); - $builder->add('buttons', 'form_buttons', + $builder->add( + 'buttons', + 'form_buttons', [ 'apply_onclick' => 'Mautic.activateBackdrop()', 'save_onclick' => 'Mautic.activateBackdrop()', @@ -103,11 +124,19 @@ public function getName() * * @param OptionsResolverInterface $resolver */ - public function setDefaultOptions(OptionsResolverInterface $resolver) + public function configureOptions(OptionsResolver $resolver) { - $resolver->setRequired([ - 'doNotChange', - 'doNotChangeDisplayMode', - ]); + $resolver->setRequired( + [ + 'doNotChange', + 'doNotChangeDisplayMode', + ] + ); + + $resolver->setDefaults( + [ + 'fileFields' => [], + ] + ); } } diff --git a/app/bundles/ConfigBundle/Views/Config/form.html.php b/app/bundles/ConfigBundle/Views/Config/form.html.php index 2467bb6ba59..0b4e6eb3627 100644 --- a/app/bundles/ConfigBundle/Views/Config/form.html.php +++ b/app/bundles/ConfigBundle/Views/Config/form.html.php @@ -62,7 +62,7 @@ ?>
- widget($form[$key]); ?> + widget($form[$key], ['formConfig' => $formConfigs[$key]]); ?>
diff --git a/app/bundles/CoreBundle/Translations/en_US/validators.ini b/app/bundles/CoreBundle/Translations/en_US/validators.ini index e15abdebdd2..d911c1f935d 100644 --- a/app/bundles/CoreBundle/Translations/en_US/validators.ini +++ b/app/bundles/CoreBundle/Translations/en_US/validators.ini @@ -10,4 +10,5 @@ mautic.form.lists.notblank="List values cannot be blank." mautic.core.theme.missing.config="The theme you tried to install doesn't have the config.json file in the root folder. The theme could not be installed." mautic.core.theme.default.cannot.overwrite="%name% is the default theme and therefore cannot be overwritten." mautic.core.valid_url_required="A valid URL is required." -mautic.core.theme.upload.empty="The file was not selected. Select a ZIP file to upload." \ No newline at end of file +mautic.core.theme.upload.empty="The file was not selected. Select a ZIP file to upload." +mautic.core.invalid_file_type="Invalid file type {{ type }}. Use a file that matches of of the following mime types: {{ types }}." \ No newline at end of file diff --git a/app/bundles/LeadBundle/Form/Type/LeadImportType.php b/app/bundles/LeadBundle/Form/Type/LeadImportType.php index 5cdfc44aeb3..23ee276ca7d 100644 --- a/app/bundles/LeadBundle/Form/Type/LeadImportType.php +++ b/app/bundles/LeadBundle/Form/Type/LeadImportType.php @@ -13,6 +13,7 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Validator\Constraints\File; /** * Class LeadImportType. @@ -25,13 +26,26 @@ class LeadImportType extends AbstractType */ public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->add('file', 'file', [ - 'label' => 'mautic.lead.import.file', - 'attr' => [ - 'accept' => '.csv', - 'class' => 'form-control', - ], - ]); + $builder->add( + 'file', + 'file', + [ + 'label' => 'mautic.lead.import.file', + 'attr' => [ + 'accept' => '.csv', + 'class' => 'form-control', + ], + 'constraints' => [ + new File( + [ + 'mimeTypes' => ['text/csv', 'text/plain'], + 'mimeTypesMessage' => 'mautic.core.invalid_file_type', + ] + ), + ], + 'error_bubbling' => true, + ] + ); $constraints = [ new \Symfony\Component\Validator\Constraints\NotBlank( @@ -40,54 +54,74 @@ public function buildForm(FormBuilderInterface $builder, array $options) ]; $default = (empty($options['data']['delimiter'])) ? ',' : htmlspecialchars($options['data']['delimiter']); - $builder->add('delimiter', 'text', [ - 'label' => 'mautic.lead.import.delimiter', - 'attr' => [ - 'class' => 'form-control', - ], - 'data' => $default, - 'constraints' => $constraints, - ]); + $builder->add( + 'delimiter', + 'text', + [ + 'label' => 'mautic.lead.import.delimiter', + 'attr' => [ + 'class' => 'form-control', + ], + 'data' => $default, + 'constraints' => $constraints, + ] + ); $default = (empty($options['data']['enclosure'])) ? '"' : htmlspecialchars($options['data']['enclosure']); - $builder->add('enclosure', 'text', [ - 'label' => 'mautic.lead.import.enclosure', - 'attr' => [ - 'class' => 'form-control', - ], - 'data' => $default, - 'constraints' => $constraints, - ]); + $builder->add( + 'enclosure', + 'text', + [ + 'label' => 'mautic.lead.import.enclosure', + 'attr' => [ + 'class' => 'form-control', + ], + 'data' => $default, + 'constraints' => $constraints, + ] + ); $default = (empty($options['data']['escape'])) ? '\\' : $options['data']['escape']; - $builder->add('escape', 'text', [ - 'label' => 'mautic.lead.import.escape', - 'attr' => [ - 'class' => 'form-control', - ], - 'data' => $default, - 'constraints' => $constraints, - ]); + $builder->add( + 'escape', + 'text', + [ + 'label' => 'mautic.lead.import.escape', + 'attr' => [ + 'class' => 'form-control', + ], + 'data' => $default, + 'constraints' => $constraints, + ] + ); $default = (empty($options['data']['batchlimit'])) ? 100 : (int) $options['data']['batchlimit']; - $builder->add('batchlimit', 'text', [ - 'label' => 'mautic.lead.import.batchlimit', - 'attr' => [ - 'class' => 'form-control', - 'tooltip' => 'mautic.lead.import.batchlimit_tooltip', - ], - 'data' => $default, - 'constraints' => $constraints, - ]); + $builder->add( + 'batchlimit', + 'text', + [ + 'label' => 'mautic.lead.import.batchlimit', + 'attr' => [ + 'class' => 'form-control', + 'tooltip' => 'mautic.lead.import.batchlimit_tooltip', + ], + 'data' => $default, + 'constraints' => $constraints, + ] + ); - $builder->add('start', 'submit', [ - 'attr' => [ - 'class' => 'btn btn-primary', - 'icon' => 'fa fa-upload', - 'onclick' => "mQuery(this).prop('disabled', true); mQuery('form[name=\'lead_import\']').submit();", - ], - 'label' => 'mautic.lead.import.upload', - ]); + $builder->add( + 'start', + 'submit', + [ + 'attr' => [ + 'class' => 'btn btn-primary', + 'icon' => 'fa fa-upload', + 'onclick' => "mQuery(this).prop('disabled', true); mQuery('form[name=\'lead_import\']').submit();", + ], + 'label' => 'mautic.lead.import.upload', + ] + ); if (!empty($options['action'])) { $builder->setAction($options['action']); diff --git a/app/bundles/LeadBundle/Translations/en_US/validators.ini b/app/bundles/LeadBundle/Translations/en_US/validators.ini index 7236801e837..2eb4a538828 100644 --- a/app/bundles/LeadBundle/Translations/en_US/validators.ini +++ b/app/bundles/LeadBundle/Translations/en_US/validators.ini @@ -20,4 +20,4 @@ mautic.lead.submitaction.leadfield.notblank="Choose a contact field." mautic.lead.time.invalid="Time is not valid. Must be in HH:ii format where 00 <= HH <= 23 and 00 <= ii <= 59 e.g. 11:00." mautic.lead.date.invalid="Date is not valid. Must be in Y-m-d format e.g. 1986-07-25." mautic.lead.datetime.invalid="Invalid datetime format. Valid format is Y-m-d H:i e.g. 1986-07-25 11:00." -mautic.company.choosecompany.notblank="Choose a company" +mautic.company.choosecompany.notblank="Choose a company" \ No newline at end of file diff --git a/app/bundles/UserBundle/Config/config.php b/app/bundles/UserBundle/Config/config.php index c6fde663866..6a8157783d2 100644 --- a/app/bundles/UserBundle/Config/config.php +++ b/app/bundles/UserBundle/Config/config.php @@ -115,17 +115,10 @@ 'path' => '/saml/metadata.xml', 'controller' => 'LightSamlSpBundle:Default:metadata', ], - 'lightsaml_sp.discovery' => [ 'path' => '/saml/discovery', 'controller' => 'LightSamlSpBundle:Default:discovery', ], - - 'lightsaml_sp.sessions' => [ - 'path' => '/saml/sessions', - 'controller' => 'LightSamlSpBundle:Default:sessions', - ], - ], ], @@ -148,6 +141,9 @@ 'mautic.user.config.subscriber' => [ 'class' => 'Mautic\UserBundle\EventListener\ConfigSubscriber', ], + 'mautic.user.route.subscriber' => [ + 'class' => 'Mautic\UserBundle\EventListener\RouteSubscriber', + ], ], 'forms' => [ 'mautic.form.type.user' => [ @@ -191,9 +187,9 @@ 'arguments' => 'mautic.user.model.role', 'alias' => 'role_list', ], - 'mautic.form.type.saml_config' => [ + 'mautic.form.type.userconfig' => [ 'class' => 'Mautic\UserBundle\Form\Type\ConfigType', - 'alias' => 'saml_config', + 'alias' => 'userconfig', ], ], 'other' => [ @@ -287,6 +283,9 @@ 'arguments' => [ 'doctrine.orm.entity_manager', 'lightsaml_sp.username_mapper.simple', + 'mautic.user.model.user', + 'security.encoder_factory', + '%mautic.saml_idp_default_role%', ], ], ], @@ -303,8 +302,14 @@ ], ], 'parameters' => [ - 'saml_enabled' => false, - 'idp_entity_id' => '', - 'idp_ceritificate' => '', + 'saml_idp_metadata' => '', + 'saml_idp_certificate' => '', + 'saml_idp_private_key' => '', + 'saml_idp_key_password' => '', + 'saml_idp_email_attribute' => '', + 'saml_idp_username_attribute' => '', + 'saml_idp_firstname_attribute' => '', + 'saml_idp_lastname_attribute' => '', + 'saml_idp_default_role' => '', ], ]; diff --git a/app/bundles/UserBundle/DependencyInjection/Compiler/SamlPass.php b/app/bundles/UserBundle/DependencyInjection/Compiler/SamlPass.php new file mode 100644 index 00000000000..1dbc8fe28c5 --- /dev/null +++ b/app/bundles/UserBundle/DependencyInjection/Compiler/SamlPass.php @@ -0,0 +1,71 @@ +getParameter('mautic.saml_idp_metadata')) { + $certificateContent = $container->getParameter('mautic.saml_idp_certificate'); + + if ($certificateContent) { + $certificateContent = base64_decode($certificateContent); + + $certDefId = 'mautic.security.saml.own.credential_cert'; + $certificateDefinition = (new Definition(X509Certificate::class)) + ->addMethodCall('loadPem', [$certificateContent]); + $container->setDefinition($certDefId, $certificateDefinition); + + $credId = 'mautic.security.saml.own.credentials'; + $credentialsDefinition = new Definition( + X509Credential::class, + [ + new Reference($certDefId), + ] + ); + $container->setDefinition($credId, $credentialsDefinition); + + $credentialStore = (new Definition(StaticCredentialStore::class)) + ->addMethodCall('add', [new Reference($credId)]) + ->addTag('lightsaml.own_credential_store'); + $container->setDefinition('mautic.security.saml.own.credential_store', $credentialStore); + } + + // Create the entity descriptor + $id = 'mautic.security.saml.idp_entity_descriptor'; + $xml = base64_decode($xml); + $entityDescriptorDefinition = (new Definition(EntityDescriptor::class)) + ->setFactory(EntityDescriptor::class.'::loadXml') + ->addArgument($xml); + $container->setDefinition($id, $entityDescriptorDefinition); + + // Create the entity descriptor store + $definition = new Definition('LightSaml\Store\EntityDescriptor\FixedEntityDescriptorStore'); + $definition->addTag('lightsaml.idp_entity_store') + ->addMethodCall('add', [new Reference($id)]); + $container->setDefinition('mautic.security.saml.idp_entity_descriptor_store.xml', $definition); + + $container->getDefinition('lightsaml_sp.username_mapper.simple') + ->setClass('Mautic\UserBundle\Security\User\UserMapper'); + } + } +} diff --git a/app/bundles/UserBundle/EventListener/ConfigSubscriber.php b/app/bundles/UserBundle/EventListener/ConfigSubscriber.php index 7c85c48eab7..321e21fdf60 100644 --- a/app/bundles/UserBundle/EventListener/ConfigSubscriber.php +++ b/app/bundles/UserBundle/EventListener/ConfigSubscriber.php @@ -1,7 +1,8 @@ array('onConfigGenerate', 0) - ); + return [ + ConfigEvents::CONFIG_ON_GENERATE => ['onConfigGenerate', 0], + ConfigEvents::CONFIG_PRE_SAVE => ['onConfigSave', 0], + ]; } - public function onConfigGenerate (ConfigBuilderEvent $event) + /** + * @param ConfigBuilderEvent $event + */ + public function onConfigGenerate(ConfigBuilderEvent $event) { - $event->addForm(array( - 'bundle' => 'UserBundle', - 'formAlias' => 'saml_config', - 'formTheme' => 'MauticApiBundle:FormTheme\Config', - 'parameters' => $event->getParametersFromConfig('MauticUserBundle') - )); + $event->addFileFields($this->fileFields) + ->addForm( + [ + 'bundle' => 'UserBundle', + 'formAlias' => 'userconfig', + 'formTheme' => 'MauticUserBundle:FormTheme\Config', + 'parameters' => $event->getParametersFromConfig('MauticUserBundle'), + ] + ); + } + + /** + * @param ConfigEvent $event + */ + public function onConfigSave(ConfigEvent $event) + { + $data = $event->getConfig('userconfig'); + + foreach ($this->fileFields as $field) { + if (isset($data[$field]) && $data[$field] instanceof UploadedFile) { + $data[$field] = $event->getFileContent($data[$field]); + + switch ($field) { + case 'saml_idp_metadata': + if (strpos($data[$field], 'setError('mautic.user.saml.metadata.invalid', [], 'userconfig', 'saml_idp_metadata'); + } + break; + case 'saml_idp_certificate': + if (strpos($data[$field], '-----BEGIN CERTIFICATE-----') !== 0) { + $event->setError('mautic.user.saml.certificate.invalid', [], 'userconfig', 'saml_idp_certificate'); + } + break; + } + + $data[$field] = $event->encodeFileContents($data[$field]); + } + } + + $event->setConfig($data, 'userconfig'); } } diff --git a/app/bundles/UserBundle/EventListener/RouteSubscriber.php b/app/bundles/UserBundle/EventListener/RouteSubscriber.php new file mode 100644 index 00000000000..66857a15f13 --- /dev/null +++ b/app/bundles/UserBundle/EventListener/RouteSubscriber.php @@ -0,0 +1,39 @@ + ['onKernelRequest', 0], + ]; + } + + public function onKernelRequest(GetResponseEvent $event) + { + if ($event->isMasterRequest()) { + $request = $event->getRequest(); + + $route = $request->attributes->get('_route'); + if (strpos($route, 'lightsaml') !== false && empty($this->params['saml_idp_metadata'])) { + throw new NotFoundHttpException(); + } + } + } +} diff --git a/app/bundles/UserBundle/Form/Type/ConfigType.php b/app/bundles/UserBundle/Form/Type/ConfigType.php index 7bda9bcc633..d3ba1eaa7c6 100644 --- a/app/bundles/UserBundle/Form/Type/ConfigType.php +++ b/app/bundles/UserBundle/Form/Type/ConfigType.php @@ -1,6 +1,7 @@ add( - 'saml_enabled', - 'yesno_button_group', + 'saml_idp_metadata', + FileType::class, [ - 'label' => 'mautic.user.config.form.saml.enabled', - 'attr' => [ - 'tooltip' => 'mautic.user.config.form.saml.enabled.tooltip', + 'label' => 'mautic.user.config.form.saml.idp.metadata', + 'label_attr' => ['class' => 'control-label'], + 'attr' => [ + 'class' => 'form-control', + 'tooltip' => 'mautic.user.config.form.saml.idp.metadata.tooltip', + 'rows' => 10, + ], + 'required' => false, + 'constraints' => [ + new File( + [ + 'mimeTypes' => ['text/plain', 'text/xml', 'application/xml'], + 'mimeTypesMessage' => 'mautic.core.invalid_file_type', + ] + ), ], ] ); $builder->add( - 'idp_entity_id', - 'text', + 'saml_idp_certificate', + FileType::class, [ - 'label' => 'mautic.user.config.form.saml.idp.entity_id', + 'label' => 'mautic.user.config.form.saml.idp.certificate', 'label_attr' => ['class' => 'control-label'], 'attr' => [ 'class' => 'form-control', - 'tooltip' => 'mautic.user.config.form.saml.idp.entity_id.tooltip', + 'tooltip' => 'mautic.user.config.form.saml.idp.certificate.tooltip', + ], + 'required' => false, + 'constraints' => [ + new File( + [ + 'mimeTypes' => ['text/plain'], + 'mimeTypesMessage' => 'mautic.core.invalid_file_type', + ] + ), ], ] ); $builder->add( - 'idp_ceritificate', - 'textarea', + 'saml_idp_email_attribute', + TextType::class, [ - 'label' => 'mautic.user.config.form.saml.idp.certificate', + 'label' => 'mautic.user.config.form.saml.idp.attribute_email', 'label_attr' => ['class' => 'control-label'], 'attr' => [ - 'class' => 'form-control', - 'tooltip' => 'mautic.user.config.form.saml.idp.certificate.tooltip', - 'rows' => 10, + 'class' => 'form-control', + ], + 'constraints' => [ + new NotBlank( + [ + 'message' => 'mautic.core.value.required', + ] + ), + ], + ] + ); + + $builder->add( + 'saml_idp_username_attribute', + TextType::class, + [ + 'label' => 'mautic.user.config.form.saml.idp.attribute_username', + 'label_attr' => ['class' => 'control-label'], + 'attr' => [ + 'class' => 'form-control', + ], + 'required' => false, + ] + ); + + $builder->add( + 'saml_idp_firstname_attribute', + TextType::class, + [ + 'label' => 'mautic.user.config.form.saml.idp.attribute_firstname', + 'label_attr' => ['class' => 'control-label'], + 'attr' => [ + 'class' => 'form-control', + ], + 'required' => false, + ] + ); + + $builder->add( + 'saml_idp_lastname_attribute', + TextType::class, + [ + 'label' => 'mautic.user.config.form.saml.idp.attribute_lastname', + 'label_attr' => ['class' => 'control-label'], + 'attr' => [ + 'class' => 'form-control', + ], + 'required' => false, + ] + ); + + $builder->add( + 'saml_idp_default_role', + 'role_list', + [ + 'label' => 'mautic.user.config.form.saml.idp.default_role', + 'label_attr' => ['class' => 'control-label'], + 'attr' => [ + 'class' => 'form-control', ], + 'required' => true, ] ); } @@ -68,6 +151,6 @@ public function buildForm(FormBuilderInterface $builder, array $options) */ public function getName() { - return 'saml_config'; + return 'userconfig'; } } diff --git a/app/bundles/UserBundle/MauticUserBundle.php b/app/bundles/UserBundle/MauticUserBundle.php index 8e70fa8fda3..161c43e11bd 100644 --- a/app/bundles/UserBundle/MauticUserBundle.php +++ b/app/bundles/UserBundle/MauticUserBundle.php @@ -11,6 +11,7 @@ namespace Mautic\UserBundle; +use Mautic\UserBundle\DependencyInjection\Compiler\SamlPass; use Mautic\UserBundle\DependencyInjection\Firewall\Factory\PluginFactory; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -29,5 +30,7 @@ public function build(ContainerBuilder $container) $extension = $container->getExtension('security'); $extension->addSecurityListenerFactory(new PluginFactory()); + + $container->addCompilerPass(new SamlPass()); } } diff --git a/app/bundles/UserBundle/Security/Authenticator/FormAuthenticator.php b/app/bundles/UserBundle/Security/Authenticator/FormAuthenticator.php index 26443b86de0..8c1ad9cab22 100644 --- a/app/bundles/UserBundle/Security/Authenticator/FormAuthenticator.php +++ b/app/bundles/UserBundle/Security/Authenticator/FormAuthenticator.php @@ -19,7 +19,6 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\Security\Core\Authentication\SimpleFormAuthenticatorInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; @@ -27,6 +26,7 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authentication\SimpleFormAuthenticatorInterface; class FormAuthenticator implements SimpleFormAuthenticatorInterface { diff --git a/app/bundles/UserBundle/Security/Store/IdStore.php b/app/bundles/UserBundle/Security/Store/IdStore.php index c06a7c219a2..999efdddbd8 100644 --- a/app/bundles/UserBundle/Security/Store/IdStore.php +++ b/app/bundles/UserBundle/Security/Store/IdStore.php @@ -1,24 +1,26 @@ manager = $manager; + $this->manager = $manager; $this->timeProvider = $timeProvider; } @@ -35,18 +37,16 @@ public function __construct(ObjectManager $manager, TimeProviderInterface $timeP * @param string $entityId * @param string $id * @param \DateTime $expiryTime - * - * @return void */ public function set($entityId, $id, \DateTime $expiryTime) { - $idEntry = $this->manager->find(IdEntry::class, ['entityId'=>$entityId, 'id'=>$id]); + $idEntry = $this->manager->find(IdEntry::class, ['entityId' => $entityId, 'id' => $id]); if (null == $idEntry) { $idEntry = new IdEntry(); } $idEntry->setEntityId($entityId) - ->setId($id) - ->setExpiryTime($expiryTime); + ->setId($id) + ->setExpiryTime($expiryTime); $this->manager->persist($idEntry); $this->manager->flush($idEntry); } @@ -60,7 +60,7 @@ public function set($entityId, $id, \DateTime $expiryTime) public function has($entityId, $id) { /** @var IdEntry $idEntry */ - $idEntry = $this->manager->find(IdEntry::class, ['entityId'=>$entityId, 'id'=>$id]); + $idEntry = $this->manager->find(IdEntry::class, ['entityId' => $entityId, 'id' => $id]); if (null == $idEntry) { return false; } @@ -70,4 +70,4 @@ public function has($entityId, $id) return true; } -} \ No newline at end of file +} diff --git a/app/bundles/UserBundle/Security/User/UserCreator.php b/app/bundles/UserBundle/Security/User/UserCreator.php index ad989aa0b4a..9ba6e57914b 100644 --- a/app/bundles/UserBundle/Security/User/UserCreator.php +++ b/app/bundles/UserBundle/Security/User/UserCreator.php @@ -1,6 +1,7 @@ entityManager = $entityManager; - $this->usernameMapper = $usernameMapper; + $this->entityManager = $entityManager; + $this->userMapper = $userMapper; + $this->userModel = $userModel; + $this->encoder = $encoder; + $this->defaultRole = (int) $defaultRole; } /** @@ -42,18 +76,19 @@ public function __construct($entityManager, $usernameMapper) */ public function createUser(Response $response) { - $username = $this->usernameMapper->getUsername($response); - - $user = new User(); - $user->setUsername($username) - ->setFirstName('Saml') - ->setLastName('Saml') - ->setPassword(1234) - ->setEmail('saml@saml.com') - ->setRole($this->entityManager->getReference('MauticUserBundle:Role', 1)); - - $this->entityManager->persist($user); - $this->entityManager->flush(); + $user = $this->userMapper->getUsername($response, true); + $user->setPassword($this->userModel->checkNewPassword($user, $this->encoder->getEncoder($user), EncryptionHelper::generateKey())); + $user->setRole($this->entityManager->getReference('MauticUserBundle:Role', $this->defaultRole)); + + // Validate that the user has all that's required + foreach ($this->requiredFields as $field) { + $getter = 'get'.ucfirst($field); + if (!$user->$getter()) { + throw new \InvalidResponseException('User does not include required fields.'); + } + } + + $this->userModel->saveEntity($user); return $user; } diff --git a/app/bundles/UserBundle/Security/User/UserMapper.php b/app/bundles/UserBundle/Security/User/UserMapper.php new file mode 100644 index 00000000000..3b7f162a31d --- /dev/null +++ b/app/bundles/UserBundle/Security/User/UserMapper.php @@ -0,0 +1,88 @@ +attributes = $attributes; + } + + /** + * @param Response $response + * + * @return string|null + */ + public function getUsername(Response $response, $returnEntity = false) + { + $user = new User(); + + foreach ($response->getAllAssertions() as $assertion) { + $this->getValueFromAssertion($assertion, $user); + } + + return ($returnEntity) ? $user : $user->getUsername(); + } + + /** + * @param Assertion $assertion + * + * @return null|string + */ + private function getValueFromAssertion(Assertion $assertion, User $user) + { + foreach ($this->attributes as $key => $attributeName) { + if (self::NAME_ID == $attributeName) { + // Check for a populated username; default to email if empty + if (!$user->getUsername()) { + if ($email = $user->getEmail()) { + $user->setUsername($email); + } elseif ( + $assertion->getSubject() && + $assertion->getSubject()->getNameID() && + $assertion->getSubject()->getNameID()->getValue() && + $assertion->getSubject()->getNameID()->getFormat() != SamlConstants::NAME_ID_FORMAT_TRANSIENT + ) { + $user->setUsername($assertion->getSubject()->getNameID()->getValue()); + } + } + } else { + foreach ($assertion->getAllAttributeStatements() as $attributeStatement) { + $attribute = $attributeStatement->getFirstAttributeByName($attributeName); + if ($attribute && $attribute->getFirstAttributeValue()) { + $setter = 'set'.ucfirst($key); + $user->$setter($attribute->getFirstAttributeValue()); + } + } + } + } + + return null; + } +} diff --git a/app/bundles/UserBundle/Translations/en_US/messages.ini b/app/bundles/UserBundle/Translations/en_US/messages.ini index c0fb2f29222..08ea4ddfa34 100644 --- a/app/bundles/UserBundle/Translations/en_US/messages.ini +++ b/app/bundles/UserBundle/Translations/en_US/messages.ini @@ -66,9 +66,15 @@ mautic.user.user.searchcommand.position="position" mautic.user.user.searchcommand.role="role" mautic.user.user.searchcommand.username="username" mautic.user.users="Users" -mautic.config.tab.saml_config="SAML Settings" -mautic.user.config.form.saml.enabled="SAML enabled?" -mautic.user.config.form.saml.idp.entity_id="Identity Service Provided (idp) id" -mautic.user.config.form.saml.idp.login_url="IDP login URL" -mautic.user.config.form.saml.idp.logout_url="IDP logout URL" -mautic.user.config.form.saml.idp.certificate="IDP Certificate" \ No newline at end of file +mautic.config.tab.userconfig="User/Authentication Settings" +mautic.user.config.form.saml.idp_attributes="Enter the names of the attributes the configured IDP uses for the following Mautic user fields. Note that email is required." +mautic.user.config.form.saml.idp.attribute_email="Email (required)" +mautic.user.config.form.saml.idp.attribute_username="Username (optional)" +mautic.user.config.form.saml.idp.attribute_firstname="First name (optional)" +mautic.user.config.form.saml.idp.attribute_lastname="Last name (optional)" +mautic.user.config.form.saml.idp.certificate="X.509 certificate" +mautic.user.config.form.saml.idp.certificate.tooltip="Upload X.509 certificate provided by the IDP to secure communication between the it and Mautic." +mautic.user.config.form.saml.idp.default_role="Default role for created users" +mautic.user.config.form.saml.idp.metadata="Identity provider metadata file" +mautic.user.config.form.saml.idp.metadata.tooltip="Upload the Identity Provider Metadata XML file." +mautic.user.config.header.saml="SAML SSO Settings" diff --git a/app/bundles/UserBundle/Translations/en_US/validators.ini b/app/bundles/UserBundle/Translations/en_US/validators.ini index 99b99ef8599..749123d8ccd 100644 --- a/app/bundles/UserBundle/Translations/en_US/validators.ini +++ b/app/bundles/UserBundle/Translations/en_US/validators.ini @@ -10,3 +10,5 @@ mautic.user.user.passwordreset.nouserfound="No user could be identified with the mautic.user.user.role.notblank="A role must be chosen for this user." mautic.user.user.username.notblank="Username is required." mautic.user.user.username.unique="Username is already in use. Please choose another." +mautic.user.saml.certificate.invalid="Certificate is invalid. It should begin with -----BEGIN CERTIFICATE-----." +mautic.user.saml.metadata.invalid="The metadata file seems to be invalid." \ No newline at end of file diff --git a/app/bundles/UserBundle/Views/FormTheme/Config/_config_userconfig_widget.html.php b/app/bundles/UserBundle/Views/FormTheme/Config/_config_userconfig_widget.html.php new file mode 100644 index 00000000000..583046b15a6 --- /dev/null +++ b/app/bundles/UserBundle/Views/FormTheme/Config/_config_userconfig_widget.html.php @@ -0,0 +1,82 @@ +children; +$fieldKeys = array_keys($fields); +$generateDownloadRow = function ($field) use ($formConfig, $fields, $view) { + $hasErrors = count($fields[$field]->vars['errors']); + $feedbackClass = (!empty($hasErrors)) ? ' has-error' : ''; + $hide = (!empty($formConfig['parameters'][$field])) ? '' : ' hide'; + $filename = \Mautic\CoreBundle\Helper\InputHelper::alphanum($view['translator']->trans($fields[$field]->vars['label']), true, '_'); + $downloadUrl = $view['router']->path('mautic_config_action', + [ + 'objectAction' => 'download', + 'objectId' => $field, + 'filename' => $filename, + ] + ); + echo << +
+ {$view['form']->label($fields[$field], $fields[$field]->vars['label'])} + + {$view['translator']->trans('mautic.core.download')} + + {$view['form']->widget($fields[$field])} + {$view['form']->errors($fields[$field])} +
+ + +HTML; +} +?> + + +
+
+

trans('mautic.user.config.header.saml'); ?>

+
+
+
+
+ +
+
+ +
+
+
+
trans('mautic.user.config.form.saml.idp_attributes'); ?>
+
+
+ row($fields['saml_idp_email_attribute']); ?> +
+
+ row($fields['saml_idp_username_attribute']); ?> +
+
+
+
+ row($fields['saml_idp_firstname_attribute']); ?> +
+
+ row($fields['saml_idp_lastname_attribute']); ?> +
+
+
+
+
+ row($fields['saml_idp_default_role']); ?> +
+
+
+
+ \ No newline at end of file diff --git a/app/config/security.php b/app/config/security.php index 4bdcf67ad79..d6183b96276 100644 --- a/app/config/security.php +++ b/app/config/security.php @@ -145,7 +145,6 @@ ] ); -$samlEnabled = ($container->hasParameter('mautic.saml_enabled') ? $container->getParameter('mautic.saml_enabled') : false); $container->loadFromExtension( 'light_saml_symfony_bridge', [ @@ -159,17 +158,25 @@ ], ], ], - 'party' => [ - 'idp' => [ - 'files' => true ? ['%kernel.root_dir%/cache/saml.xml'] : [], - ], - ], 'store' => [ 'id_state' => 'mautic.security.saml.id_store', ], ] ); +$container->loadFromExtension( + 'light_saml_sp', + [ + 'username_mapper' => [ + 'email' => '%mautic.saml_idp_email_attribute%', + 'username' => '%mautic.saml_idp_username_attribute%', + 'firstname' => '%mautic.saml_idp_firstname_attribute%', + 'lastname' => '%mautic.saml_idp_lastname_attribute%', + 'nameId' => \Mautic\UserBundle\Security\User\UserMapper::NAME_ID, + ], + ] +); + $this->import('security_api.php'); // List config keys we do not want the user to change via the config UI diff --git a/composer.json b/composer.json index 25394e8ea7b..c8b9f4f19ee 100644 --- a/composer.json +++ b/composer.json @@ -102,7 +102,8 @@ "php-http/guzzle6-adapter": "^1.1", "sparkpost/sparkpost": "~2.0.3", "doctrine/data-fixtures": "1.2.1", - "lightsaml/sp-bundle": "~1.0.3" + "lightsaml/sp-bundle": "~1.0.3", + "symfony/expression-language": "~2.8" }, "require-dev": { "symfony/web-profiler-bundle": "~2.8", diff --git a/composer.lock b/composer.lock index 8f41c982e4c..166efeaaf82 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "65b42da289ad6599229b48fd86bbbb01", - "content-hash": "bc0a9aaae70cb55ee6643637e950e467", + "hash": "f2d2ff926020c13f104abf453a117495", + "content-hash": "078af63003971d0565a0f1b347c8b66b", "packages": [ { "name": "aws/aws-sdk-php", @@ -4938,6 +4938,55 @@ "homepage": "https://symfony.com", "time": "2016-07-28 16:56:28" }, + { + "name": "symfony/expression-language", + "version": "v2.8.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/expression-language.git", + "reference": "2b667229749832a918e4b1d22e185f4ed98b63ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/2b667229749832a918e4b1d22e185f4ed98b63ac", + "reference": "2b667229749832a918e4b1d22e185f4ed98b63ac", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\ExpressionLanguage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony ExpressionLanguage Component", + "homepage": "https://symfony.com", + "time": "2016-11-03 07:52:58" + }, { "name": "symfony/filesystem", "version": "v2.8.11",