Commit 85a4ebe7 authored by Christian BUFFIN's avatar Christian BUFFIN
Browse files

Corrections et améliorations du paramétrage des sous-traitants (plus travail...

Corrections et améliorations du paramétrage des sous-traitants (plus travail préparatoire), issue 361.
parent 44d110a5
...@@ -210,7 +210,7 @@ CREATE UNIQUE INDEX typages_organisations_typage_id_organisation_id_idx ON typag ...@@ -210,7 +210,7 @@ CREATE UNIQUE INDEX typages_organisations_typage_id_organisation_id_idx ON typag
INSERT INTO typages_organisations (typage_id, organisation_id) INSERT INTO typages_organisations (typage_id, organisation_id)
SELECT typages.id, organisations.id SELECT typages.id, organisations.id
FROM typages FROM typages
LEFT OUTER JOIN organisations ON (1 = 1); INNER JOIN organisations ON (1 = 1);
ALTER TABLE fichiers ADD COLUMN typage_id INTEGER DEFAULT NULL REFERENCES typages(id) ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE fichiers ADD COLUMN typage_id INTEGER DEFAULT NULL REFERENCES typages(id) ON DELETE CASCADE ON UPDATE CASCADE;
......
...@@ -22,6 +22,8 @@ ...@@ -22,6 +22,8 @@
*/ */
App::uses('EtatFiche', 'Model'); App::uses('EtatFiche', 'Model');
App::uses('Inflector', 'Utility');
App::uses('LinkedOrganisationInterface', 'Model/Interface');
App::uses('ListeDroit', 'Model'); App::uses('ListeDroit', 'Model');
class DroitsComponent extends Component { class DroitsComponent extends Component {
...@@ -91,6 +93,10 @@ class DroitsComponent extends Component { ...@@ -91,6 +93,10 @@ class DroitsComponent extends Component {
return false; return false;
} }
public function isLogged() {
return empty($this->Session->read('Auth.User.id')) === false;
}
/** /**
* Vérification si l'user est DPO de son organisation * Vérification si l'user est DPO de son organisation
* *
...@@ -303,7 +309,9 @@ class DroitsComponent extends Component { ...@@ -303,7 +309,9 @@ class DroitsComponent extends Component {
* *
* @param int $id * @param int $id
* @return boolean * @return boolean
* *
* @deprecated
*
* @access public * @access public
* @created 29/04/2015 * @created 29/04/2015
* @version V1.0.0 * @version V1.0.0
...@@ -323,6 +331,100 @@ class DroitsComponent extends Component { ...@@ -323,6 +331,100 @@ class DroitsComponent extends Component {
return false; return false;
} }
public function assertNotSu()
{
if ($this->isSu() === true) {
throw new ForbiddenException(__d('default', 'default.flasherrorPasDroitPage'));
}
}
public function assertSu()
{
if ($this->isSu() !== true) {
throw new ForbiddenException(__d('default', 'default.flasherrorPasDroitPage'));
}
}
public function assertNotDpo()
{
if ($this->isDpo() === true) {
throw new ForbiddenException(__d('default', 'default.flasherrorPasDroitPage'));
}
}
public function assertDpo()
{
if ($this->isDpo() !== true) {
throw new ForbiddenException(__d('default', 'default.flasherrorPasDroitPage'));
}
}
public function assertLogged()
{
if ($this->isLogged() !== true) {
throw new ForbiddenException(__d('default', 'default.flasherrorPasDroitPage'));
}
}
public function assertAuthorized($level)
{
if ($this->authorized($level) !== true) {
throw new ForbiddenException(__d('default', 'default.flasherrorPasDroitPage'));
}
}
/**
* Vérification de l'existence de l'enregistrement.
*
* @param $modelClass
* @param $id
*/
public function assertRecordExists($modelClass, $id) {
$model = ClassRegistry::init($modelClass);
if ($id !== null) {
$query = [
'fields' => [$model->primaryKey],
'contain' => false,
'conditions' => [
"{$model->alias}.{$model->primaryKey}" => $id
]
];
$record = $model->find('first', $query);
if (empty($record) === true) {
throw new NotFoundException();
}
}
}
public function assertRecordAuthorized($modelClass, $id, array $params = []) {
$params += ['superadmin' => false,'dpo' => false];
$model = ClassRegistry::init($modelClass);
// Vérification de l'existence de l'enregistrement.
if ($id !== null) {
$this->assertRecordExists($modelClass, $id);
//@info: on ne bypass jamais pour le DPO ?
$bypass = (
($params['superadmin'] === true && $this->isSu() === true)
|| ($params['dpo'] === true && $this->isDpo() === true)
);
if ($bypass !== true) {
if ($modelClass === 'Organisation') {
$organisations = [$id];
} else {
if (is_subclass_of($model, 'LinkedOrganisationInterface') === false) {
$msgid = 'La classe de modèle %s doit implémenter l\'interface %s pour utiliser la méthode %s';
throw new RuntimeException(sprintf($msgid, $modelClass, 'LinkedOrganisationInterface', __METHOD__));
}
$organisations = $model->getLinkedOrganisationsIds($id);
}
if (in_array($this->Session->read('Organisation.id'), $organisations) !== true) {
throw new ForbiddenException(__d('default', 'default.flasherrorPasDroitPage'));
}
}
}
}
/** /**
* Check si l'organisation en cours à un DPO déclaré * Check si l'organisation en cours à un DPO déclaré
...@@ -356,4 +458,60 @@ class DroitsComponent extends Component { ...@@ -356,4 +458,60 @@ class DroitsComponent extends Component {
return false; return false;
} }
/**
* Récupère et renvoie un enregistrement en fonction de l'utilisateur connecté (SU ou utilisateur appartenant à
* l'entité ayant créé l'enregistrement).
* L'enregistrement doit être lié à la classe Organisation par une relation hasAndBelongsToMany.
* Vérifie le cas échéant que celui-ci n'est associé à aucun entité.
*
* Utilisé pour vérification avant modification ou suppression d'un sous-traitant.
*
* @throws NotFoundException
* @throws ForbiddenException
*
* @todo: à utiliser pour d'autres contrôleurs
*
* @param string $modelClass Le nom de la classe de modèle de l'enregistrement
* @param int $id L'id de l'enregistrement
* @param bool $checkUnused Doit-on vérifier qu'aucune organisation n'est liée à l'enregistrement ?
* @return array
*/
public function getAndCheckLinkedOrganisationsRecord($modelClass, $id, $checkUnused)
{
$model = ClassRegistry::init($modelClass);
$query = [
'fields' => $model->fields(),
'contain' => [
'Organisation' => [
'fields' => [
'id'
]
]
],
'conditions' => [
"{$model->alias}.{$model->primaryKey}" => $id
]
];
$record = $model->find('first', $query);
if (empty($record) === true) {
throw new NotFoundException();
}
$record['Organisation'] = ['Organisation' => Hash::extract($record, 'Organisation.{n}.id')];
if ($checkUnused === true && count($record['Organisation']['Organisation']) > 0) {
throw new ForbiddenException(__d('default', 'default.flasherrorPasDroitPage'));
}
// A-t-on les droits sur l'enregistrement ?
if ($this->isSu() === false) {
$creator = Hash::get($record, "{$model->alias}.createdbyorganisation");
if ($creator === null || $creator !== $this->Session->read('Organisation.id')) {
throw new ForbiddenException(__d('default', 'default.flasherrorPasDroitPage'));
}
}
return $record;
}
} }
...@@ -49,7 +49,7 @@ class NotificationsComponent extends Component { ...@@ -49,7 +49,7 @@ class NotificationsComponent extends Component {
'vu' => false 'vu' => false
) )
)); ));
if ($notification->save()) { if ($notification->save(null, ['atomic' => true])) {
//$Email = new CakeEmail(); //$Email = new CakeEmail();
//$Email->config('email'); //$Email->config('email');
......
This diff is collapsed.
...@@ -13,7 +13,7 @@ msgstr "" ...@@ -13,7 +13,7 @@ msgstr ""
# ====================================================================================================================== # ======================================================================================================================
####################### Controller/AdminsController.php ####################### ####################### Controller/SoustraitantsController.php #######################
msgid "soustraitant.titreGestionSoustraitantApplication" msgid "soustraitant.titreGestionSoustraitantApplication"
msgstr "Liste des sous-traitants présents dans toute l'application" msgstr "Liste des sous-traitants présents dans toute l'application"
...@@ -22,7 +22,7 @@ msgid "soustraitant.titreGestionSoustraitantEntitee" ...@@ -22,7 +22,7 @@ msgid "soustraitant.titreGestionSoustraitantEntitee"
msgstr "Liste des sous-traitants présents dans l'entité" msgstr "Liste des sous-traitants présents dans l'entité"
msgid "soustraitant.titreAjouterSoustraitant" msgid "soustraitant.titreAjouterSoustraitant"
msgstr "Ajouter un sous-traitant" msgstr "Ajout d'un sous-traitant"
msgid "soustraitant.titreModificationSoustraitant" msgid "soustraitant.titreModificationSoustraitant"
msgstr "Modification d'un sous-traitant" msgstr "Modification d'un sous-traitant"
...@@ -36,11 +36,14 @@ msgstr "Visualisation d'un sous-traitant" ...@@ -36,11 +36,14 @@ msgstr "Visualisation d'un sous-traitant"
#### SUCCESS #### #### SUCCESS ####
msgid "soustraitant.flashsuccessSaveSoustraitant"
msgstr "Le sous-traitant a bien été enregistré"
msgid "soustraitant.flashsuccessDissocierSoustraitantEntite" msgid "soustraitant.flashsuccessDissocierSoustraitantEntite"
msgstr "La dissociation du sous-traitant de l'entité a été enregistrée" msgstr "La dissociation du sous-traitant de l'entité a été enregistrée"
msgid "soustraitant.flashsuccessSuppressionSoustraitantEntite" msgid "soustraitant.flashsuccessSuppressionSoustraitantEntite"
msgstr "La suppression du sous-traitant pour l'entité a été enregistrée" msgstr "La suppression du sous-traitant a été enregistrée"
msgid "soustraitant.flashsuccessSousTraitantAffecterEnregistrer" msgid "soustraitant.flashsuccessSousTraitantAffecterEnregistrer"
msgstr "Le sous-traitant a été associé dans l'entité" msgstr "Le sous-traitant a été associé dans l'entité"
...@@ -49,6 +52,9 @@ msgstr "Le sous-traitant a été associé dans l'entité" ...@@ -49,6 +52,9 @@ msgstr "Le sous-traitant a été associé dans l'entité"
#### ERREUR #### #### ERREUR ####
msgid "soustraitant.flasherrorSaveSoustraitant"
msgstr "Une erreur est survenue lors de l'enregistrement du sous-traitant"
msgid "soustraitant.flasherrorErreurDissocierSoustraitantEntite" msgid "soustraitant.flasherrorErreurDissocierSoustraitantEntite"
msgstr "Une erreur est survenue lors de la dissociation du sous-traitant de l'entité" msgstr "Une erreur est survenue lors de la dissociation du sous-traitant de l'entité"
...@@ -73,6 +79,12 @@ msgstr " Filtrer les sous-traitants" ...@@ -73,6 +79,12 @@ msgstr " Filtrer les sous-traitants"
msgid "soustraitant.champFiltreEntite" msgid "soustraitant.champFiltreEntite"
msgstr "Filtrer par entité" msgstr "Filtrer par entité"
msgid "soustraitant.champFiltreEntiteAssociee"
msgstr "Filtrer par entité associée"
msgid "soustraitant.champFiltreEntiteCreatrice"
msgstr "Filtrer par entité créatrice"
msgid "soustraitant.placeholderChoisirEntite" msgid "soustraitant.placeholderChoisirEntite"
msgstr "Choisir une entité" msgstr "Choisir une entité"
...@@ -116,7 +128,7 @@ msgid "soustraitant.titreTableauRaisonSocialeSoustraitant" ...@@ -116,7 +128,7 @@ msgid "soustraitant.titreTableauRaisonSocialeSoustraitant"
msgstr "Raison sociale" msgstr "Raison sociale"
msgid "soustraitant.titreTableauEntiteSoustraitant" msgid "soustraitant.titreTableauEntiteSoustraitant"
msgstr "Entité" msgstr "Entité(s) associée(s)"
msgid "soustraitant.titreTableauNumeroSiretSoustraitant" msgid "soustraitant.titreTableauNumeroSiretSoustraitant"
msgstr "N° Siret" msgstr "N° Siret"
......
<?php
interface LinkedOrganisationInterface
{
/**
* Retourne les id des entités liées à l'enregistrement.
*
* @param int $id
* @return array
*/
public function getLinkedOrganisationsIds($id);
/**
* Retourne une condition permettant de limiter aux enregistrements liés à une ou plusieurs entités.
*
* @param array|int $organisation_id
* @return string
*/
public function getConditionOrganisation($organisation_id);
}
...@@ -21,8 +21,9 @@ ...@@ -21,8 +21,9 @@
*/ */
App::uses('AppModel', 'Model'); App::uses('AppModel', 'Model');
App::uses('LinkedOrganisationInterface', 'Model/Interface');
class Soustraitant extends AppModel { class Soustraitant extends AppModel implements LinkedOrganisationInterface {
public $name = 'Soustraitant'; public $name = 'Soustraitant';
...@@ -109,4 +110,71 @@ class Soustraitant extends AppModel { ...@@ -109,4 +110,71 @@ class Soustraitant extends AppModel {
'with' => 'SoustraitantOrganisation' 'with' => 'SoustraitantOrganisation'
] ]
]; ];
/**
* Retourne les id des entités liées au sous-traitant.
*
* @param int $id
* @return array
*/
public function getLinkedOrganisationsIds($id) {
$query = [
'fields' => ['organisation_id'],
'conditions' => ['soustraitant_id' => $id],
];
return Hash::extract(
$this->SoustraitantOrganisation->find('all', $query),
'{n}.SoustraitantOrganisation.organisation_id'
);
}
/**
* Retourne une condition permettant de limiter aux sous-traitants liés à une ou plusieurs entités.
*
* @param array|int $organisation_id
* @return string
*/
public function getConditionOrganisation($organisation_id) {
$query = [
'alias' => 'soustraitants_organisations',
'fields' => [
'soustraitants_organisations.soustraitant_id'
],
'conditions' => [
'soustraitants_organisations.organisation_id' => $organisation_id
]
];
$sql = $this->SoustraitantOrganisation->sql($query);
return "{$this->alias}.{$this->primaryKey} IN ( {$sql} )";
}
/**
* Retourne une liste (en clé et en valeur) uniques et triées pour un champ particulier.
*
* @todo: dans un behavior ?
*
* @param string $fieldName
* @param int $organisation_id
* @return array
*/
public function getStringOptionList($fieldName, $organisation_id) {
$query = [
'fields' => [
"{$this->alias}.{$fieldName}",
"{$this->alias}.{$fieldName}",
],
'conditions' => [],
'group' => [
"{$this->alias}.{$fieldName}",
],
'order' => ["{$this->alias}.{$fieldName} ASC"]
];
if ($organisation_id !== null) {
$query['conditions'][] = $this->getConditionOrganisation($organisation_id);
}
return $this->find('list', $query);
}
} }
This diff is collapsed.
<?php
App::uses('AppController', 'Controller');
App::uses( 'ControllerTestCaseAccessTrait', 'Test/Trait/Controller' );
/**
* Tests d'intégration de la classe SoustraitantsController.
*
* ./cake_utils.sh tests app Controller/SoustraitantsController
*
* @package app.Test.Case.Controller
*/
class SoustraitantsControllerTest extends ControllerTestCase
{
use ControllerTestCaseAccessTrait;
public $fixtures = [
'app.Fiche',
'app.ListeDroit',
'app.Notification',
'app.Organisation',
'app.OrganisationUser',
'app.OrganisationUserRole',
'app.Role',
'app.RoleDroit',
'app.Soustraitant',
'app.SoustraitantOrganisation',
'app.User',
'app.Valeur',
];
public function setUp() {
parent::setUp();
$this->controller = $this->generate('Soustraitants');
}
public function dataAccessAdd() {
return [
// 1. Utilisateurs pouvant accéder à la fonctionnalité
[200, 'Superadministrateur.superadmin', '/soustraitants/add'],
[200, 'Administrateur.ibleu', '/soustraitants/add'],
[200, 'DPO.nroux', '/soustraitants/add'],
// 2. Utilisateurs ne pouvant pas accéder à la fonctionnalité
[403, 'Rédacteur.rjaune', '/soustraitants/add'],
[403, 'Valideur.cnoir', '/soustraitants/add'],
[403, 'Consultant.mrose', '/soustraitants/add'],
];
}
/**
* @dataProvider dataAccessAdd
*/
public function testAccessAdd($expectedStatus, $user, $url, $options = []) {
$this->assertActionAccess($expectedStatus, $user, $url, $options);
}
public function dataAccessDelete() {
return [
// 1. Utilisateurs pouvant accéder à la fonctionnalité
// 1.1. Enregistrement existant
// 1.1.1. Créé par le Superadmin
// 1.1.1.1. Non lié à une entité
[302, 'Superadministrateur.superadmin', '/soustraitants/delete/1'],
[403, 'Administrateur.ibleu', '/soustraitants/delete/1'],
[403, 'DPO.nroux', '/soustraitants/delete/1'],
// 1.1.1.2. Lié à une ou des entités
[403, 'Superadministrateur.superadmin', '/soustraitants/delete/2'],
[403, 'Administrateur.ibleu', '/soustraitants/delete/2'],
[403, 'DPO.nroux', '/soustraitants/delete/2'],
// 1.1.2. Créé par une entité
// 1.1.2.1. Non lié à une entité
[302, 'Superadministrateur.superadmin', '/soustraitants/delete/5'],
// 1.1.2.1.1. Pour l'entité créatrice
[302, 'Administrateur.ibleu', '/soustraitants/delete/5'],
[302, 'DPO.nroux', '/soustraitants/delete/5'],
// 1.1.2.1.2. Pour une autre entité que l'entité créatrice
[403, 'Administrateur.findigo', '/soustraitants/delete/5'],
[403, 'DPO.hvermeil', '/soustraitants/delete/5'],
// 1.1.2.2. Lié à une ou des entités
[403, 'Superadministrateur.superadmin', '/soustraitants/delete/6'],
[403, 'Administrateur.ibleu', '/soustraitants/delete/6'],
[403, 'DPO.nroux', '/soustraitants/delete/6'],
// 1.2. Enregistrement inexistant
[404, 'Superadministrateur.superadmin', '/soustraitants/delete/666'],
[404, 'Administrateur.ibleu', '/soustraitants/delete/666'],
[404, 'DPO.nroux', '/soustraitants/delete/666'],
// 2. Utilisateurs ne pouvant pas accéder à la fonctionnalité
// 2.1. Enregistrement existant
// 2.1.1. Créé par le Superadmin
// 2.1.1.1. Non lié à une entité
[403, 'Rédacteur.rjaune', '/soustraitants/delete/1'],
[403, 'Valideur.cnoir', '/soustraitants/delete/1'],
[403, 'Consultant.mrose', '/soustraitants/delete/1'],
// 2.1.1.2. Lié à une ou des entités
[403, 'Rédacteur.rjaune', '/soustraitants/delete/2'],
[403, 'Valideur.cnoir', '/soustraitants/delete/2'],
[403, 'Consultant.mrose', '/soustraitants/delete/2'],
// 2.1.2. Créé par une entité
// 2.1.2.1. Non lié à une entité
[403, 'Rédacteur.rjaune', '/soustraitants/delete/5'],
[403, 'Valideur.cnoir', '/soustraitants/delete/5'],
[403, 'Consultant.mrose', '/soustraitants/delete/5'],
// 2.1.2.2. Lié à une ou des entités
[403, 'Rédacteur.rjaune', '/soustraitants/delete/6'],
[403, 'Valideur.cnoir', '/soustraitants/delete/6'],
[403, 'Consultant.mrose', '/soustraitants/delete/6'],
// 2.2. Enregistrement inexistant
[403, 'Rédacteur.rjaune', '/soustraitants/delete/666'],
[403, 'Valideur.cnoir', '/soustraitants/delete/666'],
[403, 'Consultant.mrose', '/soustraitants/delete/666'],
];
}
/**
* @dataProvider dataAccessDelete
*/
public function testAccessDelete($expectedStatus, $user, $url, $options = []) {
$this->assertActionAccess($expectedStatus, $user, $url, $options);
}
public function dataAccessDissocierSoustraitant() {
return [
// 1. Utilisateurs pouvant accéder à la fonctionnalité
// 1.1. Enregistrement existant et associé
[302, 'Superadministrateur.superadmin', '/soustraitants/dissocierSoustraitant/2'],
[302, 'Administrateur.ibleu', '/soustraitants/dissocierSoustraitant/2'],
[302, 'DPO.nroux', '/soustraitants/dissocierSoustraitant/2'],
// Un utilisateur mono-collectivité ne peut pas accéder à l'enregistrement d'une autre collectivité
[403, 'Administrateur.findigo', '/soustraitants/dissocierSoustraitant/2'],
[403, 'DPO.hvermeil', '/soustraitants/dissocierSoustraitant/2'],
// 1.2. Enregistrement inexistant
[404, 'Superadministrateur.superadmin', '/soustraitants/dissocierSoustraitant/666'],
[404, 'Administrateur.ibleu', '/soustraitants/dissocierSoustraitant/666'],
[404, 'DPO.nroux', '/soustraitants/dissocierSoustraitant/666'],
// 2. Utilisateurs ne pouvant pas accéder à la fonctionnalité
// 1.1. Enregistrement existant
[403, 'Rédacteur.rjaune', '/soustraitants/dissocierSoustraitant/2'],
[403, 'Valideur.cnoir', '/soustraitants/dissocierSoustraitant/2'],
[403, 'Consultant.mrose', '/soustraitants/dissocierSoustraitant/2'],
// 2.2. Enregistrement inexistant
[403, 'Rédacteur.rjaune', '/soustraitants/dissocierSoustraitant/666'],
[403, 'Valideur.cnoir', '/soustraitants/dissocierSoustraitant/666'],
[403, 'Consultant.mrose', '/soustraitants/dissocierSoustraitant/666'],
];
}
/**
* @dataProvider dataAccessDissocierSoustraitant
*/
public function testAccessDissocierSoustraitant($expectedStatus, $user, $url, $options = []) {
$this->assertActionAccess($expectedStatus, $user, $url, $options);