Jun 04, 2012
Update Security Settings: time consuming? Non per forza!
Analisi di un processo tedioso per un manutentore di siti con molti oggetti
Nella mia carriera di tartaruga plonista, mi è capitato diverse volte di lavorare con siti di dimensioni notevoli. Quello a cui più sono affezionato a oggi conta quasi 400K oggetti in catalogo.
Quando il cliente ti chiede di cambiare una piccola impostazione di sicurezza in un workflow associato a 600 o 700 oggetti, tu plonista programmatore e manutentore sai che la to-do list sarà:
- Cambiare il workflow associato all'oggetto;
- Aggiornare il prodotto nell'installazione e ricaricare il profilo legato ai workflow;
- Premere in ZMI Update Security Settings e, se il sito è molto grosso, andare a casa! Ci si pensa la mattina seguente... (se la notte è stata sufficientemente lunga)
Ma non si può proprio fare meglio?
Si! Si può e ora vediamo come.
Cosa succede quando si invoca questo procedura?
446 security.declareProtected(ManagePortal, 'updateRoleMappings')
447 @postonly
448 def updateRoleMappings(self, REQUEST=None):
449 """ Allow workflows to update the role-permission mappings.
450 """
451 wfs = {}
452 for id in self.objectIds():
453 wf = self.getWorkflowById(id)
454 if hasattr(aq_base(wf), 'updateRoleMappingsFor'):
455 wfs[id] = wf
456 portal = aq_parent(aq_inner(self))
457 count = self._recursiveUpdateRoleMappings(portal, wfs)
458 if REQUEST is not None:
459 return self.manage_selectWorkflows(REQUEST, manage_tabs_message=
460 '%d object(s) updated.' % count)
461 else:
462 return count
Da qui come vediamo si richiama il metodo _recursiveUpdateRoleMappings passando gli oggetti portale e i workflow collegati; cosa succede all'interno di questo metodo?
603 ....... 604 if hasattr(aq_base(ob), 'objectItems'): 605 obs = ob.objectItems() 606 if obs: 607 for k, v in obs: 608 changed = getattr(v, '_p_changed', 0) 609 count = count + self._recursiveUpdateRoleMappings(v, wfs) 610 if changed is None: 611 # Re-ghostify. 612 v._p_deactivate() 613 return count
Al suo interno, il metodo, aggiorna la security dell'oggetto e, alla fine, chiama ricorsivamente se stesso su tutti gli oggetti che possono contenere altri oggetti. Si tratta di un algoritmo di visita ricorsiva che visita ogni oggetto del portale!
Questa non può certo essere una procedura veloce quando abbiamo un portale con un grande numero di oggetti. Si pensi poi che fra gli oggetti presi in analisi c'è di tutto:
- front-page
- news
- syndication_information
- crit__Type_ATPortalTypeCriterion
- events
- aggregator
- crit__review_state_ATSimpleStringCriterion
- crit__start_ATFriendlyDateCriteria
- Members
Se quello che vogliamo fare è cambiare il workflow a un tipo di oggetto che conta qualche decina di istanziazioni, questo approccio sembra oltre modo dispendioso.
Quindi... che fare?
Prima di tutto è necessario aver bene in mente cosa si deve fare. I casi possono essere due:
- Voglio cambiare il tipo di workflow a un oggetto;
- Voglio cambiare una serie di permessi, e quindi cambiare mapping, a un workflow già associato a un tipo di oggetto;

Qui possiamo scegliere che workflow associare ad un certo portal type, e Plone stesso ci viene incontro in un'operazione delicata come può essere qualla di rimappare gli stati correnti su quelli che prevede il workflow che andiamo ad assegnare.
Nel secondo caso, dopo aver modificato i permessi di un workflow, abbiamo necessità di aggiornare tutte le istanze di quel tipo di oggetto, ma come fare? Beh, in questo caso è grep a essere tuo amico.
Ogni volta che si sviluppa con Plone, il modo più semplice per fare qualcosa è chiedersi e capire come fa Plone a farlo. Il caso 1, visto poco sopra, cambia completamente workflow a un oggetto e per terminare, dovrà applicare l'update della sicurezza agli oggetti. Il caso 2 sembra essere un subset di operazioni del caso 1.
Se andiamo a vedere in plone/app/controlpanel/types.py, nel codice utilizzato dal pannello di controllo di plone, vediamo che, dopo aver fatto tutta una serie di operazioni riguardanti il cambio di stati fra il vecchio e il nuovo workflow, quello che viene fatto è chiamare:
remap_workflow(context,
type_ids=type_ids,
chain=chain,
state_map=state_map)
Ci siamo quasi! remap_workflow è un metodo in plone/app/workflow/remap.py. Fa un sacco di cose, ed è normale. Non dimentichiamo che deve essere utilizzato da una procedura che cambia completamente il workflow di un'oggetto da un tipo a un altro.
Scorrendo il codice possiamo vedere che ad un certo punto sul nuovo workflow assegnato all'oggetto viene richiamato il metodo updateRoleMappingsFor, che viene utilizzato anche quando si fa l'Update Security settings dalla ZMI.
Ok... Abbiamo trovato il path da seguire!!
Bene, testiamolo!
Consideriamo l'oggetto front-page e il suo workflow, il simple_pubblication_workflow. Nello stato privato, il simple_pubblication_workflow dice che il permesso di View ce l'hanno Contributor, Editor, Manager, Owner, Reader e Site Administrator.

Security mapping dello stato privato del simple pubblication workflow
Infatti se guardiamo in ZMI in nome-sito/front-page/manage_access, abbiamo:

Security matrix di default applicata all'oggetto front-page
Modifichiamo il workflow facendo in modo che nello stato privato nessuno di questi ruoli sia direttamente associato al permesso di View, ma si usi l'Acquire permission settings.
Dopodiché possiamo procedere direttamente tramite pdb:
(STX Next pdb) self.context <ATDocument at /test/front-page> (STX Next pdb) wf = self.context.portal_workflow.getWorkflowsFor(self.context.portal_type) (STX Next pdb) wf [<DCWorkflowDefinition at /test/portal_workflow/simple_publication_workflow>] (STX Next pdb) wf[0].updateRoleMappingsFor(self.context) 1 (STX Next pdb) from transaction import commit (STX Next pdb) commit() None
A questo punto possiamo tornare a vedere direttamente in ZMI cos'è successo. Se andiamo in nome-sito/front-page/manage_access, possiamo vedere che il permesso di View, ora rispecchia fedelmente le modifiche che abbiamo applicato al workflow:

Security matrix di default applicata all'oggetto front-page
Finito?
Non proprio, se leggiamo il codice utilizzato da plone, possiamo vedere che alla fine del metodo viene eseguito anche un:
obj.reindexObject(idxs=['allowedRolesAndUsers', 'review_state'])
Guardando il catalog, vediamo che l'indice allowedRolesAndUsers associato al documento front-page contiene questi valori:
['Contributor', 'Editor', 'Manager', 'Reader', 'Site Administrator', '_View_Permission', 'user:admin']
Noi non abbiamo cambiato il workflow o gli stati dell'oggetto, solo la sua security, per cui un semplice:
obj.reindexObject(idxs=['allowedRolesAndUser'])
sarà sufficiente per sistemare la situazione; e infatti ora in catalogo abbiamo l'indice associato all'unico valore ['Anonymous'].
Finalmente abbiamo tutti gli elementi che ci servono. A questo punto non resta che fare un piccolo script che cerca tutti i brain di un certo tipo in catalogo. Così fare l'update della sicurezza è un'attività semplice e molto più veloce rispetto all'Update security settings fatto dal portal_workflow in ZMI.
Chiaramente, se si modifica un workflow associato a qualche centinaio di migliaia di oggetti... beh... ci aggiorniamo domattina! : )